diff options
| author | tv <tv@krebsco.de> | 2026-11-01 22:29:07 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-01 22:29:07 +0100 |
| commit | b9b54f318a7f7892a9f6e2a4f4fcf2a276492b8c (patch) | |
| tree | 713f4de7df8f7b89e0eece3f0f2fba81e65c48d2 /src/document | |
| parent | deea3053154301308c4a7217b2f741f4e6dd5f1f (diff) | |
api: allow reading past versions
Diffstat (limited to 'src/document')
| -rw-r--r-- | src/document/at.rs | 249 | ||||
| -rw-r--r-- | src/document/mod.rs | 1 | ||||
| -rw-r--r-- | src/document/store.rs | 3 |
3 files changed, 253 insertions, 0 deletions
diff --git a/src/document/at.rs b/src/document/at.rs new file mode 100644 index 0000000..d318f17 --- /dev/null +++ b/src/document/at.rs @@ -0,0 +1,249 @@ +//! The document as it was at given heads: every read of `ReadDoc` made at them (its +//! `*_at` variant), so whatever reads a document (`store`) reads it as it was then. + +use std::ops::RangeBounds; + +use automerge::iter::{DocIter, Keys, ListRange, MapRange, Spans, Values}; +use automerge::marks::{Mark, MarkSet}; +use automerge::{ + AutomergeError, Change, ChangeHash, Cursor, CursorPosition, MoveCursor, ObjId, ObjType, + Parents, Prop, ReadDoc, Stats, TextEncoding, Value, hydrate, +}; + +use super::Document; + +pub(crate) struct At<'a> { + doc: &'a Document, + heads: Vec<ChangeHash>, +} + +impl<'a> At<'a> { + /// `doc` at `heads`; None if it lacks one of them (it hasn't synced it yet, or + /// never will: of another epoch, or no change at all). + pub(crate) fn new(doc: &'a Document, heads: &[ChangeHash]) -> Option<Self> { + heads + .iter() + .all(|head| doc.get_change_by_hash(head).is_some()) + .then(|| Self { + doc, + heads: heads.to_vec(), + }) + } + + fn or_ours<'h>(&'h self, heads: Option<&'h [ChangeHash]>) -> Option<&'h [ChangeHash]> { + Some(heads.unwrap_or(&self.heads)) + } +} + +impl ReadDoc for At<'_> { + fn parents<O: AsRef<ObjId>>(&self, obj: O) -> Result<Parents<'_>, AutomergeError> { + self.doc.parents_at(obj, &self.heads) + } + + fn parents_at<O: AsRef<ObjId>>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result<Parents<'_>, AutomergeError> { + self.doc.parents_at(obj, heads) + } + + fn keys<O: AsRef<ObjId>>(&self, obj: O) -> Keys<'_> { + self.doc.keys_at(obj, &self.heads) + } + + fn keys_at<O: AsRef<ObjId>>(&self, obj: O, heads: &[ChangeHash]) -> Keys<'_> { + self.doc.keys_at(obj, heads) + } + + fn iter_at<O: AsRef<ObjId>>(&self, obj: O, heads: Option<&[ChangeHash]>) -> DocIter<'_> { + self.doc.iter_at(obj, self.or_ours(heads)) + } + + fn map_range<'b, O: AsRef<ObjId>, R: RangeBounds<String> + 'b>( + &'b self, + obj: O, + range: R, + ) -> MapRange<'b> { + self.doc.map_range_at(obj, range, &self.heads) + } + + fn map_range_at<'b, O: AsRef<ObjId>, R: RangeBounds<String> + 'b>( + &'b self, + obj: O, + range: R, + heads: &[ChangeHash], + ) -> MapRange<'b> { + self.doc.map_range_at(obj, range, heads) + } + + fn list_range<O: AsRef<ObjId>, R: RangeBounds<usize>>( + &self, + obj: O, + range: R, + ) -> ListRange<'_> { + self.doc.list_range_at(obj, range, &self.heads) + } + + fn list_range_at<O: AsRef<ObjId>, R: RangeBounds<usize>>( + &self, + obj: O, + range: R, + heads: &[ChangeHash], + ) -> ListRange<'_> { + self.doc.list_range_at(obj, range, heads) + } + + fn values<O: AsRef<ObjId>>(&self, obj: O) -> Values<'_> { + self.doc.values_at(obj, &self.heads) + } + + fn values_at<O: AsRef<ObjId>>(&self, obj: O, heads: &[ChangeHash]) -> Values<'_> { + self.doc.values_at(obj, heads) + } + + fn length<O: AsRef<ObjId>>(&self, obj: O) -> usize { + self.doc.length_at(obj, &self.heads) + } + + fn length_at<O: AsRef<ObjId>>(&self, obj: O, heads: &[ChangeHash]) -> usize { + self.doc.length_at(obj, heads) + } + + fn object_type<O: AsRef<ObjId>>(&self, obj: O) -> Result<ObjType, AutomergeError> { + // An object's type is fixed when it is made. + self.doc.object_type(obj) + } + + fn marks<O: AsRef<ObjId>>(&self, obj: O) -> Result<Vec<Mark>, AutomergeError> { + self.doc.marks_at(obj, &self.heads) + } + + fn marks_at<O: AsRef<ObjId>>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result<Vec<Mark>, AutomergeError> { + self.doc.marks_at(obj, heads) + } + + fn get_marks<O: AsRef<ObjId>>( + &self, + obj: O, + index: usize, + heads: Option<&[ChangeHash]>, + ) -> Result<MarkSet, AutomergeError> { + self.doc.get_marks(obj, index, self.or_ours(heads)) + } + + fn text<O: AsRef<ObjId>>(&self, obj: O) -> Result<String, AutomergeError> { + self.doc.text_at(obj, &self.heads) + } + + fn text_at<O: AsRef<ObjId>>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result<String, AutomergeError> { + self.doc.text_at(obj, heads) + } + + fn spans<O: AsRef<ObjId>>(&self, obj: O) -> Result<Spans<'_>, AutomergeError> { + self.doc.spans_at(obj, &self.heads) + } + + fn spans_at<O: AsRef<ObjId>>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result<Spans<'_>, AutomergeError> { + self.doc.spans_at(obj, heads) + } + + fn get_cursor<O: AsRef<ObjId>, I: Into<CursorPosition>>( + &self, + obj: O, + position: I, + at: Option<&[ChangeHash]>, + ) -> Result<Cursor, AutomergeError> { + self.doc.get_cursor(obj, position, self.or_ours(at)) + } + + fn get_cursor_moving<O: AsRef<ObjId>, I: Into<CursorPosition>>( + &self, + obj: O, + position: I, + at: Option<&[ChangeHash]>, + move_cursor: MoveCursor, + ) -> Result<Cursor, AutomergeError> { + self.doc + .get_cursor_moving(obj, position, self.or_ours(at), move_cursor) + } + + fn get_cursor_position<O: AsRef<ObjId>>( + &self, + obj: O, + cursor: &Cursor, + at: Option<&[ChangeHash]>, + ) -> Result<usize, AutomergeError> { + self.doc.get_cursor_position(obj, cursor, self.or_ours(at)) + } + + fn get<O: AsRef<ObjId>, P: Into<Prop>>( + &self, + obj: O, + prop: P, + ) -> Result<Option<(Value<'_>, ObjId)>, AutomergeError> { + self.doc.get_at(obj, prop, &self.heads) + } + + fn get_at<O: AsRef<ObjId>, P: Into<Prop>>( + &self, + obj: O, + prop: P, + heads: &[ChangeHash], + ) -> Result<Option<(Value<'_>, ObjId)>, AutomergeError> { + self.doc.get_at(obj, prop, heads) + } + + fn hydrate<O: AsRef<ObjId>>( + &self, + obj: O, + heads: Option<&[ChangeHash]>, + ) -> Result<hydrate::Value, AutomergeError> { + ReadDoc::hydrate(self.doc, obj, self.or_ours(heads)) + } + + fn get_all<O: AsRef<ObjId>, P: Into<Prop>>( + &self, + obj: O, + prop: P, + ) -> Result<Vec<(Value<'_>, ObjId)>, AutomergeError> { + self.doc.get_all_at(obj, prop, &self.heads) + } + + fn get_all_at<O: AsRef<ObjId>, P: Into<Prop>>( + &self, + obj: O, + prop: P, + heads: &[ChangeHash], + ) -> Result<Vec<(Value<'_>, ObjId)>, AutomergeError> { + self.doc.get_all_at(obj, prop, heads) + } + + fn get_missing_deps(&self, heads: &[ChangeHash]) -> Vec<ChangeHash> { + self.doc.get_missing_deps(heads) + } + + fn get_change_by_hash(&self, hash: &ChangeHash) -> Option<Change> { + self.doc.get_change_by_hash(hash) + } + + fn stats(&self) -> Stats { + self.doc.stats() + } + + fn text_encoding(&self) -> TextEncoding { + self.doc.text_encoding() + } +} diff --git a/src/document/mod.rs b/src/document/mod.rs index fa1fa77..b9a8b6b 100644 --- a/src/document/mod.rs +++ b/src/document/mod.rs @@ -34,6 +34,7 @@ pub(crate) fn actor(doc: &Document) -> ActorId { doc.get_actor().clone() } +pub(crate) mod at; pub(crate) mod changes; pub(crate) mod file; pub(crate) mod lock; diff --git a/src/document/store.rs b/src/document/store.rs index c2c0622..c16767b 100644 --- a/src/document/store.rs +++ b/src/document/store.rs @@ -94,6 +94,9 @@ pub(crate) enum StoreError { /// An index past a list's end (`length` items). #[error("index {index} is past the list's end ({length} items)")] PastTheEnd { index: usize, length: usize }, + /// A read at heads the document doesn't have (`at::At`). + #[error("this node doesn't have these heads (yet)")] + UnknownHeads, /// The node is rejoining a newer epoch: no writes until it is done /// (`Replica::writable`). #[error("the node is rejoining a newer epoch; writes are refused until it is done")] |
