Skip to main content

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}