Skip to main content

gonzalo_core/
store.rs

1//! The generic storage substrate trait and write-outcome types.
2
3use crate::{ContentHash, Record, RecordKey, Result, Revision};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7/// A detected concurrent-edit conflict: the caller's write expected
8/// `expected` to be the current revision, but the store holds `current`.
9/// Surfaced, never silently resolved.
10#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
11pub struct Conflict {
12    pub key: RecordKey,
13    pub expected: Option<Revision>,
14    pub current: Record,
15}
16
17/// The outcome of a conditional write. `Conflict` is a normal, recoverable
18/// result — not an error.
19#[derive(Clone, Debug, PartialEq, Eq)]
20#[must_use = "a PutResult may be a Conflict that must be handled, never silently dropped"]
21pub enum PutResult {
22    Committed(Revision),
23    Conflict(Box<Conflict>),
24}
25
26/// The outcome of a conditional delete. Like a `Conflict` from `put`, a
27/// `Conflict` here is a normal, recoverable result — not an error.
28#[derive(Clone, Debug, PartialEq, Eq)]
29#[must_use = "a DeleteResult may be a Conflict that must be handled, never silently dropped"]
30pub enum DeleteResult {
31    /// The key is now absent: the record was removed, or there was nothing to
32    /// remove (`expected == None`, or an `expected` revision that was already
33    /// gone). Idempotent.
34    Deleted,
35    /// `expected` was supplied but the store's current revision differs; the
36    /// record was left untouched and `current` holds the live record.
37    Conflict(Box<Conflict>),
38}
39
40/// A pluggable storage substrate over generic records.
41#[async_trait]
42pub trait Store: Send + Sync {
43    /// Fetch a record by key, or `None` if absent.
44    async fn get(&self, key: &RecordKey) -> Result<Option<Record>>;
45
46    /// Conditionally write `record`. `expected` is the revision the caller
47    /// believes is current (`None` means "expect no existing record").
48    /// If the store's current revision differs, returns `PutResult::Conflict`.
49    async fn put(&self, record: Record, expected: Option<Revision>) -> Result<PutResult>;
50
51    /// List keys matching `prefix`.
52    async fn list(&self, prefix: &crate::KeyPrefix) -> Result<Vec<RecordKey>>;
53
54    /// Conditionally delete the record at `key`. `expected` is the revision the
55    /// caller believes is current: `None` deletes unconditionally (idempotent
56    /// no-op if already absent); `Some(rev)` deletes only if the current revision
57    /// matches, returning `DeleteResult::Conflict` if a concurrent write moved it
58    /// first. Deleting an already-absent key is a no-op `Deleted`.
59    ///
60    /// Delete is LOCAL to this store: it is not a tombstone and is NOT propagated
61    /// by `sync` — a later sync against a peer that still holds the record copies
62    /// it back. See ADR 0018.
63    async fn delete(&self, key: &RecordKey, expected: Option<Revision>) -> Result<DeleteResult>;
64}
65
66/// A content-addressed blob store for out-of-line record bodies
67/// ([`Body::Blob`]). Content is keyed by its [`ContentHash`], so byte-identical
68/// bodies — e.g. code-graph slices shared across worktrees (ADR 0012) — are
69/// stored once. Writes are **write-if-absent**: storing content that already
70/// exists is an idempotent no-op, never a conflict (same hash ⇒ same bytes).
71///
72/// [`Body::Blob`]: crate::Body::Blob
73#[async_trait]
74pub trait BlobStore: Send + Sync {
75    /// Store `content` addressed by its hash, write-if-absent, and return the
76    /// hash. Idempotent: storing identical content again is a no-op.
77    async fn put_blob(&self, content: &[u8]) -> Result<ContentHash>;
78
79    /// Fetch blob content by hash, or `None` if absent.
80    async fn get_blob(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>>;
81
82    /// List the hashes of every stored blob. Order is unspecified. Used by GC
83    /// to enumerate candidates for sweeping (ADR 0012).
84    async fn list_blobs(&self) -> Result<Vec<ContentHash>>;
85
86    /// Delete the blob addressed by `hash`. Deleting an absent blob is an
87    /// idempotent no-op — GC may race another sweeper or a re-put.
88    async fn delete_blob(&self, hash: &ContentHash) -> Result<()>;
89}