nodedb_lite/sync/delegate.rs
1//! `SyncDelegate` — the callback interface the transport uses to read pending
2//! work from the owning `NodeDbLite` and to apply inbound state changes.
3//!
4//! Held as `Arc<dyn SyncDelegate>` by the transport so the runner does not
5//! own the database.
6//!
7//! The trait compiles without the `sync` feature. `NodeDbLite` implements it
8//! in every native build, so inbound-apply logic stays testable and usable
9//! by an embedder-provided transport. Only the WebSocket transport that
10//! drives it is feature-gated.
11
12use crate::engine::crdt::engine::PendingDelta;
13use crate::sync::array::PendingArrayOp;
14use crate::sync::outbound::columnar::PendingColumnarBatch;
15use crate::sync::outbound::fts::{PendingFtsDelete, PendingFtsIndex};
16use crate::sync::outbound::kv::PendingKvWrite;
17use crate::sync::outbound::spatial::{PendingSpatialDelete, PendingSpatialInsert};
18use crate::sync::outbound::timeseries::PendingTimeseriesBatch;
19use crate::sync::outbound::vector::{PendingVectorDelete, PendingVectorInsert};
20
21/// Callback interface for the sync runner to read/write pending deltas
22/// from the owning `NodeDbLite`. This avoids the runner owning the database.
23///
24/// **Runtime contract:** All methods are called from inside a Tokio runtime
25/// (via `run_sync_loop`). Implementations MUST NOT use
26/// `tokio::task::block_in_place` to call `&self` async methods — that
27/// pattern panics on `current_thread` runtimes and is exactly what this
28/// trait was redesigned to avoid. Async work (e.g. persisting a synced
29/// function definition to storage) belongs on `import_definition`, which
30/// is `async fn` for that reason. Sync methods that touch only in-memory
31/// state may stay sync.
32#[async_trait::async_trait]
33pub trait SyncDelegate: Send + Sync + 'static {
34 /// This instance's durable identity: `lite_id`, `epoch`, and the Loro peer
35 /// id local operations are authored under.
36 ///
37 /// Read for every frame rather than cached by the transport. All three
38 /// change while a session is alive — a `peer_id_collision` refusal rotates
39 /// the peer id, a reported fork replaces the whole identity — and a frame
40 /// stamped from a stale copy carries exactly the identity Origin refused.
41 fn sync_identity(&self) -> crate::identity::LiteIdentity;
42
43 /// Replace this instance's producer identity after Origin reports a fork,
44 /// re-authoring local state under it.
45 ///
46 /// Async because it persists the new identity and flushes the rebuilt
47 /// documents; a rotation that is not durable resumes the forked identity
48 /// on the next open.
49 async fn regenerate_identity(&self);
50
51 /// Adopt a new Loro peer id after Origin refuses the current one as
52 /// another replica's, re-authoring local state under it.
53 async fn rotate_peer_id(&self);
54
55 /// Get all pending CRDT deltas to push to Origin.
56 fn pending_deltas(&self) -> Vec<PendingDelta>;
57 /// Assign a stable stream seq to a pending CRDT delta (first-send only).
58 ///
59 /// No-op if the delta already has a non-zero seq, so the same seq is
60 /// reused on reconnect re-sends and Origin can deduplicate.
61 async fn set_pending_delta_seq(&self, mutation_id: u64, seq: u64);
62 /// Acknowledge deltas up to the given mutation_id.
63 fn acknowledge(&self, mutation_id: u64);
64 /// Reject a specific delta (rollback optimistic state).
65 fn reject(&self, mutation_id: u64);
66 /// Reject a delta with policy-aware resolution.
67 /// Consults the PolicyRegistry before deciding how to handle the rejection.
68 fn reject_with_policy(
69 &self,
70 mutation_id: u64,
71 hint: &nodedb_types::sync::compensation::CompensationHint,
72 );
73 /// Import remote deltas from Origin into `collection`'s local CRDT state.
74 ///
75 /// The collection travels with the update because each collection has its
76 /// own Loro document; the update bytes alone do not identify the target.
77 fn import_remote(&self, collection: &str, data: &[u8]);
78
79 /// Apply a server-originated row post-image (opcode `0x15`).
80 ///
81 /// Distinct from [`Self::import_remote`], which takes Loro update bytes.
82 /// Origin sends this for writes with no client-authored CRDT operation to
83 /// replicate — SQL DML, and DDL-managed system rows such as retention
84 /// policies and alerts — carrying the row's full post-image (or, for a
85 /// delete, an empty payload and [`RowOp::Delete`]).
86 ///
87 /// A KV collection's post-image is the `{key, value…}` row every KV read
88 /// returns. A payload that is not a row map is a serialization error,
89 /// and nothing is applied.
90 ///
91 /// Async because applying the row writes through to local storage.
92 ///
93 /// [`RowOp::Delete`]: nodedb_types::sync::wire::RowOp::Delete
94 async fn apply_remote_row(
95 &self,
96 msg: &nodedb_types::sync::wire::RowPushMsg,
97 ) -> Result<(), nodedb_types::error::NodeDbError>;
98 /// Import a definition sync message (function/trigger/procedure) from Origin.
99 /// Async because persisting the definition to storage involves
100 /// KV store writes through `spawn_blocking`.
101 async fn import_definition(&self, msg: &nodedb_types::sync::wire::DefinitionSyncMsg);
102
103 /// Materialize a collection announced by a sync peer (opcode `0x13`).
104 ///
105 /// Create-if-absent registers the collection from the descriptor and
106 /// persists authoritative metadata so the SQL catalog surfaces the real
107 /// engine, bitemporal flag, and columns. Async because it performs storage
108 /// reads/writes. Errors are logged and swallowed at the impl boundary so a
109 /// bad announcement never kills the sync session.
110 async fn import_collection_schema(
111 &self,
112 msg: &nodedb_types::sync::wire::CollectionSchemaSyncMsg,
113 );
114
115 /// Apply a single `ArrayDelta` frame from Origin.
116 ///
117 /// An applied or already-present op advances the array's catch-up
118 /// high-water mark to its HLC. Returns the `ArrayAckMsg` to send back to
119 /// Origin (advancing its GC frontier), or `None` if the frame was already
120 /// applied (idempotent) or rejected (logged as a warning).
121 ///
122 /// Async because recording the high-water mark writes to storage.
123 async fn handle_array_delta(
124 &self,
125 msg: &nodedb_types::sync::wire::ArrayDeltaMsg,
126 ) -> Option<nodedb_types::sync::wire::ArrayAckMsg>;
127
128 /// Apply a batch of `ArrayDelta` frames from Origin.
129 ///
130 /// Applies each op in order. The catch-up high-water mark advances to
131 /// the last op before the first rejected op, so a rejected op is asked
132 /// for again on the next catch-up. Returns the `ArrayAckMsg` for the
133 /// highest HLC applied (or `None` if the batch was empty or all ops were
134 /// idempotent/rejected).
135 async fn handle_array_delta_batch(
136 &self,
137 msg: &nodedb_types::sync::wire::ArrayDeltaBatchMsg,
138 ) -> Option<nodedb_types::sync::wire::ArrayAckMsg>;
139
140 /// Process an `ArrayReject` from Origin.
141 ///
142 /// Applies the reason's [`RejectDisposition`]: a `Drop` removes the op
143 /// from the pending queue, and every other disposition keeps it. For
144 /// `RetentionFloor`, persists the array's catch-up flag and returns the
145 /// catch-up request to send: it starts at `Hlc::ZERO`, so Origin answers
146 /// with a snapshot. Returns `None` for every other reason.
147 ///
148 /// [`RejectDisposition`]: crate::sync::array::RejectDisposition
149 async fn handle_array_reject(
150 &self,
151 msg: &nodedb_types::sync::wire::ArrayRejectMsg,
152 ) -> Option<nodedb_types::sync::wire::ArrayCatchupRequestMsg>;
153
154 /// Buffer an `ArraySnapshot` header from Origin. Errors are logged.
155 async fn handle_array_snapshot(&self, msg: &nodedb_types::sync::wire::ArraySnapshotMsg);
156
157 /// Buffer an `ArraySnapshotChunk` from Origin.
158 ///
159 /// On the last chunk the snapshot is assembled and applied: the array's
160 /// catch-up flag is cleared, its high-water mark advances to the
161 /// snapshot HLC, and the `ArrayAckMsg` carrying that HLC is returned.
162 /// Returns `None` while chunks remain. An assembly or apply error is
163 /// logged and flags the array, so the next session asks for a snapshot
164 /// again.
165 async fn handle_array_snapshot_chunk(
166 &self,
167 msg: &nodedb_types::sync::wire::ArraySnapshotChunkMsg,
168 ) -> Option<nodedb_types::sync::wire::ArrayAckMsg>;
169
170 /// Start the array catch-up round for a newly connected session.
171 ///
172 /// Returns one `ArrayCatchupRequestMsg` per array this replica holds,
173 /// each starting at the array's catch-up high-water mark (or `Hlc::ZERO`
174 /// when flagged for a snapshot). Drops partial snapshot assemblies from
175 /// the previous session. Returns an empty list when no arrays exist.
176 fn begin_array_catchup(&self) -> Vec<nodedb_types::sync::wire::ArrayCatchupRequestMsg>;
177
178 // ── Array push ───────────────────────────────────────────────────────────
179 /// Up to `limit` queued local array ops with an HLC strictly greater than
180 /// `after`, in HLC order. `None` starts at the head of the queue.
181 async fn pending_array_ops(
182 &self,
183 after: Option<nodedb_array::sync::hlc::Hlc>,
184 limit: usize,
185 ) -> Result<Vec<PendingArrayOp>, crate::error::LiteError>;
186
187 /// Persist `op.seq` into the queued op before its first send.
188 ///
189 /// On an error the caller does not send, and the op waits for a later
190 /// tick.
191 async fn persist_array_op_seq(
192 &self,
193 op: &PendingArrayOp,
194 ) -> Result<(), crate::error::LiteError>;
195
196 /// Remove the queued array ops named in `hlcs`: ops Origin applied, and
197 /// ops the push loop cannot encode. Ops no longer queued are skipped.
198 async fn retire_array_ops(
199 &self,
200 hlcs: &[nodedb_array::sync::hlc::Hlc],
201 ) -> Result<(), crate::error::LiteError>;
202
203 /// The `ArraySchema` body announcing `array`'s current schema to Origin:
204 /// the schema HLC and Loro snapshot, read together. Returns `None` when
205 /// `array` has no local schema.
206 fn array_schema_announce(
207 &self,
208 array: &str,
209 ) -> Result<Option<nodedb_types::sync::wire::ArraySchemaSyncMsg>, crate::error::LiteError>;
210
211 // ── Columnar ─────────────────────────────────────────────────────────────
212 /// Drain up to `PUSH_DRAIN_LIMIT` pending batches from durable storage,
213 /// skipping any currently in-flight entries.
214 ///
215 /// Returns `(durable_key, batch)` pairs. On successful send, call
216 /// `mark_columnar_batch_in_flight`. The durable entry is deleted only when
217 /// Origin's ack arrives via `ack_columnar_batch_in_flight`.
218 async fn pending_columnar_batches(&self) -> Vec<(Vec<u8>, PendingColumnarBatch)>;
219 /// Record that a columnar batch has been sent and is awaiting Origin ack.
220 async fn mark_columnar_batch_in_flight(&self, batch_id: u64, durable_key: Vec<u8>);
221 /// On Origin ack: remove in-flight record and delete the durable entry.
222 async fn ack_columnar_batch_in_flight(&self, batch_id: u64);
223 /// Delete the durable entry for a confirmed batch directly (used for
224 /// un-encodable entries that must be discarded at send time).
225 async fn acknowledge_columnar_batch(&self, durable_key: Vec<u8>);
226
227 // ── Vector ───────────────────────────────────────────────────────────────
228 /// Drain up to `PUSH_DRAIN_LIMIT` pending insert entries, skipping in-flight.
229 async fn pending_vector_inserts(&self) -> Vec<(Vec<u8>, PendingVectorInsert)>;
230 /// Record that a vector insert has been sent and is awaiting Origin ack.
231 async fn mark_vector_insert_in_flight(&self, batch_id: u64, durable_key: Vec<u8>);
232 /// On Origin ack: remove in-flight record and delete the durable entry.
233 async fn ack_vector_insert_in_flight(&self, batch_id: u64);
234 /// Delete the durable insert entry directly (for un-encodable entries).
235 async fn acknowledge_vector_insert(&self, durable_key: Vec<u8>);
236
237 /// Drain up to `PUSH_DRAIN_LIMIT` pending delete entries, skipping in-flight.
238 async fn pending_vector_deletes(&self) -> Vec<(Vec<u8>, PendingVectorDelete)>;
239 /// Record that a vector delete has been sent and is awaiting Origin ack.
240 async fn mark_vector_delete_in_flight(&self, batch_id: u64, durable_key: Vec<u8>);
241 /// On Origin ack: remove in-flight record and delete the durable entry.
242 async fn ack_vector_delete_in_flight(&self, batch_id: u64);
243 /// Delete the durable delete entry directly (for un-encodable entries).
244 async fn acknowledge_vector_delete(&self, durable_key: Vec<u8>);
245
246 // ── FTS ──────────────────────────────────────────────────────────────────
247 /// Drain up to `PUSH_DRAIN_LIMIT` pending index entries, skipping in-flight.
248 async fn pending_fts_indexes(
249 &self,
250 ) -> Result<Vec<(Vec<u8>, PendingFtsIndex)>, crate::error::LiteError>;
251 /// Record that an FTS index entry has been sent and is awaiting Origin ack.
252 async fn mark_fts_index_in_flight(&self, batch_id: u64, durable_key: Vec<u8>);
253 /// On Origin ack: remove in-flight record and delete the durable entry.
254 async fn ack_fts_index_in_flight(&self, batch_id: u64);
255 /// Delete the durable index entry directly (for un-encodable entries).
256 async fn acknowledge_fts_index(&self, durable_key: Vec<u8>);
257
258 /// Drain up to `PUSH_DRAIN_LIMIT` pending FTS delete entries, skipping in-flight.
259 async fn pending_fts_deletes(
260 &self,
261 ) -> Result<Vec<(Vec<u8>, PendingFtsDelete)>, crate::error::LiteError>;
262 /// Record that an FTS delete entry has been sent and is awaiting Origin ack.
263 async fn mark_fts_delete_in_flight(&self, batch_id: u64, durable_key: Vec<u8>);
264 /// On Origin ack: remove in-flight record and delete the durable entry.
265 async fn ack_fts_delete_in_flight(&self, batch_id: u64);
266 /// Delete the durable delete entry directly (for un-encodable entries).
267 async fn acknowledge_fts_delete(&self, durable_key: Vec<u8>);
268
269 // ── Spatial ──────────────────────────────────────────────────────────────
270 /// Drain up to `PUSH_DRAIN_LIMIT` pending insert entries, skipping in-flight.
271 async fn pending_spatial_inserts(&self) -> Vec<(Vec<u8>, PendingSpatialInsert)>;
272 /// Record that a spatial insert has been sent and is awaiting Origin ack.
273 async fn mark_spatial_insert_in_flight(&self, batch_id: u64, durable_key: Vec<u8>);
274 /// On Origin ack: remove in-flight record and delete the durable entry.
275 async fn ack_spatial_insert_in_flight(&self, batch_id: u64);
276 /// Delete the durable insert entry directly (for un-encodable entries).
277 async fn acknowledge_spatial_insert(&self, durable_key: Vec<u8>);
278
279 /// Drain up to `PUSH_DRAIN_LIMIT` pending spatial delete entries, skipping in-flight.
280 async fn pending_spatial_deletes(&self) -> Vec<(Vec<u8>, PendingSpatialDelete)>;
281 /// Record that a spatial delete has been sent and is awaiting Origin ack.
282 async fn mark_spatial_delete_in_flight(&self, batch_id: u64, durable_key: Vec<u8>);
283 /// On Origin ack: remove in-flight record and delete the durable entry.
284 async fn ack_spatial_delete_in_flight(&self, batch_id: u64);
285 /// Delete the durable delete entry directly (for un-encodable entries).
286 async fn acknowledge_spatial_delete(&self, durable_key: Vec<u8>);
287
288 // ── Timeseries ───────────────────────────────────────────────────────────
289 /// Drain up to `PUSH_DRAIN_LIMIT` pending batches, skipping in-flight entries.
290 async fn pending_timeseries_batches(&self) -> Vec<(Vec<u8>, PendingTimeseriesBatch)>;
291 /// Record that a timeseries batch has been sent and is awaiting Origin ack.
292 ///
293 /// Indexed by `stream_seq` for the cumulative `applied_seq` success path,
294 /// and carrying `batch_id` so a terminally rejected batch — which never
295 /// advances that frontier — can still be named and retired.
296 async fn mark_timeseries_batch_in_flight(
297 &self,
298 stream_seq: u64,
299 batch_id: u64,
300 durable_key: Vec<u8>,
301 );
302 /// On Origin ack: delete all durable entries whose seq ≤ `applied_seq`.
303 async fn ack_timeseries_batches_through_seq(&self, applied_seq: u64);
304 /// On terminal rejection: delete the durable entry for exactly `batch_id`.
305 ///
306 /// Required alongside the sweep above because a rejected batch's seq sits
307 /// *above* `applied_seq` (it never applied), so the cumulative path can
308 /// never retire it and the push loop would re-send it indefinitely.
309 async fn ack_timeseries_batch_by_id(&self, batch_id: u64);
310 /// Delete the durable entry directly (for empty/un-encodable batches).
311 async fn acknowledge_timeseries_batch(&self, durable_key: Vec<u8>);
312
313 // ── KV ───────────────────────────────────────────────────────────────────
314 /// Up to `PUSH_DRAIN_LIMIT` pending KV writes in queue order, skipping
315 /// the ones in flight. Returns `(durable_key, write)` pairs.
316 async fn pending_kv_writes(
317 &self,
318 ) -> Result<Vec<(Vec<u8>, PendingKvWrite)>, crate::error::LiteError>;
319 /// Persist the seq assigned to the KV write stored under `key`.
320 ///
321 /// Called before the frame is sent. On an error the caller does not
322 /// send, and the write waits for the next tick.
323 async fn persist_kv_write_seq(
324 &self,
325 key: &[u8],
326 write: &PendingKvWrite,
327 ) -> Result<(), crate::error::LiteError>;
328 /// Record that the KV write with `batch_id` was sent and awaits its ack.
329 async fn mark_kv_write_in_flight(&self, batch_id: u64);
330 /// Retire the KV write with `batch_id` after Origin's final answer:
331 /// forget it is in flight and delete its durable entry.
332 async fn retire_kv_write(&self, batch_id: u64) -> Result<(), crate::error::LiteError>;
333
334 // ── Stable seq persistence ────────────────────────────────────────────────
335
336 /// Persist an assigned stream seq into the durable columnar entry at `key`.
337 ///
338 /// Must be called before sending the frame. If this returns an error the
339 /// caller must NOT send — it should retain the entry for the next drain tick.
340 async fn persist_columnar_seq(
341 &self,
342 key: &[u8],
343 batch: &PendingColumnarBatch,
344 ) -> Result<(), crate::error::LiteError>;
345
346 /// Persist an assigned stream seq into the durable timeseries entry at `key`.
347 async fn persist_timeseries_seq(
348 &self,
349 key: &[u8],
350 batch: &PendingTimeseriesBatch,
351 ) -> Result<(), crate::error::LiteError>;
352
353 /// Persist an assigned stream seq into the durable vector insert entry at `key`.
354 async fn persist_vector_insert_seq(
355 &self,
356 key: &[u8],
357 insert: &PendingVectorInsert,
358 ) -> Result<(), crate::error::LiteError>;
359
360 /// Persist an assigned stream seq into the durable vector delete entry at `key`.
361 async fn persist_vector_delete_seq(
362 &self,
363 key: &[u8],
364 delete: &PendingVectorDelete,
365 ) -> Result<(), crate::error::LiteError>;
366
367 /// Persist an assigned stream seq into the durable FTS index entry at `key`.
368 async fn persist_fts_index_seq(
369 &self,
370 key: &[u8],
371 entry: &PendingFtsIndex,
372 ) -> Result<(), crate::error::LiteError>;
373
374 /// Persist an assigned stream seq into the durable FTS delete entry at `key`.
375 async fn persist_fts_delete_seq(
376 &self,
377 key: &[u8],
378 entry: &PendingFtsDelete,
379 ) -> Result<(), crate::error::LiteError>;
380
381 /// Persist an assigned stream seq into the durable spatial insert entry at `key`.
382 async fn persist_spatial_insert_seq(
383 &self,
384 key: &[u8],
385 insert: &PendingSpatialInsert,
386 ) -> Result<(), crate::error::LiteError>;
387
388 /// Persist an assigned stream seq into the durable spatial delete entry at `key`.
389 async fn persist_spatial_delete_seq(
390 &self,
391 key: &[u8],
392 delete: &PendingSpatialDelete,
393 ) -> Result<(), crate::error::LiteError>;
394
395 // ── Reconnect ────────────────────────────────────────────────────────────
396
397 /// Clear all engine in-flight maps on reconnect.
398 ///
399 /// The durable entries are still in storage and will be re-drained on the
400 /// next push tick. Origin's idempotent gate deduplicates re-sent batches.
401 async fn clear_engine_in_flight(&self);
402
403 // ── Producer state ───────────────────────────────────────────────────────
404
405 /// Durably persist the server-assigned `producer_id` and `accepted_epoch`
406 /// so they survive process restart and can be reloaded on reconnect.
407 ///
408 /// Called after every successful handshake acknowledgement.
409 async fn persist_producer_state(&self, producer_id: u64, accepted_epoch: u64);
410
411 /// Load the last-persisted `producer_id` and `accepted_epoch`.
412 ///
413 /// Returns `(0, 0)` if no state has been persisted yet (first run).
414 /// Called at the start of each connection attempt so the client has its
415 /// identity available before the outbound handshake is built.
416 async fn load_producer_state(&self) -> (u64, u64);
417
418 // ── Stream sequence ──────────────────────────────────────────────────────
419
420 /// Assign the next monotonic sequence number for the given `stream_id`.
421 ///
422 /// Delegates to `StreamSeqTracker::next_seq` which persists before
423 /// returning (persist-before-send invariant). On storage error, logs a
424 /// warning and returns `0` — a zero seq applies unconditionally on Origin,
425 /// so this is safe degradation with no data loss.
426 async fn next_stream_seq(&self, stream_id: u64) -> u64;
427
428 /// Record that Origin has applied `applied_seq` for `stream_id`.
429 ///
430 /// Delegates to `StreamSeqTracker::record_ack`. Errors are logged and
431 /// ignored — the last_assigned frontier already prevents re-sending
432 /// un-acked seqs, so this is a refinement of the acknowledged frontier.
433 async fn record_stream_ack(&self, stream_id: u64, applied_seq: u64);
434
435 // ── Collection schema (outbound announce) ───────────────────────────────
436
437 /// Look up a collection's persisted metadata by name, for building the
438 /// outbound `CollectionSchema` (opcode `0x13`) announce sent before a
439 /// collection's first CRDT delta in a sync session.
440 ///
441 /// Returns `None` if the collection has no explicitly persisted metadata
442 /// (e.g. an implicit CRDT-only collection created without DDL) — the
443 /// caller skips announcing it and lets Origin infer a schemaless
444 /// collection from the first delta, matching pre-announce behavior.
445 async fn get_collection_meta(
446 &self,
447 name: &str,
448 ) -> Option<crate::nodedb::collection::CollectionMeta>;
449}