Skip to content

Commit 55da2be

Browse files
committed
feat(sync): add RowPush wire message for server-originated writes
Origin needs to fan out row post-images for writes with no client-authored CRDT delta (SQL DML, DDL-managed system rows). Reusing DeltaPush was ambiguous since it carries Loro update bytes rather than a MessagePack post-image, leaving the receiver unable to distinguish the two encodings or the upsert/delete operation. Add a dedicated RowPush message (0x15) with an explicit RowOp and wire it into the sync session loop.
1 parent 1d4a0a8 commit 55da2be

5 files changed

Lines changed: 85 additions & 14 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodedb-types/src/sync/wire/delta.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,59 @@ pub struct CollectionPurgedMsg {
113113
/// Origin WAL LSN at which the hard-delete was committed.
114114
pub purge_lsn: u64,
115115
}
116+
117+
/// Row-level operation carried by a [`RowPushMsg`].
118+
#[derive(
119+
Debug,
120+
Clone,
121+
Copy,
122+
PartialEq,
123+
Eq,
124+
Default,
125+
Serialize,
126+
Deserialize,
127+
zerompk::ToMessagePack,
128+
zerompk::FromMessagePack,
129+
)]
130+
pub enum RowOp {
131+
/// The row was created or updated; `payload` is its post-image.
132+
#[default]
133+
Upsert,
134+
/// The row was removed; `payload` is empty.
135+
Delete,
136+
}
137+
138+
/// Row post-image push (server → client, 0x15).
139+
///
140+
/// Origin sends this for writes that originated on the server — SQL DML, or
141+
/// DDL-managed system rows such as retention policies and alerts — where there
142+
/// is no client-authored CRDT operation to replicate. The peer applies it as a
143+
/// row upsert or delete against its local state.
144+
///
145+
/// Deliberately NOT [`DeltaPushMsg`]: that message is client → server and its
146+
/// `delta` field is Loro update bytes. Carrying a MessagePack post-image in it
147+
/// would leave the receiver unable to tell the two encodings apart, and the
148+
/// operation (upsert vs delete) would have to be inferred from an empty
149+
/// payload rather than stated.
150+
#[derive(
151+
Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
152+
)]
153+
pub struct RowPushMsg {
154+
/// Collection the row belongs to.
155+
pub collection: String,
156+
/// Row / document ID.
157+
pub document_id: String,
158+
/// MessagePack-encoded row post-image. Empty for [`RowOp::Delete`].
159+
pub payload: Vec<u8>,
160+
/// Whether the row was written or removed.
161+
pub op: RowOp,
162+
/// Origin-assigned WAL LSN for the write, when known (`0` otherwise).
163+
#[serde(default)]
164+
pub lsn: u64,
165+
/// Originating peer / node ID.
166+
#[serde(default)]
167+
pub peer_id: u64,
168+
/// Per-collection monotonic sequence, for ordering diagnostics.
169+
#[serde(default)]
170+
pub sequence: u64,
171+
}

nodedb-types/src/sync/wire/frame.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ pub enum SyncMessageType {
3636
/// must drop local Loro state and remove the collection's redb
3737
/// record; future deltas for the collection are not sync-eligible.
3838
CollectionPurged = 0x14,
39+
/// Row post-image push (server → client, 0x15).
40+
///
41+
/// Carries a row's full post-image as MessagePack, NOT Loro update
42+
/// bytes — distinct from [`Self::DeltaPush`], which is client → server
43+
/// and carries a CRDT delta. Origin fans these out for writes that
44+
/// originated on the server (SQL DML, DDL-managed system rows), where no
45+
/// client-authored CRDT operation exists to replicate.
46+
RowPush = 0x15,
3947
ShapeSubscribe = 0x20,
4048
ShapeSnapshot = 0x21,
4149
ShapeDelta = 0x22,
@@ -121,6 +129,7 @@ impl SyncMessageType {
121129
0x12 => Some(Self::DeltaReject),
122130
0x13 => Some(Self::CollectionSchema),
123131
0x14 => Some(Self::CollectionPurged),
132+
0x15 => Some(Self::RowPush),
124133
0x20 => Some(Self::ShapeSubscribe),
125134
0x21 => Some(Self::ShapeSnapshot),
126135
0x22 => Some(Self::ShapeDelta),

nodedb-types/src/sync/wire/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ pub use array::{
8080
};
8181
pub use collection_schema::{CollectionDescriptor, CollectionSchemaSyncMsg};
8282
pub use columnar::{ColumnarInsertAckMsg, ColumnarInsertMsg};
83-
pub use delta::{CollectionPurgedMsg, DeltaAckMsg, DeltaPushMsg, DeltaRejectMsg};
83+
pub use delta::{
84+
CollectionPurgedMsg, DeltaAckMsg, DeltaPushMsg, DeltaRejectMsg, RowOp, RowPushMsg,
85+
};
8486
pub use frame::{SyncFrame, SyncMessageType};
8587
pub use fts::{FtsDeleteAckMsg, FtsDeleteMsg, FtsIndexAckMsg, FtsIndexMsg};
8688
pub use presence::{PeerPresence, PresenceBroadcastMsg, PresenceLeaveMsg, PresenceUpdateMsg};

nodedb/src/control/server/sync/session_handler/session_loop.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -439,20 +439,28 @@ pub(in crate::control::server::sync) async fn handle_sync_session(
439439
.insert(delta.collection.clone());
440440
}
441441

442-
let push_msg = nodedb_types::sync::wire::DeltaPushMsg {
442+
// A row post-image, not a Loro delta — so this goes out as
443+
// `RowPush`, never `DeltaPush`. The two carry different
444+
// encodings, and the peer cannot tell them apart from the
445+
// bytes alone.
446+
let push_msg = nodedb_types::sync::wire::RowPushMsg {
443447
collection: delta.collection,
444448
document_id: delta.document_id,
445-
delta: delta.payload,
449+
payload: delta.payload,
450+
op: match delta.op {
451+
crate::event::crdt_sync::types::DeltaOp::Upsert => {
452+
nodedb_types::sync::wire::RowOp::Upsert
453+
}
454+
crate::event::crdt_sync::types::DeltaOp::Delete => {
455+
nodedb_types::sync::wire::RowOp::Delete
456+
}
457+
},
458+
lsn: delta.lsn,
446459
peer_id: delta.peer_id,
447-
mutation_id: delta.sequence,
448-
checksum: 0,
449-
device_valid_time_ms: None,
450-
producer_id: 0,
451-
epoch: 0,
452-
seq: 0,
460+
sequence: delta.sequence,
453461
};
454462
if let Some(frame) = nodedb_types::sync::wire::SyncFrame::new_msgpack(
455-
nodedb_types::sync::wire::SyncMessageType::DeltaPush,
463+
nodedb_types::sync::wire::SyncMessageType::RowPush,
456464
&push_msg,
457465
) && ws
458466
.send(Message::Binary(frame.to_bytes().into()))

0 commit comments

Comments
 (0)