Skip to main content

gonzalo_core/
ancestry.rs

1//! A [`Store`] decorator that retains each committed body in a content-addressed
2//! [`BlobStore`], keyed by its revision hash, so [`sync`](crate::sync) can fetch
3//! a divergence's common ancestor for a true 3-way merge (ADR 0016).
4
5use async_trait::async_trait;
6
7use crate::{
8    BlobStore, DeleteResult, KeyPrefix, PutResult, Record, RecordKey, Result, Revision, Store,
9};
10
11/// Wraps a record [`Store`] and an ancestry [`BlobStore`]. On a committed `put`
12/// it also writes the record's `body.bytes()` to the ancestry store; because
13/// `Revision.hash == ContentHash::of(body.bytes())`, each version's body is
14/// later retrievable by its revision hash. `get`/`list` delegate unchanged.
15pub struct AncestryStore<S, B> {
16    inner: S,
17    ancestry: B,
18}
19
20impl<S, B> AncestryStore<S, B> {
21    pub fn new(inner: S, ancestry: B) -> Self {
22        Self { inner, ancestry }
23    }
24
25    /// The ancestry blob store (revision hash → body bytes), to hand to
26    /// [`sync_with_ancestry`](crate::sync::sync_with_ancestry).
27    pub fn ancestry(&self) -> &B {
28        &self.ancestry
29    }
30
31    /// The wrapped record store.
32    pub fn inner(&self) -> &S {
33        &self.inner
34    }
35}
36
37#[async_trait]
38impl<S: Store, B: BlobStore> Store for AncestryStore<S, B> {
39    async fn get(&self, key: &RecordKey) -> Result<Option<Record>> {
40        self.inner.get(key).await
41    }
42
43    async fn put(&self, record: Record, expected: Option<Revision>) -> Result<PutResult> {
44        // Retain the body under its revision hash on a successful commit, so a
45        // later divergence can be merged against this exact version.
46        let body_bytes = record.body.bytes().to_vec();
47        let outcome = self.inner.put(record, expected).await?;
48        if matches!(outcome, PutResult::Committed(_)) {
49            self.ancestry.put_blob(&body_bytes).await?;
50        }
51        Ok(outcome)
52    }
53
54    async fn list(&self, prefix: &KeyPrefix) -> Result<Vec<RecordKey>> {
55        self.inner.list(prefix).await
56    }
57
58    async fn delete(&self, key: &RecordKey, expected: Option<Revision>) -> Result<DeleteResult> {
59        // Delete is local and leaves ancestry blobs untouched: retained bodies
60        // stay available for a later divergence's 3-way merge (ADR 0016), and a
61        // sync from a peer may resurrect the record (ADR 0018).
62        self.inner.delete(key, expected).await
63    }
64}
65
66#[cfg(test)]
67pub(crate) mod tests {
68    use super::*;
69    use crate::store::Conflict;
70    use crate::{Body, ContentHash, CoreError, Identity, Meta, RecordKind, revision::Revision};
71    use std::collections::BTreeMap;
72    use std::sync::Mutex;
73
74    /// An in-memory `Store` + `BlobStore` double.
75    #[derive(Default)]
76    pub(crate) struct Mem {
77        records: Mutex<BTreeMap<RecordKey, Record>>,
78        blobs: Mutex<BTreeMap<ContentHash, Vec<u8>>>,
79    }
80
81    #[async_trait]
82    impl Store for Mem {
83        async fn get(&self, key: &RecordKey) -> Result<Option<Record>> {
84            Ok(self.records.lock().unwrap().get(key).cloned())
85        }
86        async fn put(&self, record: Record, expected: Option<Revision>) -> Result<PutResult> {
87            let mut g = self.records.lock().unwrap();
88            let current = g.get(&record.key).map(|r| r.revision.clone());
89            if current != expected {
90                if let Some(cur) = g.get(&record.key).cloned() {
91                    return Ok(PutResult::Conflict(Box::new(Conflict {
92                        key: record.key.clone(),
93                        expected,
94                        current: cur,
95                    })));
96                }
97                return Err(CoreError::NotFound(record.key.clone()));
98            }
99            let rev = record.revision.clone();
100            g.insert(record.key.clone(), record);
101            Ok(PutResult::Committed(rev))
102        }
103        async fn list(&self, prefix: &KeyPrefix) -> Result<Vec<RecordKey>> {
104            Ok(self
105                .records
106                .lock()
107                .unwrap()
108                .keys()
109                .filter(|k| prefix.matches(k))
110                .cloned()
111                .collect())
112        }
113        async fn delete(
114            &self,
115            key: &RecordKey,
116            expected: Option<Revision>,
117        ) -> Result<DeleteResult> {
118            let mut g = self.records.lock().unwrap();
119            match g.get(key) {
120                None => Ok(DeleteResult::Deleted),
121                Some(cur) if expected.is_none() || expected.as_ref() == Some(&cur.revision) => {
122                    g.remove(key);
123                    Ok(DeleteResult::Deleted)
124                }
125                Some(cur) => Ok(DeleteResult::Conflict(Box::new(Conflict {
126                    key: key.clone(),
127                    expected,
128                    current: cur.clone(),
129                }))),
130            }
131        }
132    }
133
134    #[async_trait]
135    impl BlobStore for Mem {
136        async fn put_blob(&self, content: &[u8]) -> Result<ContentHash> {
137            let hash = ContentHash::of(content);
138            self.blobs
139                .lock()
140                .unwrap()
141                .insert(hash.clone(), content.to_vec());
142            Ok(hash)
143        }
144        async fn get_blob(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>> {
145            Ok(self.blobs.lock().unwrap().get(hash).cloned())
146        }
147        async fn list_blobs(&self) -> Result<Vec<ContentHash>> {
148            Ok(self.blobs.lock().unwrap().keys().cloned().collect())
149        }
150        async fn delete_blob(&self, hash: &ContentHash) -> Result<()> {
151            self.blobs.lock().unwrap().remove(hash);
152            Ok(())
153        }
154    }
155
156    pub(crate) fn rec(id: &str, kind: RecordKind, payload: &str) -> Record {
157        let body = Body::Inline(payload.as_bytes().to_vec());
158        Record {
159            revision: Revision::initial(body.bytes()),
160            parent: None,
161            body,
162            kind,
163            key: RecordKey::new("ns", "col", id),
164            meta: Meta {
165                author: Identity::new("t"),
166                origin_system: "test".into(),
167                created: 0,
168                updated: 0,
169                labels: BTreeMap::new(),
170            },
171            links: Vec::new(),
172        }
173    }
174
175    #[tokio::test]
176    async fn put_retains_body_under_revision_hash_and_delegates() {
177        let store = AncestryStore::new(Mem::default(), Mem::default());
178        let r = rec("k", RecordKind::MemoryTier, r#"{"a":1}"#);
179        let rev = r.revision.clone();
180        assert!(matches!(
181            store.put(r.clone(), None).await.unwrap(),
182            PutResult::Committed(_)
183        ));
184
185        // The body is retrievable from the ancestry store by its revision hash.
186        let retained = store.ancestry().get_blob(&rev.hash).await.unwrap();
187        assert_eq!(retained.as_deref(), Some(r.body.bytes()));
188
189        // get/list delegate to the wrapped record store.
190        assert_eq!(store.get(&r.key).await.unwrap().unwrap().revision, rev);
191        assert_eq!(
192            store.list(&KeyPrefix::default()).await.unwrap(),
193            vec![r.key]
194        );
195    }
196
197    #[tokio::test]
198    async fn conflicting_put_does_not_retain() {
199        let store = AncestryStore::new(Mem::default(), Mem::default());
200        let r = rec("k", RecordKind::MemoryTier, r#"{"a":1}"#);
201        let _ = store.put(r.clone(), None).await.unwrap();
202
203        // A stale write (wrong `expected`) conflicts and must not retain.
204        let other = rec("k", RecordKind::MemoryTier, r#"{"a":2}"#);
205        let other_rev = other.revision.clone();
206        assert!(matches!(
207            store.put(other, None).await.unwrap(),
208            PutResult::Conflict(_)
209        ));
210        assert!(
211            store
212                .ancestry()
213                .get_blob(&other_rev.hash)
214                .await
215                .unwrap()
216                .is_none()
217        );
218    }
219}