nodedb_lite/nodedb/core/ops/
crdt.rs1use std::sync::Arc;
7
8use nodedb_types::error::{NodeDbError, NodeDbResult};
9
10use crate::nodedb::core::types::{NodeDbLite, SyncGate};
11use crate::nodedb::lock_ext::LockExt;
12use crate::storage::engine::StorageEngine;
13
14impl<S: StorageEngine> NodeDbLite<S> {
15 pub fn set_sync_gate(&self, gate: Arc<dyn SyncGate>) {
18 let mut slot = self
19 .sync_gate
20 .write()
21 .unwrap_or_else(std::sync::PoisonError::into_inner);
22 *slot = Some(gate);
23 }
24
25 pub(crate) fn should_sync_doc(
28 &self,
29 collection: &str,
30 fields: &std::collections::HashMap<String, nodedb_types::Value>,
31 ) -> bool {
32 match self.sync_gate.read() {
33 Ok(slot) => slot
34 .as_ref()
35 .map(|g| g.should_sync(collection, fields))
36 .unwrap_or(true),
37 Err(_) => true,
38 }
39 }
40
41 pub fn pending_crdt_deltas(
43 &self,
44 ) -> NodeDbResult<Vec<crate::engine::crdt::engine::PendingDelta>> {
45 let crdt = self.crdt.lock_or_recover();
46 Ok(crdt.pending_deltas().to_vec())
47 }
48
49 pub fn acknowledge_deltas(&self, acked_id: u64) -> NodeDbResult<()> {
51 let mut crdt = self.crdt.lock_or_recover();
52 crdt.acknowledge(acked_id);
53 Ok(())
54 }
55
56 pub fn set_crdt_pending_delta_seq(&self, mutation_id: u64, seq: u64) -> NodeDbResult<()> {
61 let mut crdt = self.crdt.lock_or_recover();
62 crdt.set_pending_delta_seq(mutation_id, seq);
63 Ok(())
64 }
65
66 pub fn import_remote_deltas(&self, collection: &str, data: &[u8]) -> NodeDbResult<()> {
71 let guard = self
72 .fts_state
73 .try_admit_mutation("import_remote_deltas")
74 .map_err(NodeDbError::from)?;
75 let result = (|| {
76 let imported = self
77 .crdt
78 .lock_or_recover()
79 .import_remote(collection, data)
80 .map_err(NodeDbError::storage)?;
81 self.reindex_documents_local(
84 collection,
85 imported.changed_rows.iter().map(String::as_str),
86 )?;
87 Ok(())
88 })();
89 guard.finish(result)
90 }
91
92 pub async fn apply_remote_row(
108 &self,
109 collection: &str,
110 document_id: &str,
111 payload: &[u8],
112 delete: bool,
113 ) -> NodeDbResult<()> {
114 let guard = self.fts_state.admit_mutation().await;
115 let result = async {
116 if self.is_kv_collection(collection).await? {
117 return self
118 .kv_apply_remote_row(collection, document_id, payload, delete)
119 .await;
120 }
121 self.apply_remote_document_row(collection, document_id, payload, delete)
122 }
123 .await;
124 guard.finish(result)
125 }
126
127 fn apply_remote_document_row(
129 &self,
130 collection: &str,
131 document_id: &str,
132 payload: &[u8],
133 delete: bool,
134 ) -> NodeDbResult<()> {
135 use crate::nodedb::convert::value_to_loro;
136 use nodedb_types::value::Value;
137
138 let mut crdt = self.crdt.lock_or_recover();
139 let mutation_id = if delete {
140 crdt.delete(collection, document_id)
141 .map_err(NodeDbError::storage)?
142 } else {
143 let value = nodedb_types::value_from_msgpack(payload).map_err(|e| {
146 NodeDbError::serialization(
147 "msgpack",
148 format!("row push for '{collection}'/'{document_id}' does not decode: {e}"),
149 )
150 })?;
151 let Value::Object(fields) = value else {
152 return Err(NodeDbError::serialization(
153 "msgpack",
154 format!(
155 "row push for '{collection}'/'{document_id}' is a {}, not a row map",
156 value.type_name()
157 ),
158 ));
159 };
160 let loro_fields: Vec<(&str, loro::LoroValue)> = fields
161 .iter()
162 .map(|(k, v)| (k.as_str(), value_to_loro(v)))
163 .collect();
164 crdt.upsert(collection, document_id, &loro_fields)
165 .map_err(NodeDbError::from)?
166 };
167 crdt.drop_pending(mutation_id);
168
169 drop(crdt);
170
171 self.reindex_documents_local(collection, [document_id])?;
174 Ok(())
175 }
176
177 pub fn reject_delta(&self, mutation_id: u64) -> NodeDbResult<()> {
179 let guard = self
180 .fts_state
181 .try_admit_mutation("reject_delta")
182 .map_err(NodeDbError::from)?;
183 let result = (|| {
184 let mut crdt = self.crdt.lock_or_recover();
185 let rejected = crdt.reject_delta(mutation_id);
186 drop(crdt);
187 if let Some(delta) = rejected {
188 self.reindex_documents_local(&delta.collection, [delta.document_id.as_str()])?;
189 }
190 Ok(())
191 })();
192 guard.finish(result)
193 }
194
195 #[cfg(all(not(target_arch = "wasm32"), feature = "sync"))]
210 pub fn start_sync(
211 self: &Arc<Self>,
212 config: crate::sync::SyncConfig,
213 ) -> Option<Arc<crate::sync::SyncClient>> {
214 if self.tasks.has(crate::tasks::TaskKind::Sync) {
215 return None;
216 }
217
218 let client = Arc::new(crate::sync::SyncClient::new(config));
219 let delegate: Arc<dyn crate::sync::SyncDelegate> = Arc::clone(self) as _;
220 let client_clone = Arc::clone(&client);
221 let (stop_tx, mut stop) = crate::tasks::TaskRegistry::signal();
222 let handle = crate::runtime::spawn(async move {
223 tokio::select! {
229 _ = crate::sync::run_sync_loop(client_clone, delegate) => {}
230 _ = stop.stopped() => {}
231 }
232 });
233 self.tasks
234 .track(crate::tasks::TaskKind::Sync, stop_tx, handle);
235 Some(client)
236 }
237
238 #[cfg(all(not(target_arch = "wasm32"), feature = "sync"))]
246 pub async fn stop_sync(&self) -> bool {
247 self.tasks.stop(crate::tasks::TaskKind::Sync).await
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use std::collections::HashMap;
254
255 use nodedb_types::value::Value;
256
257 use crate::storage::pagedb_storage::PagedbStorageMem;
258
259 use super::*;
260
261 async fn open_db() -> Arc<NodeDbLite<PagedbStorageMem>> {
262 let storage = PagedbStorageMem::open_in_memory()
263 .await
264 .expect("open in-memory storage");
265 NodeDbLite::open(storage).await.expect("open NodeDbLite")
266 }
267
268 #[tokio::test]
269 async fn a_pushed_document_row_lands_in_its_loro_document() {
270 let db = open_db().await;
271 let mut fields = HashMap::new();
272 fields.insert("name".to_string(), Value::String("ada".into()));
273 let row = nodedb_types::value_to_msgpack(&Value::Object(fields)).expect("encode");
274
275 db.apply_remote_row("people", "p1", &row, false)
276 .await
277 .expect("apply row");
278
279 let stored = db.crdt.lock_or_recover().read("people", "p1");
280 let Some(loro::LoroValue::Map(map)) = stored else {
281 panic!("row p1 is not a map: {stored:?}");
282 };
283 assert_eq!(
284 map.get("name"),
285 Some(&loro::LoroValue::String("ada".to_string().into()))
286 );
287 assert!(db.pending_crdt_deltas().expect("pending").is_empty());
288 }
289
290 #[tokio::test]
291 async fn a_pushed_document_payload_that_is_not_a_row_map_is_refused() {
292 let db = open_db().await;
293 let scalar = nodedb_types::value_to_msgpack(&Value::Integer(1)).expect("encode");
294
295 let err = db
296 .apply_remote_row("people", "p1", &scalar, false)
297 .await
298 .expect_err("a scalar is not a row");
299 assert!(err.message().contains("not a row map"), "{err}");
300 }
301}