1use std::{cmp, collections::HashMap, io, ops::RangeInclusive};
2
3use bytes::BytesMut;
4use opentelemetry::propagation::{Extractor, Injector};
5use rangemap::RangeInclusiveSet;
6use serde::{Deserialize, Serialize};
7use speedy::{Readable, Writable};
8use tokio_util::codec::{Decoder, LengthDelimitedCodec};
9use tracing::warn;
10
11use crate::{
12 actor::ActorId,
13 agent::{Booked, Bookie},
14 base::{CrsqlDbVersion, CrsqlDbVersionRange, CrsqlSeq, CrsqlSeqRange},
15 broadcast::{ChangeV1, Timestamp},
16};
17
18#[derive(Debug, Clone, PartialEq, Readable, Writable)]
19pub enum SyncMessage {
20 V1(SyncMessageV1),
21}
22
23#[derive(Debug, Clone, PartialEq, Readable, Writable)]
24pub enum SyncMessageV1 {
25 State(SyncStateV1),
26 Changeset(ChangeV1),
27 Clock(Timestamp),
28 Rejection(SyncRejectionV1),
29 Request(SyncRequestV1),
30}
31
32#[derive(Debug, Default, Clone, PartialEq, Readable, Writable)]
33pub struct SyncTraceContextV1 {
34 pub traceparent: Option<String>,
35 pub tracestate: Option<String>,
36}
37
38impl Injector for SyncTraceContextV1 {
39 fn set(&mut self, key: &str, value: String) {
40 match key {
41 "traceparent" if !value.is_empty() => self.traceparent = Some(value),
42 "tracestate" if !value.is_empty() => self.tracestate = Some(value),
43 _ => {}
44 }
45 }
46}
47
48impl Extractor for SyncTraceContextV1 {
49 fn get(&self, key: &str) -> Option<&str> {
50 match key {
51 "traceparent" => self.traceparent.as_deref(),
52 "tracestate" => self.tracestate.as_deref(),
53 _ => None,
54 }
55 }
56
57 fn keys(&self) -> Vec<&str> {
58 let mut v = Vec::with_capacity(2);
59 if self.traceparent.is_some() {
60 v.push("traceparent");
61 }
62 if self.tracestate.is_some() {
63 v.push("tracestate");
64 }
65 v
66 }
67}
68
69pub type SyncRequestV1 = Vec<(ActorId, Vec<SyncNeedV1>)>;
70
71#[derive(Debug, thiserror::Error, Clone, PartialEq, Readable, Writable)]
72pub enum SyncRejectionV1 {
73 #[error("max concurrency reached")]
74 MaxConcurrencyReached,
75 #[error("different cluster")]
76 DifferentCluster,
77}
78
79#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
80pub struct SyncStateV1 {
81 pub actor_id: ActorId,
82 pub heads: HashMap<ActorId, CrsqlDbVersion>,
83 pub need: HashMap<ActorId, Vec<RangeInclusive<CrsqlDbVersion>>>,
84 pub partial_need: HashMap<ActorId, HashMap<CrsqlDbVersion, Vec<RangeInclusive<CrsqlSeq>>>>,
85 pub last_cleared_ts: Option<Timestamp>,
86}
87
88impl SyncStateV1 {
89 pub fn need_len(&self) -> u64 {
90 self.need
91 .values()
92 .flat_map(|v| v.iter().map(|range| (range.end().0 - range.start().0) + 1))
93 .sum::<u64>()
94 + (
95 self.partial_need
96 .values()
97 .flat_map(|partials| {
98 partials.values().flat_map(|ranges| {
99 ranges
100 .iter()
101 .map(|range| (range.end().0 - range.start().0) + 1)
102 })
103 })
104 .sum::<u64>()
105 / 50
106 )
108 }
109
110 pub fn need_len_for_actor(&self, actor_id: &ActorId) -> u64 {
111 self.need
112 .get(actor_id)
113 .map(|v| {
114 v.iter()
115 .map(|range| (range.end().0 - range.start().0) + 1)
116 .sum()
117 })
118 .unwrap_or(0)
119 + self
120 .partial_need
121 .get(actor_id)
122 .map(|partials| partials.len() as u64)
123 .unwrap_or(0)
124 }
125
126 pub fn compute_available_needs(
127 &self,
128 other: &SyncStateV1,
129 ) -> HashMap<ActorId, Vec<SyncNeedV1>> {
130 let mut needs: HashMap<ActorId, Vec<SyncNeedV1>> = HashMap::new();
131
132 for (actor_id, head) in other.heads.iter() {
133 if *actor_id == self.actor_id {
134 continue;
135 }
136 if *head == CrsqlDbVersion(0) {
137 warn!(actor_id = %other.actor_id, "sent a 0 head version for actor id {}", actor_id);
138 continue;
139 }
140 let other_haves = {
141 let mut haves =
142 RangeInclusiveSet::from_iter([(CrsqlDbVersion(1)..=*head)].into_iter());
143
144 if let Some(other_need) = other.need.get(actor_id) {
146 for need in other_need.iter() {
147 haves.remove(need.clone());
149 }
150 }
151
152 if let Some(other_partials) = other.partial_need.get(actor_id) {
154 for (v, _) in other_partials.iter() {
155 haves.remove(*v..=*v);
156 }
157 }
158
159 haves
161 };
162
163 if let Some(our_need) = self.need.get(actor_id) {
164 for range in our_need.iter() {
165 for overlap in other_haves.overlapping(range) {
166 let start = cmp::max(range.start(), overlap.start());
167 let end = cmp::min(range.end(), overlap.end());
168 needs.entry(*actor_id).or_default().push(SyncNeedV1::Full {
169 versions: CrsqlDbVersionRange::new(*start, *end),
170 })
171 }
172 }
173 }
174
175 if let Some(our_partials) = self.partial_need.get(actor_id) {
176 for (v, seqs) in our_partials.iter() {
177 if other_haves.contains(v) {
178 needs
179 .entry(*actor_id)
180 .or_default()
181 .push(SyncNeedV1::Partial {
182 version: *v,
183 seqs: seqs.iter().map(CrsqlSeqRange::from).collect(),
184 });
185 } else if let Some(other_seqs) = other
186 .partial_need
187 .get(actor_id)
188 .and_then(|versions| versions.get(v))
189 {
190 let max_other_seq = other_seqs.iter().map(|range| *range.end()).max();
191 let max_our_seq = seqs.iter().map(|range| *range.end()).max();
192
193 let end_seq = cmp::max(max_other_seq, max_our_seq);
194
195 if let Some(end) = end_seq {
196 let mut other_seqs_haves =
197 RangeInclusiveSet::from_iter([CrsqlSeq(0)..=end]);
198
199 for seqs in other_seqs.iter() {
200 other_seqs_haves.remove(seqs.clone());
201 }
202
203 let seqs = seqs
204 .iter()
205 .flat_map(|range| {
206 other_seqs_haves.overlapping(range).map(|overlap| {
207 let start = cmp::max(range.start(), overlap.start());
208 let end = cmp::min(range.end(), overlap.end());
209 CrsqlSeqRange::new(*start, *end)
210 })
211 })
212 .collect::<Vec<CrsqlSeqRange>>();
213
214 if !seqs.is_empty() {
215 needs
216 .entry(*actor_id)
217 .or_default()
218 .push(SyncNeedV1::Partial { version: *v, seqs });
219 }
220 }
221 }
222 }
223 }
224
225 let missing = match self.heads.get(actor_id) {
226 Some(our_head) => {
227 if head > our_head {
228 Some((*our_head + 1)..=*head)
229 } else {
230 None
231 }
232 }
233 None => Some(CrsqlDbVersion(1)..=*head),
234 };
235
236 if let Some(missing) = missing {
237 needs.entry(*actor_id).or_default().push(SyncNeedV1::Full {
238 versions: missing.into(),
239 });
240 }
241 }
242
243 needs
244 }
245}
246
247impl<'a, C> Readable<'a, C> for SyncStateV1
248where
249 C: speedy::Context,
250{
251 fn read_from<R: speedy::Reader<'a, C>>(reader: &mut R) -> Result<Self, C::Error> {
252 let actor_id = ActorId::read_from(reader)?;
253 let heads = HashMap::<ActorId, CrsqlDbVersion>::read_from(reader)?;
254
255 let need_len = usize::read_from(reader)?;
257 let mut need = HashMap::with_capacity(need_len);
258 for _ in 0..need_len {
259 let actor_id = ActorId::read_from(reader)?;
260 let ranges_len = usize::read_from(reader)?;
261 let mut ranges = Vec::with_capacity(ranges_len);
262 for _ in 0..ranges_len {
263 let start = CrsqlDbVersion::read_from(reader)?;
264 let end = CrsqlDbVersion::read_from(reader)?;
265 ranges.push(start..=end);
266 }
267 need.insert(actor_id, ranges);
268 }
269
270 let partial_need_len = usize::read_from(reader)?;
272 let mut partial_need = HashMap::with_capacity(partial_need_len);
273 for _ in 0..partial_need_len {
274 let actor_id = ActorId::read_from(reader)?;
275 let versions_len = usize::read_from(reader)?;
276 let mut versions_map = HashMap::with_capacity(versions_len);
277 for _ in 0..versions_len {
278 let version = CrsqlDbVersion::read_from(reader)?;
279 let seq_ranges_len = usize::read_from(reader)?;
280 let mut seq_ranges = Vec::with_capacity(seq_ranges_len);
281 for _ in 0..seq_ranges_len {
282 let start = CrsqlSeq::read_from(reader)?;
283 let end = CrsqlSeq::read_from(reader)?;
284 seq_ranges.push(start..=end);
285 }
286 versions_map.insert(version, seq_ranges);
287 }
288 partial_need.insert(actor_id, versions_map);
289 }
290
291 let last_cleared_ts = Option::<Timestamp>::read_from(reader)?;
292
293 Ok(SyncStateV1 {
294 actor_id,
295 heads,
296 need,
297 partial_need,
298 last_cleared_ts,
299 })
300 }
301}
302
303impl<C> Writable<C> for SyncStateV1
304where
305 C: speedy::Context,
306{
307 fn write_to<T: ?Sized + speedy::Writer<C>>(&self, writer: &mut T) -> Result<(), C::Error> {
308 self.actor_id.write_to(writer)?;
309 self.heads.write_to(writer)?;
310
311 self.need.len().write_to(writer)?;
313 for (actor_id, ranges) in &self.need {
314 actor_id.write_to(writer)?;
315 ranges.len().write_to(writer)?;
316 for range in ranges {
317 range.start().write_to(writer)?;
318 range.end().write_to(writer)?;
319 }
320 }
321
322 self.partial_need.len().write_to(writer)?;
324 for (actor_id, versions_map) in &self.partial_need {
325 actor_id.write_to(writer)?;
326 versions_map.len().write_to(writer)?;
327 for (version, seq_ranges) in versions_map {
328 version.write_to(writer)?;
329 seq_ranges.len().write_to(writer)?;
330 for range in seq_ranges {
331 range.start().write_to(writer)?;
332 range.end().write_to(writer)?;
333 }
334 }
335 }
336
337 self.last_cleared_ts.write_to(writer)?;
338
339 Ok(())
340 }
341}
342
343#[derive(Debug, Clone, PartialEq)]
344pub enum SyncNeedV1 {
345 Full {
346 versions: CrsqlDbVersionRange,
347 },
348 Partial {
349 version: CrsqlDbVersion,
350 seqs: Vec<CrsqlSeqRange>,
351 },
352 Empty {
353 ts: Option<Timestamp>,
354 },
355}
356
357impl SyncNeedV1 {
358 #[inline]
359 pub fn count(&self) -> usize {
360 match self {
361 SyncNeedV1::Full { versions } => versions.len(),
362 SyncNeedV1::Partial { .. } => 1,
363 SyncNeedV1::Empty { .. } => 1,
364 }
365 }
366}
367
368impl<'a, C> Readable<'a, C> for SyncNeedV1
369where
370 C: speedy::Context,
371{
372 fn read_from<R: speedy::Reader<'a, C>>(reader: &mut R) -> Result<Self, C::Error> {
373 let variant_tag = u8::read_from(reader)?;
374 match variant_tag {
375 0 => {
376 let start = CrsqlDbVersion::read_from(reader)?;
377 let end = CrsqlDbVersion::read_from(reader)?;
378 let versions = (start..=end).into();
379 Ok(SyncNeedV1::Full { versions })
380 }
381 1 => {
382 let version = CrsqlDbVersion::read_from(reader)?;
383 let seqs_len = usize::read_from(reader)?;
384 let mut seqs = Vec::with_capacity(seqs_len);
385 for _ in 0..seqs_len {
386 let start = CrsqlSeq::read_from(reader)?;
387 let end = CrsqlSeq::read_from(reader)?;
388 seqs.push((start..=end).into());
389 }
390 Ok(SyncNeedV1::Partial { version, seqs })
391 }
392 2 => {
393 let ts = Option::<Timestamp>::read_from(reader)?;
394 Ok(SyncNeedV1::Empty { ts })
395 }
396 _ => {
397 let _ = reader.read_u8()?;
399 panic!("Invalid SyncNeedV1 variant tag: {}", variant_tag);
401 }
402 }
403 }
404}
405
406impl<C> Writable<C> for SyncNeedV1
407where
408 C: speedy::Context,
409{
410 fn write_to<T: ?Sized + speedy::Writer<C>>(&self, writer: &mut T) -> Result<(), C::Error> {
411 match self {
412 SyncNeedV1::Full { versions } => {
413 0u8.write_to(writer)?;
414 versions.start().write_to(writer)?;
415 versions.end().write_to(writer)?;
416 }
417 SyncNeedV1::Partial { version, seqs } => {
418 1u8.write_to(writer)?;
419 version.write_to(writer)?;
420 seqs.len().write_to(writer)?;
421 for range in seqs {
422 range.start().write_to(writer)?;
423 range.end().write_to(writer)?;
424 }
425 }
426 SyncNeedV1::Empty { ts } => {
427 2u8.write_to(writer)?;
428 ts.write_to(writer)?;
429 }
430 }
431 Ok(())
432 }
433}
434
435impl From<SyncStateV1> for SyncMessage {
436 fn from(value: SyncStateV1) -> Self {
437 SyncMessage::V1(SyncMessageV1::State(value))
438 }
439}
440
441#[tracing::instrument(skip_all, level = "debug")]
443pub async fn generate_sync(bookie: &Bookie, self_actor_id: ActorId) -> SyncStateV1 {
444 let mut state = SyncStateV1 {
445 actor_id: self_actor_id,
446 ..Default::default()
447 };
448
449 let actors: Vec<(ActorId, Booked)> = {
450 bookie
451 .read::<&str, _>("generate_sync", None)
452 .await
453 .iter()
454 .map(|(k, v)| (*k, v.clone()))
455 .collect()
456 };
457
458 for (actor_id, booked) in actors {
459 let bookedr = booked.read("generate_sync", actor_id.as_simple()).await;
460
461 let last_version = match bookedr.last() {
462 None => continue,
463 Some(v) => v,
464 };
465
466 let need: Vec<_> = bookedr.needed().iter().cloned().collect();
467
468 if !need.is_empty() {
469 state.need.insert(actor_id, need);
470 }
471
472 {
473 for (v, partial) in bookedr
474 .partials
475 .iter()
476 .filter(|(_, partial)| !partial.is_complete())
478 {
479 state.partial_need.entry(actor_id).or_default().insert(
480 *v,
481 partial
482 .seqs
483 .gaps(&(CrsqlSeq(0)..=partial.last_seq))
484 .collect(),
485 );
486 }
487 }
488 state.heads.insert(actor_id, last_version);
489 }
490
491 state
492}
493
494#[derive(Debug, thiserror::Error)]
495pub enum SyncMessageEncodeError {
496 #[error(transparent)]
497 Encode(#[from] speedy::Error),
498 #[error(transparent)]
499 Io(#[from] io::Error),
500}
501
502#[derive(Debug, thiserror::Error)]
503pub enum SyncMessageDecodeError {
504 #[error(transparent)]
505 Decode(#[from] speedy::Error),
506 #[error("corrupted message, crc mismatch (got: {0}, expected {1})")]
507 Corrupted(u32, u32),
508 #[error(transparent)]
509 Io(#[from] io::Error),
510}
511
512impl SyncMessage {
513 pub fn state(&self) -> Option<&SyncStateV1> {
514 match self {
515 SyncMessage::V1(SyncMessageV1::State(state)) => Some(state),
516 _ => None,
517 }
518 }
519
520 pub fn from_slice<S: AsRef<[u8]>>(slice: S) -> Result<Self, speedy::Error> {
521 Self::read_from_buffer(slice.as_ref())
522 }
523
524 pub fn from_buf(buf: &mut BytesMut) -> Result<Self, SyncMessageDecodeError> {
525 Ok(Self::from_slice(buf)?)
526 }
527
528 pub fn decode(
529 codec: &mut LengthDelimitedCodec,
530 buf: &mut BytesMut,
531 ) -> Result<Option<Self>, SyncMessageDecodeError> {
532 Ok(match codec.decode(buf)? {
533 Some(mut buf) => Some(Self::from_buf(&mut buf)?),
534 None => None,
535 })
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use crate::{dbsr, dbsri, dbvr, dbvri};
542 use uuid::Uuid;
543
544 use super::*;
545
546 #[test]
547 fn test_compute_available_needs() {
548 let actor1 = ActorId(Uuid::new_v4());
549
550 let mut our_state = SyncStateV1::default();
551 our_state.heads.insert(actor1, CrsqlDbVersion(10));
552
553 let mut other_state = SyncStateV1::default();
554 other_state.heads.insert(actor1, CrsqlDbVersion(13));
555
556 assert_eq!(
557 our_state.compute_available_needs(&other_state),
558 [(
559 actor1,
560 vec![SyncNeedV1::Full {
561 versions: dbvr!(11, 13)
562 }]
563 )]
564 .into()
565 );
566
567 our_state.need.entry(actor1).or_default().push(dbvri!(2, 5));
568 our_state.need.entry(actor1).or_default().push(dbvri!(7, 7));
569
570 assert_eq!(
571 our_state.compute_available_needs(&other_state),
572 [(
573 actor1,
574 vec![
575 SyncNeedV1::Full {
576 versions: dbvr!(2, 5)
577 },
578 SyncNeedV1::Full {
579 versions: dbvr!(7, 7)
580 },
581 SyncNeedV1::Full {
582 versions: dbvr!(11, 13)
583 }
584 ]
585 )]
586 .into()
587 );
588
589 our_state.partial_need.insert(
590 actor1,
591 [(CrsqlDbVersion(9), vec![dbsri!(100, 120), dbsri!(130, 132)])].into(),
592 );
593
594 assert_eq!(
595 our_state.compute_available_needs(&other_state),
596 [(
597 actor1,
598 vec![
599 SyncNeedV1::Full {
600 versions: dbvr!(2, 5)
601 },
602 SyncNeedV1::Full {
603 versions: dbvr!(7, 7)
604 },
605 SyncNeedV1::Partial {
606 version: CrsqlDbVersion(9),
607 seqs: vec![dbsr!(100, 120), dbsr!(130, 132)]
608 },
609 SyncNeedV1::Full {
610 versions: dbvr!(11, 13)
611 }
612 ]
613 )]
614 .into()
615 );
616
617 other_state.partial_need.insert(
618 actor1,
619 [(CrsqlDbVersion(9), vec![dbsri!(100, 110), dbsri!(130, 130)])].into(),
620 );
621
622 assert_eq!(
623 our_state.compute_available_needs(&other_state),
624 [(
625 actor1,
626 vec![
627 SyncNeedV1::Full {
628 versions: dbvr!(2, 5)
629 },
630 SyncNeedV1::Full {
631 versions: dbvr!(7, 7)
632 },
633 SyncNeedV1::Partial {
634 version: CrsqlDbVersion(9),
635 seqs: vec![dbsr!(111, 120), dbsr!(131, 132)]
636 },
637 SyncNeedV1::Full {
638 versions: dbvr!(11, 13)
639 }
640 ]
641 )]
642 .into()
643 );
644 }
645
646 #[test]
647 fn test_sync_state_v1_serialization() {
648 use speedy::{Readable, Writable};
649 use std::collections::HashMap;
650
651 let actor1 = ActorId(Uuid::new_v4());
652 let actor2 = ActorId(Uuid::new_v4());
653
654 let mut sync_state = SyncStateV1 {
655 actor_id: actor1,
656 heads: HashMap::new(),
657 need: HashMap::new(),
658 partial_need: HashMap::new(),
659 last_cleared_ts: Some(Timestamp::from(12345u64)),
660 };
661
662 sync_state.heads.insert(actor1, CrsqlDbVersion(10));
664 sync_state.heads.insert(actor2, CrsqlDbVersion(20));
665
666 sync_state.need.insert(
668 actor1,
669 vec![
670 CrsqlDbVersion(1)..=CrsqlDbVersion(5),
671 CrsqlDbVersion(8)..=CrsqlDbVersion(9),
672 ],
673 );
674
675 let mut partial_map = HashMap::new();
677 partial_map.insert(
678 CrsqlDbVersion(15),
679 vec![CrsqlSeq(100)..=CrsqlSeq(110), CrsqlSeq(120)..=CrsqlSeq(130)],
680 );
681 sync_state.partial_need.insert(actor2, partial_map);
682
683 let bytes = sync_state.write_to_vec().unwrap();
685
686 let deserialized = SyncStateV1::read_from_buffer(&bytes).unwrap();
688
689 assert_eq!(sync_state, deserialized);
690 }
691
692 #[test]
693 fn test_sync_state_v1_empty_serialization() {
694 use speedy::{Readable, Writable};
695
696 let sync_state = SyncStateV1 {
697 actor_id: ActorId(Uuid::new_v4()),
698 heads: HashMap::new(),
699 need: HashMap::new(),
700 partial_need: HashMap::new(),
701 last_cleared_ts: None,
702 };
703
704 let bytes = sync_state.write_to_vec().unwrap();
705 let deserialized = SyncStateV1::read_from_buffer(&bytes).unwrap();
706
707 assert_eq!(sync_state, deserialized);
708 }
709
710 #[test]
711 fn test_sync_need_v1_full_serialization() {
712 use speedy::{Readable, Writable};
713
714 let sync_need = SyncNeedV1::Full {
715 versions: (CrsqlDbVersion(5)..=CrsqlDbVersion(10)).into(),
716 };
717
718 let bytes = sync_need.write_to_vec().unwrap();
719 let deserialized = SyncNeedV1::read_from_buffer(&bytes).unwrap();
720
721 assert_eq!(sync_need, deserialized);
722 }
723
724 #[test]
725 fn test_sync_need_v1_partial_serialization() {
726 use speedy::{Readable, Writable};
727
728 let sync_need = SyncNeedV1::Partial {
729 version: CrsqlDbVersion(42),
730 seqs: vec![
731 (CrsqlSeq(0)..=CrsqlSeq(10)).into(),
732 (CrsqlSeq(20)..=CrsqlSeq(30)).into(),
733 (CrsqlSeq(50)..=CrsqlSeq(50)).into(),
734 ],
735 };
736
737 let bytes = sync_need.write_to_vec().unwrap();
738 let deserialized = SyncNeedV1::read_from_buffer(&bytes).unwrap();
739
740 assert_eq!(sync_need, deserialized);
741 }
742
743 #[test]
744 fn test_sync_need_v1_empty_serialization() {
745 use speedy::{Readable, Writable};
746
747 let sync_need = SyncNeedV1::Empty {
748 ts: Some(Timestamp::from(67890u64)),
749 };
750
751 let bytes = sync_need.write_to_vec().unwrap();
752 let deserialized = SyncNeedV1::read_from_buffer(&bytes).unwrap();
753
754 assert_eq!(sync_need, deserialized);
755 }
756
757 #[test]
758 fn test_sync_need_v1_empty_with_none_ts() {
759 use speedy::{Readable, Writable};
760
761 let sync_need = SyncNeedV1::Empty { ts: None };
762
763 let bytes = sync_need.write_to_vec().unwrap();
764 let deserialized = SyncNeedV1::read_from_buffer(&bytes).unwrap();
765
766 assert_eq!(sync_need, deserialized);
767 }
768
769 #[test]
770 fn test_sync_need_v1_roundtrip_all_variants() {
771 use speedy::{Readable, Writable};
772
773 let test_cases = vec![
774 SyncNeedV1::Full {
775 versions: (CrsqlDbVersion(1)..=CrsqlDbVersion(1)).into(),
776 },
777 SyncNeedV1::Partial {
778 version: CrsqlDbVersion(10),
779 seqs: vec![],
780 },
781 SyncNeedV1::Partial {
782 version: CrsqlDbVersion(20),
783 seqs: vec![(CrsqlSeq(1)..=CrsqlSeq(5)).into()],
784 },
785 SyncNeedV1::Empty { ts: None },
786 SyncNeedV1::Empty {
787 ts: Some(Timestamp::zero()),
788 },
789 ];
790
791 for (i, original) in test_cases.into_iter().enumerate() {
792 let bytes = original.write_to_vec().unwrap();
793 let deserialized = SyncNeedV1::read_from_buffer(&bytes)
794 .unwrap_or_else(|e| panic!("Failed to deserialize test case {}: {}", i, e));
795
796 assert_eq!(original, deserialized, "Test case {} failed", i);
797 }
798 }
799}