1use nodedb_types::sync::wire::{DeltaAckMsg, DeltaPushMsg, DeltaRejectMsg};
4
5use super::state::SyncClient;
6use crate::engine::crdt::engine::PendingDelta;
7use crate::sync::compensation::CompensationEvent;
8
9impl SyncClient {
10 pub async fn build_delta_pushes(
20 &self,
21 pending: &[PendingDelta],
22 peer_id: u64,
23 ) -> Vec<DeltaPushMsg> {
24 let flow = self.flow.lock().await;
25 let batch_limit = flow.next_batch_size();
26 drop(flow);
27
28 if batch_limit == 0 {
29 return Vec::new();
30 }
31
32 let device_valid_time_ms = crate::runtime::now_millis() as i64;
33
34 {
40 let mut targets = self.delta_targets.lock().await;
41 for delta in pending.iter().take(batch_limit) {
42 targets.insert(
43 delta.mutation_id,
44 (delta.collection.clone(), delta.document_id.clone()),
45 );
46 }
47 }
48
49 pending
50 .iter()
51 .take(batch_limit)
52 .map(|delta| DeltaPushMsg {
53 collection: delta.collection.clone(),
54 document_id: delta.document_id.clone(),
55 checksum: crc32c::crc32c(&delta.delta_bytes),
56 delta: delta.delta_bytes.clone(),
57 peer_id,
58 mutation_id: delta.mutation_id,
59 device_valid_time_ms: Some(device_valid_time_ms),
60 producer_id: 0,
62 epoch: 0,
63 seq: 0,
64 device_id: 0,
70 delta_signature: [0u8; 32],
71 })
72 .collect()
73 }
74
75 pub async fn record_push(&self, mutation_ids: &[u64]) {
77 let mut flow = self.flow.lock().await;
78 flow.record_push(mutation_ids);
79 self.metrics.record_push(mutation_ids.len() as u64);
80 }
81
82 pub async fn handle_delta_ack(&self, ack: &DeltaAckMsg) {
84 self.delta_targets.lock().await.remove(&ack.mutation_id);
87
88 let mut clock = self.clock.lock().await;
89 clock.advance(0, ack.lsn); drop(clock);
91
92 let mut flow = self.flow.lock().await;
93 if let Some(rtt_ms) = flow.record_ack(ack.mutation_id) {
94 tracing::debug!(
95 mutation_id = ack.mutation_id,
96 lsn = ack.lsn,
97 rtt_ms,
98 batch_size = flow.current_batch_size(),
99 "delta acknowledged"
100 );
101 } else {
102 tracing::debug!(
103 mutation_id = ack.mutation_id,
104 lsn = ack.lsn,
105 "delta acknowledged (no in-flight entry)"
106 );
107 }
108
109 if let Some(skew_ms) = ack.clock_skew_warning_ms {
110 tracing::warn!(
111 mutation_id = ack.mutation_id,
112 skew_ms,
113 "Origin reports device clock skew exceeds tolerance"
114 );
115 self.metrics.record_clock_skew_warning();
116 }
117 }
118
119 pub async fn handle_delta_reject(&self, reject: &DeltaRejectMsg) {
121 tracing::warn!(
122 mutation_id = reject.mutation_id,
123 reason = %reject.reason,
124 "delta rejected by Origin"
125 );
126
127 {
128 let mut flow = self.flow.lock().await;
129 flow.record_reject(reject.mutation_id);
130 }
131 self.metrics.record_reject();
132
133 let (collection, document_id) = self
134 .delta_targets
135 .lock()
136 .await
137 .remove(&reject.mutation_id)
138 .unwrap_or_default();
139
140 if let Some(hint) = &reject.compensation {
141 use nodedb_types::sync::compensation::CompensationHint;
142 let is_conflict = match hint {
143 CompensationHint::UniqueViolation { .. }
144 | CompensationHint::ForeignKeyMissing { .. }
145 | CompensationHint::SchemaViolation { .. } => true,
146 CompensationHint::Custom { .. } => true,
150 _ => false,
151 };
152 if is_conflict {
153 self.metrics.record_conflict(&reject.reason);
154 }
155
156 self.compensation.dispatch(CompensationEvent {
157 mutation_id: reject.mutation_id,
158 collection,
159 document_id,
160 hint: hint.clone(),
161 });
162 }
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::sync::client::SyncConfig;
170 use std::sync::Arc;
171
172 fn make_config() -> SyncConfig {
173 SyncConfig::new("wss://localhost:9090/sync", "test.jwt.token")
174 }
175
176 #[tokio::test]
177 async fn build_delta_pushes() {
178 let client = SyncClient::new(make_config());
179 let pending = vec![
180 PendingDelta {
181 mutation_id: 1,
182 collection: "orders".into(),
183 document_id: "o1".into(),
184 delta_bytes: vec![1, 2, 3],
185 seq: 0,
186 },
187 PendingDelta {
188 mutation_id: 2,
189 collection: "users".into(),
190 document_id: "u1".into(),
191 delta_bytes: vec![4, 5, 6],
192 seq: 0,
193 },
194 ];
195
196 let msgs = client.build_delta_pushes(&pending, 42).await;
197 assert_eq!(msgs.len(), 2);
198 assert_eq!(msgs[0].peer_id, 42);
199 assert_eq!(msgs[0].mutation_id, 1);
200 assert_eq!(msgs[1].collection, "users");
201 assert!(msgs[0].device_valid_time_ms.is_some());
202 assert!(msgs[0].device_valid_time_ms.unwrap() > 0);
203 }
204
205 #[tokio::test]
206 async fn handle_delta_ack_advances_clock() {
207 let client = SyncClient::new(make_config());
208 client
209 .handle_delta_ack(&DeltaAckMsg {
210 mutation_id: 1,
211 lsn: 42,
212 clock_skew_warning_ms: None,
213 applied_seq: 0,
214 status: nodedb_types::sync::wire::AckStatus::Applied,
215 })
216 .await;
217
218 let clock = client.clock().lock().await;
219 assert_eq!(clock.get(0), 42); }
221
222 #[tokio::test]
223 async fn handle_delta_reject_dispatches_compensation() {
224 let client = SyncClient::new(make_config());
225
226 let count = Arc::new(std::sync::atomic::AtomicU32::new(0));
227 let count_clone = count.clone();
228 client.set_compensation_handler(Arc::new(move |_: CompensationEvent| {
229 count_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
230 }));
231
232 client
233 .handle_delta_reject(&DeltaRejectMsg {
234 mutation_id: 1,
235 reason: "unique violation".into(),
236 compensation: Some(
237 nodedb_types::sync::compensation::CompensationHint::UniqueViolation {
238 field: "email".into(),
239 conflicting_value: "a@b.com".into(),
240 },
241 ),
242 })
243 .await;
244
245 assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 1);
246 }
247
248 #[tokio::test]
249 async fn delta_push_includes_crc32c() {
250 let client = SyncClient::new(make_config());
251 let delta_bytes = vec![1, 2, 3, 4, 5];
252 let expected_crc = crc32c::crc32c(&delta_bytes);
253 let pending = vec![PendingDelta {
254 mutation_id: 1,
255 collection: "test".into(),
256 document_id: "d1".into(),
257 delta_bytes,
258 seq: 0,
259 }];
260 let msgs = client.build_delta_pushes(&pending, 42).await;
261 assert_eq!(msgs[0].checksum, expected_crc);
262 assert_ne!(msgs[0].checksum, 0);
263 }
264
265 #[tokio::test]
266 async fn flow_control_pauses_when_window_full() {
267 let client = SyncClient::with_flow_control(
268 make_config(),
269 crate::sync::flow_control::FlowControlConfig {
270 max_in_flight: 2,
271 initial_batch_size: 10,
272 ..Default::default()
273 },
274 );
275 let pending = vec![
276 PendingDelta {
277 mutation_id: 1,
278 collection: "a".into(),
279 document_id: "d1".into(),
280 delta_bytes: vec![1],
281 seq: 0,
282 },
283 PendingDelta {
284 mutation_id: 2,
285 collection: "a".into(),
286 document_id: "d2".into(),
287 delta_bytes: vec![2],
288 seq: 0,
289 },
290 PendingDelta {
291 mutation_id: 3,
292 collection: "a".into(),
293 document_id: "d3".into(),
294 delta_bytes: vec![3],
295 seq: 0,
296 },
297 ];
298
299 let msgs = client.build_delta_pushes(&pending, 42).await;
300 assert_eq!(msgs.len(), 2);
301
302 client.record_push(&[1, 2]).await;
303
304 let msgs = client.build_delta_pushes(&pending, 42).await;
305 assert_eq!(msgs.len(), 0);
306
307 client
308 .handle_delta_ack(&DeltaAckMsg {
309 mutation_id: 1,
310 lsn: 10,
311 clock_skew_warning_ms: None,
312 applied_seq: 0,
313 status: nodedb_types::sync::wire::AckStatus::Applied,
314 })
315 .await;
316 let msgs = client.build_delta_pushes(&pending, 42).await;
317 assert_eq!(msgs.len(), 1);
318 }
319}