atproto blogging
1//! CRDT document trait and sync state tracking.
2
3use loro::VersionVector;
4use weaver_api::com_atproto::repo::strong_ref::StrongRef;
5
6/// Sync state for a CRDT document.
7///
8/// Tracks the edit root, last diff, and version at last sync.
9#[derive(Clone, Debug, Default)]
10pub struct SyncState {
11 /// StrongRef to the sh.weaver.edit.root record.
12 pub edit_root: Option<StrongRef<'static>>,
13
14 /// StrongRef to the most recent sh.weaver.edit.diff record.
15 pub last_diff: Option<StrongRef<'static>>,
16
17 /// Version vector at the time of last sync.
18 pub last_synced_version: Option<VersionVector>,
19}
20
21impl SyncState {
22 /// Create new empty sync state.
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 /// Check if we have an edit root (i.e., have synced at least once).
28 pub fn has_root(&self) -> bool {
29 self.edit_root.is_some()
30 }
31}
32
33/// Trait for CRDT documents that can be synced to AT Protocol PDS.
34///
35/// Implementors provide access to the underlying CRDT operations
36/// and sync state tracking.
37pub trait CrdtDocument {
38 /// Export full snapshot bytes.
39 fn export_snapshot(&self) -> Vec<u8>;
40
41 /// Export updates since the last synced version.
42 /// Returns None if no changes since last sync.
43 fn export_updates_since_sync(&self) -> Option<Vec<u8>>;
44
45 /// Import remote changes.
46 fn import(&mut self, data: &[u8]) -> Result<(), crate::CrdtError>;
47
48 /// Get current version vector.
49 fn version(&self) -> VersionVector;
50
51 /// Get the edit root StrongRef.
52 fn edit_root(&self) -> Option<StrongRef<'static>>;
53
54 /// Set the edit root StrongRef.
55 fn set_edit_root(&mut self, root: Option<StrongRef<'static>>);
56
57 /// Get the last diff StrongRef.
58 fn last_diff(&self) -> Option<StrongRef<'static>>;
59
60 /// Set the last diff StrongRef.
61 fn set_last_diff(&mut self, diff: Option<StrongRef<'static>>);
62
63 /// Mark current version as synced.
64 fn mark_synced(&mut self);
65
66 /// Check if there are changes since last sync.
67 fn has_unsynced_changes(&self) -> bool;
68}
69
70// Blanket implementation for LoroTextBuffer with embedded SyncState
71// (Concrete types can provide their own implementations)
72
73/// A simple CRDT document wrapping LoroTextBuffer with sync state.
74pub struct SimpleCrdtDocument {
75 buffer: crate::LoroTextBuffer,
76 sync_state: SyncState,
77}
78
79impl SimpleCrdtDocument {
80 /// Create a new empty document.
81 pub fn new() -> Self {
82 Self {
83 buffer: crate::LoroTextBuffer::new(),
84 sync_state: SyncState::new(),
85 }
86 }
87
88 /// Create from snapshot.
89 pub fn from_snapshot(snapshot: &[u8]) -> Result<Self, crate::CrdtError> {
90 Ok(Self {
91 buffer: crate::LoroTextBuffer::from_snapshot(snapshot)?,
92 sync_state: SyncState::new(),
93 })
94 }
95
96 /// Get the underlying buffer.
97 pub fn buffer(&self) -> &crate::LoroTextBuffer {
98 &self.buffer
99 }
100
101 /// Get mutable access to the buffer.
102 pub fn buffer_mut(&mut self) -> &mut crate::LoroTextBuffer {
103 &mut self.buffer
104 }
105}
106
107impl Default for SimpleCrdtDocument {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113impl CrdtDocument for SimpleCrdtDocument {
114 fn export_snapshot(&self) -> Vec<u8> {
115 self.buffer.export_snapshot()
116 }
117
118 fn export_updates_since_sync(&self) -> Option<Vec<u8>> {
119 self.sync_state
120 .last_synced_version
121 .as_ref()
122 .and_then(|v| self.buffer.export_updates_since(v))
123 }
124
125 fn import(&mut self, data: &[u8]) -> Result<(), crate::CrdtError> {
126 self.buffer.import(data)
127 }
128
129 fn version(&self) -> VersionVector {
130 self.buffer.version()
131 }
132
133 fn edit_root(&self) -> Option<StrongRef<'static>> {
134 self.sync_state.edit_root.clone()
135 }
136
137 fn set_edit_root(&mut self, root: Option<StrongRef<'static>>) {
138 self.sync_state.edit_root = root;
139 }
140
141 fn last_diff(&self) -> Option<StrongRef<'static>> {
142 self.sync_state.last_diff.clone()
143 }
144
145 fn set_last_diff(&mut self, diff: Option<StrongRef<'static>>) {
146 self.sync_state.last_diff = diff;
147 }
148
149 fn mark_synced(&mut self) {
150 self.sync_state.last_synced_version = Some(self.buffer.version());
151 }
152
153 fn has_unsynced_changes(&self) -> bool {
154 match &self.sync_state.last_synced_version {
155 None => true, // Never synced
156 Some(last) => self.buffer.version() != *last,
157 }
158 }
159}