1use nodedb_types::sync::wire::{
4 ArrayAckMsg, ArrayCatchupRequestMsg, ResyncReason, ResyncRequestMsg, ShapeDeltaMsg,
5 ShapeSnapshotMsg, VectorClockSyncMsg,
6};
7
8use super::state::SyncClient;
9
10impl SyncClient {
11 pub async fn handle_shape_snapshot(&self, msg: &ShapeSnapshotMsg) {
18 let mut shapes = self.shapes.lock().await;
19 shapes.mark_snapshot_loaded(&msg.shape_id, msg.snapshot_lsn);
20 drop(shapes);
21
22 self.last_seen_lsn
25 .lock()
26 .await
27 .insert(msg.shape_id.clone(), msg.snapshot_lsn);
28
29 *self.resync_requested.lock().await = false;
31 *self.pending_resync.lock().await = None;
32
33 tracing::info!(
34 shape_id = %msg.shape_id,
35 lsn = msg.snapshot_lsn,
36 doc_count = msg.doc_count,
37 "shape snapshot received"
38 );
39 }
40
41 pub async fn handle_shape_delta(&self, msg: &ShapeDeltaMsg) {
43 let mut shapes = self.shapes.lock().await;
44 shapes.advance_lsn(&msg.shape_id, msg.lsn);
45 tracing::debug!(
46 shape_id = %msg.shape_id,
47 collection = %msg.collection,
48 doc_id = %msg.document_id,
49 lsn = msg.lsn,
50 "shape delta received"
51 );
52 }
53
54 pub async fn handle_clock_sync(&self, msg: &VectorClockSyncMsg) {
56 let mut clock = self.clock.lock().await;
57 for (peer_hex, &counter) in &msg.clocks {
58 if let Ok(peer_id) = u64::from_str_radix(peer_hex, 16) {
59 clock.advance(peer_id, counter);
60 }
61 }
62 }
63
64 pub async fn check_sequence_gap(&self, shape_id: &str, lsn: u64) -> Option<ResyncRequestMsg> {
70 if *self.resync_requested.lock().await {
71 return None;
72 }
73
74 let mut tracker = self.last_seen_lsn.lock().await;
75 if let Some(&last_lsn) = tracker.get(shape_id)
76 && lsn > last_lsn + 1
77 {
78 tracing::warn!(
79 shape_id,
80 expected = last_lsn + 1,
81 received = lsn,
82 "sequence gap detected in incoming delta stream"
83 );
84 tracker.insert(shape_id.to_string(), lsn);
85
86 *self.resync_requested.lock().await = true;
87
88 return Some(ResyncRequestMsg {
89 reason: ResyncReason::SequenceGap {
90 expected: last_lsn + 1,
91 received: lsn,
92 },
93 from_mutation_id: last_lsn + 1,
94 collection: String::new(),
95 shape_id: shape_id.to_string(),
96 });
97 }
98 tracker.insert(shape_id.to_string(), lsn);
99 None
100 }
101
102 pub async fn reset_sequence_tracking(&self) {
104 self.last_seen_lsn.lock().await.clear();
105 *self.resync_requested.lock().await = false;
106 *self.pending_resync.lock().await = None;
107 }
108
109 pub async fn set_pending_resync(&self, msg: ResyncRequestMsg) {
111 *self.pending_resync.lock().await = Some(msg);
112 }
113
114 pub async fn take_pending_resync(&self) -> Option<ResyncRequestMsg> {
116 self.pending_resync.lock().await.take()
117 }
118
119 pub async fn set_pending_array_ack(&self, msg: ArrayAckMsg) {
128 let mut map = self.pending_array_ack.lock().await;
129 let entry = map.entry(msg.array.clone()).or_insert_with(|| msg.clone());
130 if msg.ack_hlc_bytes > entry.ack_hlc_bytes {
131 *entry = msg;
132 }
133 }
134
135 pub async fn drain_pending_array_acks(&self) -> Vec<ArrayAckMsg> {
140 let mut map = self.pending_array_ack.lock().await;
141 if map.is_empty() {
142 return Vec::new();
143 }
144 let drained: Vec<ArrayAckMsg> = map.drain().map(|(_, v)| v).collect();
145 drained
146 }
147
148 pub async fn reset_array_catchups(&self, requests: Vec<ArrayCatchupRequestMsg>) {
153 let mut map = self.pending_array_catchups.lock().await;
154 map.clear();
155 map.extend(requests.into_iter().map(|r| (r.array.clone(), r)));
156 }
157
158 pub async fn queue_array_catchup(&self, request: ArrayCatchupRequestMsg) {
161 self.pending_array_catchups
162 .lock()
163 .await
164 .insert(request.array.clone(), request);
165 }
166
167 pub async fn drain_array_catchups(&self) -> Vec<ArrayCatchupRequestMsg> {
170 std::mem::take(&mut *self.pending_array_catchups.lock().await)
171 .into_values()
172 .collect()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::sync::client::SyncConfig;
180
181 fn make_config() -> SyncConfig {
182 SyncConfig::new("wss://localhost:9090/sync", "test.jwt.token")
183 }
184
185 #[tokio::test]
186 async fn shape_snapshot_updates_manager() {
187 let client = SyncClient::new(make_config());
188 {
189 let mut shapes = client.shapes().lock().await;
190 shapes.subscribe(nodedb_types::sync::shape::ShapeDefinition {
191 shape_id: "s1".into(),
192 tenant_id: 1,
193 shape_type: nodedb_types::sync::shape::ShapeType::Vector {
194 collection: "vecs".into(),
195 field_name: None,
196 },
197 description: "test".into(),
198 field_filter: vec![],
199 });
200 }
201
202 client
203 .handle_shape_snapshot(&ShapeSnapshotMsg {
204 shape_id: "s1".into(),
205 data: Vec::new(),
206 snapshot_lsn: 100,
207 doc_count: 50,
208 })
209 .await;
210
211 let shapes = client.shapes().lock().await;
212 let sub = shapes.get("s1").unwrap();
213 assert!(sub.snapshot_loaded);
214 assert_eq!(sub.last_lsn, 100);
215 }
216
217 #[tokio::test]
218 async fn sequence_gap_detection_no_gap() {
219 let client = SyncClient::new(make_config());
220 assert!(client.check_sequence_gap("s1", 1).await.is_none());
221 assert!(client.check_sequence_gap("s1", 2).await.is_none());
222 assert!(client.check_sequence_gap("s1", 3).await.is_none());
223 }
224
225 #[tokio::test]
226 async fn sequence_gap_detection_with_gap() {
227 let client = SyncClient::new(make_config());
228 assert!(client.check_sequence_gap("s1", 1).await.is_none());
229 let resync = client.check_sequence_gap("s1", 5).await;
230 assert!(resync.is_some());
231 let msg = resync.unwrap();
232 assert_eq!(msg.from_mutation_id, 2);
233 assert!(matches!(
234 msg.reason,
235 ResyncReason::SequenceGap {
236 expected: 2,
237 received: 5
238 }
239 ));
240 }
241
242 #[tokio::test]
243 async fn sequence_gap_only_one_resync_per_connection() {
244 let client = SyncClient::new(make_config());
245 assert!(client.check_sequence_gap("s1", 1).await.is_none());
246 assert!(client.check_sequence_gap("s1", 10).await.is_some());
247 assert!(client.check_sequence_gap("s1", 20).await.is_none());
248 }
249
250 #[tokio::test]
251 async fn array_catchup_queue_keeps_one_request_per_array_in_name_order() {
252 use nodedb_types::sync::wire::ArrayCatchupRequestMsg;
253 let req = |array: &str, byte: u8| ArrayCatchupRequestMsg {
254 array: array.into(),
255 from_hlc_bytes: [byte; 18],
256 };
257 let client = SyncClient::new(make_config());
258
259 client.queue_array_catchup(req("stale", 1)).await;
260 client
261 .reset_array_catchups(vec![req("b", 2), req("a", 3)])
262 .await;
263 client.queue_array_catchup(req("b", 0)).await;
264
265 let drained = client.drain_array_catchups().await;
266 assert_eq!(drained, vec![req("a", 3), req("b", 0)]);
267 assert!(client.drain_array_catchups().await.is_empty());
268 }
269
270 #[tokio::test]
271 async fn array_ack_merge_keeps_highest_hlc() {
272 let client = SyncClient::new(make_config());
273
274 let lower = ArrayAckMsg {
276 array: "arr1".into(),
277 replica_id: 1,
278 ack_hlc_bytes: [0x00; 18],
279 applied_seq: 0,
280 status: nodedb_types::sync::wire::AckStatus::Applied,
281 };
282 let higher = ArrayAckMsg {
283 array: "arr1".into(),
284 replica_id: 1,
285 ack_hlc_bytes: [0xFF; 18],
286 applied_seq: 0,
287 status: nodedb_types::sync::wire::AckStatus::Applied,
288 };
289
290 client.set_pending_array_ack(lower).await;
291 client.set_pending_array_ack(higher.clone()).await;
292
293 let drained = client.drain_pending_array_acks().await;
294 assert_eq!(drained.len(), 1);
295 assert_eq!(drained[0].ack_hlc_bytes, higher.ack_hlc_bytes);
296 }
297
298 #[tokio::test]
299 async fn array_ack_merge_keeps_higher_over_lower() {
300 let client = SyncClient::new(make_config());
301
302 let mut higher_bytes = [0x00u8; 18];
304 higher_bytes[0] = 0x10;
305 let mut lower_bytes = [0x00u8; 18];
306 lower_bytes[0] = 0x01;
307
308 client
309 .set_pending_array_ack(ArrayAckMsg {
310 array: "arr2".into(),
311 replica_id: 1,
312 ack_hlc_bytes: higher_bytes,
313 applied_seq: 0,
314 status: nodedb_types::sync::wire::AckStatus::Applied,
315 })
316 .await;
317 client
318 .set_pending_array_ack(ArrayAckMsg {
319 array: "arr2".into(),
320 replica_id: 1,
321 ack_hlc_bytes: lower_bytes,
322 applied_seq: 0,
323 status: nodedb_types::sync::wire::AckStatus::Applied,
324 })
325 .await;
326
327 let drained = client.drain_pending_array_acks().await;
328 assert_eq!(drained.len(), 1);
329 assert_eq!(drained[0].ack_hlc_bytes, higher_bytes);
330 }
331
332 #[tokio::test]
333 async fn array_ack_merge_separate_arrays_both_drained() {
334 let client = SyncClient::new(make_config());
335
336 client
337 .set_pending_array_ack(ArrayAckMsg {
338 array: "arr_a".into(),
339 replica_id: 1,
340 ack_hlc_bytes: [0x01; 18],
341 applied_seq: 0,
342 status: nodedb_types::sync::wire::AckStatus::Applied,
343 })
344 .await;
345 client
346 .set_pending_array_ack(ArrayAckMsg {
347 array: "arr_b".into(),
348 replica_id: 1,
349 ack_hlc_bytes: [0x02; 18],
350 applied_seq: 0,
351 status: nodedb_types::sync::wire::AckStatus::Applied,
352 })
353 .await;
354
355 let mut drained = client.drain_pending_array_acks().await;
356 drained.sort_by(|a, b| a.array.cmp(&b.array));
357 assert_eq!(drained.len(), 2);
358 assert_eq!(drained[0].array, "arr_a");
359 assert_eq!(drained[1].array, "arr_b");
360 }
361
362 #[tokio::test]
363 async fn reset_sequence_tracking_clears_state() {
364 let client = SyncClient::new(make_config());
365 assert!(client.check_sequence_gap("s1", 1).await.is_none());
366 assert!(client.check_sequence_gap("s1", 10).await.is_some());
367
368 client.reset_sequence_tracking().await;
369
370 assert!(client.check_sequence_gap("s1", 1).await.is_none());
371 assert!(client.check_sequence_gap("s1", 5).await.is_some());
372 }
373}