Skip to main content

klukai_types/
sync.rs

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                // this is how many chunks we're looking at, kind of random...
107            )
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                // remove needs
145                if let Some(other_need) = other.need.get(actor_id) {
146                    for need in other_need.iter() {
147                        // create gaps
148                        haves.remove(need.clone());
149                    }
150                }
151
152                // remove partials
153                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                // we are left with all the versions they fully have!
160                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        // Read need: HashMap<ActorId, Vec<RangeInclusive<CrsqlDbVersion>>>
256        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        // Read partial_need: HashMap<ActorId, HashMap<CrsqlDbVersion, Vec<RangeInclusive<CrsqlSeq>>>>
271        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        // Write need: HashMap<ActorId, Vec<RangeInclusive<CrsqlDbVersion>>>
312        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        // Write partial_need: HashMap<ActorId, HashMap<CrsqlDbVersion, Vec<RangeInclusive<CrsqlSeq>>>>
323        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                // Read and discard the invalid tag to avoid issues, then create a proper error
398                let _ = reader.read_u8()?;
399                // This is a bit of a hack but should work for speedy contexts
400                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// generates a `SyncMessage` to tell another node what versions we're missing
442#[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                // don't set partial if it is effectively complete
477                .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        // Add some heads
663        sync_state.heads.insert(actor1, CrsqlDbVersion(10));
664        sync_state.heads.insert(actor2, CrsqlDbVersion(20));
665
666        // Add some needs
667        sync_state.need.insert(
668            actor1,
669            vec![
670                CrsqlDbVersion(1)..=CrsqlDbVersion(5),
671                CrsqlDbVersion(8)..=CrsqlDbVersion(9),
672            ],
673        );
674
675        // Add some partial needs
676        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        // Serialize
684        let bytes = sync_state.write_to_vec().unwrap();
685
686        // Deserialize
687        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}