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, CrsqlSeq},
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: *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.clone(),
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
207                                        .overlapping(range)
208                                        .map(|overlap| {
209                                            let start = cmp::max(range.start(), overlap.start());
210                                            let end = cmp::min(range.end(), overlap.end());
211                                            *start..=*end
212                                        })
213                                        .collect::<Vec<RangeInclusive<CrsqlSeq>>>()
214                                })
215                                .collect::<Vec<RangeInclusive<CrsqlSeq>>>();
216
217                            if !seqs.is_empty() {
218                                needs
219                                    .entry(*actor_id)
220                                    .or_default()
221                                    .push(SyncNeedV1::Partial { version: *v, seqs });
222                            }
223                        }
224                    }
225                }
226            }
227
228            let missing = match self.heads.get(actor_id) {
229                Some(our_head) => {
230                    if head > our_head {
231                        Some((*our_head + 1)..=*head)
232                    } else {
233                        None
234                    }
235                }
236                None => Some(CrsqlDbVersion(1)..=*head),
237            };
238
239            if let Some(missing) = missing {
240                needs
241                    .entry(*actor_id)
242                    .or_default()
243                    .push(SyncNeedV1::Full { versions: missing });
244            }
245        }
246
247        needs
248    }
249}
250
251impl<'a, C> Readable<'a, C> for SyncStateV1
252where
253    C: speedy::Context,
254{
255    fn read_from<R: speedy::Reader<'a, C>>(reader: &mut R) -> Result<Self, C::Error> {
256        let actor_id = ActorId::read_from(reader)?;
257        let heads = HashMap::<ActorId, CrsqlDbVersion>::read_from(reader)?;
258
259        // Read need: HashMap<ActorId, Vec<RangeInclusive<CrsqlDbVersion>>>
260        let need_len = usize::read_from(reader)?;
261        let mut need = HashMap::with_capacity(need_len);
262        for _ in 0..need_len {
263            let actor_id = ActorId::read_from(reader)?;
264            let ranges_len = usize::read_from(reader)?;
265            let mut ranges = Vec::with_capacity(ranges_len);
266            for _ in 0..ranges_len {
267                let start = CrsqlDbVersion::read_from(reader)?;
268                let end = CrsqlDbVersion::read_from(reader)?;
269                ranges.push(start..=end);
270            }
271            need.insert(actor_id, ranges);
272        }
273
274        // Read partial_need: HashMap<ActorId, HashMap<CrsqlDbVersion, Vec<RangeInclusive<CrsqlSeq>>>>
275        let partial_need_len = usize::read_from(reader)?;
276        let mut partial_need = HashMap::with_capacity(partial_need_len);
277        for _ in 0..partial_need_len {
278            let actor_id = ActorId::read_from(reader)?;
279            let versions_len = usize::read_from(reader)?;
280            let mut versions_map = HashMap::with_capacity(versions_len);
281            for _ in 0..versions_len {
282                let version = CrsqlDbVersion::read_from(reader)?;
283                let seq_ranges_len = usize::read_from(reader)?;
284                let mut seq_ranges = Vec::with_capacity(seq_ranges_len);
285                for _ in 0..seq_ranges_len {
286                    let start = CrsqlSeq::read_from(reader)?;
287                    let end = CrsqlSeq::read_from(reader)?;
288                    seq_ranges.push(start..=end);
289                }
290                versions_map.insert(version, seq_ranges);
291            }
292            partial_need.insert(actor_id, versions_map);
293        }
294
295        let last_cleared_ts = Option::<Timestamp>::read_from(reader)?;
296
297        Ok(SyncStateV1 {
298            actor_id,
299            heads,
300            need,
301            partial_need,
302            last_cleared_ts,
303        })
304    }
305}
306
307impl<C> Writable<C> for SyncStateV1
308where
309    C: speedy::Context,
310{
311    fn write_to<T: ?Sized + speedy::Writer<C>>(&self, writer: &mut T) -> Result<(), C::Error> {
312        self.actor_id.write_to(writer)?;
313        self.heads.write_to(writer)?;
314
315        // Write need: HashMap<ActorId, Vec<RangeInclusive<CrsqlDbVersion>>>
316        self.need.len().write_to(writer)?;
317        for (actor_id, ranges) in &self.need {
318            actor_id.write_to(writer)?;
319            ranges.len().write_to(writer)?;
320            for range in ranges {
321                range.start().write_to(writer)?;
322                range.end().write_to(writer)?;
323            }
324        }
325
326        // Write partial_need: HashMap<ActorId, HashMap<CrsqlDbVersion, Vec<RangeInclusive<CrsqlSeq>>>>
327        self.partial_need.len().write_to(writer)?;
328        for (actor_id, versions_map) in &self.partial_need {
329            actor_id.write_to(writer)?;
330            versions_map.len().write_to(writer)?;
331            for (version, seq_ranges) in versions_map {
332                version.write_to(writer)?;
333                seq_ranges.len().write_to(writer)?;
334                for range in seq_ranges {
335                    range.start().write_to(writer)?;
336                    range.end().write_to(writer)?;
337                }
338            }
339        }
340
341        self.last_cleared_ts.write_to(writer)?;
342
343        Ok(())
344    }
345}
346
347#[derive(Debug, Clone, PartialEq)]
348pub enum SyncNeedV1 {
349    Full {
350        versions: RangeInclusive<CrsqlDbVersion>,
351    },
352    Partial {
353        version: CrsqlDbVersion,
354        seqs: Vec<RangeInclusive<CrsqlSeq>>,
355    },
356    Empty {
357        ts: Option<Timestamp>,
358    },
359}
360
361impl SyncNeedV1 {
362    pub fn count(&self) -> usize {
363        match self {
364            SyncNeedV1::Full { versions } => (versions.end().0 - versions.start().0) as usize + 1,
365            SyncNeedV1::Partial { .. } => 1,
366            SyncNeedV1::Empty { .. } => 1,
367        }
368    }
369}
370
371impl<'a, C> Readable<'a, C> for SyncNeedV1
372where
373    C: speedy::Context,
374{
375    fn read_from<R: speedy::Reader<'a, C>>(reader: &mut R) -> Result<Self, C::Error> {
376        let variant_tag = u8::read_from(reader)?;
377        match variant_tag {
378            0 => {
379                let start = CrsqlDbVersion::read_from(reader)?;
380                let end = CrsqlDbVersion::read_from(reader)?;
381                let versions = start..=end;
382                Ok(SyncNeedV1::Full { versions })
383            }
384            1 => {
385                let version = CrsqlDbVersion::read_from(reader)?;
386                let seqs_len = usize::read_from(reader)?;
387                let mut seqs = Vec::with_capacity(seqs_len);
388                for _ in 0..seqs_len {
389                    let start = CrsqlSeq::read_from(reader)?;
390                    let end = CrsqlSeq::read_from(reader)?;
391                    seqs.push(start..=end);
392                }
393                Ok(SyncNeedV1::Partial { version, seqs })
394            }
395            2 => {
396                let ts = Option::<Timestamp>::read_from(reader)?;
397                Ok(SyncNeedV1::Empty { ts })
398            }
399            _ => {
400                // Read and discard the invalid tag to avoid issues, then create a proper error
401                let _ = reader.read_u8()?;
402                // This is a bit of a hack but should work for speedy contexts
403                panic!("Invalid SyncNeedV1 variant tag: {}", variant_tag);
404            }
405        }
406    }
407}
408
409impl<C> Writable<C> for SyncNeedV1
410where
411    C: speedy::Context,
412{
413    fn write_to<T: ?Sized + speedy::Writer<C>>(&self, writer: &mut T) -> Result<(), C::Error> {
414        match self {
415            SyncNeedV1::Full { versions } => {
416                0u8.write_to(writer)?;
417                versions.start().write_to(writer)?;
418                versions.end().write_to(writer)?;
419            }
420            SyncNeedV1::Partial { version, seqs } => {
421                1u8.write_to(writer)?;
422                version.write_to(writer)?;
423                seqs.len().write_to(writer)?;
424                for range in seqs {
425                    range.start().write_to(writer)?;
426                    range.end().write_to(writer)?;
427                }
428            }
429            SyncNeedV1::Empty { ts } => {
430                2u8.write_to(writer)?;
431                ts.write_to(writer)?;
432            }
433        }
434        Ok(())
435    }
436}
437
438impl From<SyncStateV1> for SyncMessage {
439    fn from(value: SyncStateV1) -> Self {
440        SyncMessage::V1(SyncMessageV1::State(value))
441    }
442}
443
444// generates a `SyncMessage` to tell another node what versions we're missing
445#[tracing::instrument(skip_all, level = "debug")]
446pub async fn generate_sync(bookie: &Bookie, self_actor_id: ActorId) -> SyncStateV1 {
447    let mut state = SyncStateV1 {
448        actor_id: self_actor_id,
449        ..Default::default()
450    };
451
452    let actors: Vec<(ActorId, Booked)> = {
453        bookie
454            .read::<&str, _>("generate_sync", None)
455            .await
456            .iter()
457            .map(|(k, v)| (*k, v.clone()))
458            .collect()
459    };
460
461    for (actor_id, booked) in actors {
462        let bookedr = booked.read("generate_sync", actor_id.as_simple()).await;
463
464        let last_version = match bookedr.last() {
465            None => continue,
466            Some(v) => v,
467        };
468
469        let need: Vec<_> = bookedr.needed().iter().cloned().collect();
470
471        if !need.is_empty() {
472            state.need.insert(actor_id, need);
473        }
474
475        {
476            for (v, partial) in bookedr
477                .partials
478                .iter()
479                // don't set partial if it is effectively complete
480                .filter(|(_, partial)| !partial.is_complete())
481            {
482                state.partial_need.entry(actor_id).or_default().insert(
483                    *v,
484                    partial
485                        .seqs
486                        .gaps(&(CrsqlSeq(0)..=partial.last_seq))
487                        .collect(),
488                );
489            }
490        }
491        state.heads.insert(actor_id, last_version);
492    }
493
494    state
495}
496
497#[derive(Debug, thiserror::Error)]
498pub enum SyncMessageEncodeError {
499    #[error(transparent)]
500    Encode(#[from] speedy::Error),
501    #[error(transparent)]
502    Io(#[from] io::Error),
503}
504
505#[derive(Debug, thiserror::Error)]
506pub enum SyncMessageDecodeError {
507    #[error(transparent)]
508    Decode(#[from] speedy::Error),
509    #[error("corrupted message, crc mismatch (got: {0}, expected {1})")]
510    Corrupted(u32, u32),
511    #[error(transparent)]
512    Io(#[from] io::Error),
513}
514
515impl SyncMessage {
516    pub fn state(&self) -> Option<&SyncStateV1> {
517        match self {
518            SyncMessage::V1(SyncMessageV1::State(state)) => Some(state),
519            _ => None,
520        }
521    }
522
523    pub fn from_slice<S: AsRef<[u8]>>(slice: S) -> Result<Self, speedy::Error> {
524        Self::read_from_buffer(slice.as_ref())
525    }
526
527    pub fn from_buf(buf: &mut BytesMut) -> Result<Self, SyncMessageDecodeError> {
528        Ok(Self::from_slice(buf)?)
529    }
530
531    pub fn decode(
532        codec: &mut LengthDelimitedCodec,
533        buf: &mut BytesMut,
534    ) -> Result<Option<Self>, SyncMessageDecodeError> {
535        Ok(match codec.decode(buf)? {
536            Some(mut buf) => Some(Self::from_buf(&mut buf)?),
537            None => None,
538        })
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use uuid::Uuid;
545
546    use super::*;
547
548    #[test]
549    fn test_compute_available_needs() {
550        let actor1 = ActorId(Uuid::new_v4());
551
552        let mut our_state = SyncStateV1::default();
553        our_state.heads.insert(actor1, CrsqlDbVersion(10));
554
555        let mut other_state = SyncStateV1::default();
556        other_state.heads.insert(actor1, CrsqlDbVersion(13));
557
558        assert_eq!(
559            our_state.compute_available_needs(&other_state),
560            [(
561                actor1,
562                vec![SyncNeedV1::Full {
563                    versions: CrsqlDbVersion(11)..=CrsqlDbVersion(13)
564                }]
565            )]
566            .into()
567        );
568
569        our_state
570            .need
571            .entry(actor1)
572            .or_default()
573            .push(CrsqlDbVersion(2)..=CrsqlDbVersion(5));
574        our_state
575            .need
576            .entry(actor1)
577            .or_default()
578            .push(CrsqlDbVersion(7)..=CrsqlDbVersion(7));
579
580        assert_eq!(
581            our_state.compute_available_needs(&other_state),
582            [(
583                actor1,
584                vec![
585                    SyncNeedV1::Full {
586                        versions: CrsqlDbVersion(2)..=CrsqlDbVersion(5)
587                    },
588                    SyncNeedV1::Full {
589                        versions: CrsqlDbVersion(7)..=CrsqlDbVersion(7)
590                    },
591                    SyncNeedV1::Full {
592                        versions: CrsqlDbVersion(11)..=CrsqlDbVersion(13)
593                    }
594                ]
595            )]
596            .into()
597        );
598
599        our_state.partial_need.insert(
600            actor1,
601            [(
602                CrsqlDbVersion(9),
603                vec![CrsqlSeq(100)..=CrsqlSeq(120), CrsqlSeq(130)..=CrsqlSeq(132)],
604            )]
605            .into(),
606        );
607
608        assert_eq!(
609            our_state.compute_available_needs(&other_state),
610            [(
611                actor1,
612                vec![
613                    SyncNeedV1::Full {
614                        versions: CrsqlDbVersion(2)..=CrsqlDbVersion(5)
615                    },
616                    SyncNeedV1::Full {
617                        versions: CrsqlDbVersion(7)..=CrsqlDbVersion(7)
618                    },
619                    SyncNeedV1::Partial {
620                        version: CrsqlDbVersion(9),
621                        seqs: vec![CrsqlSeq(100)..=CrsqlSeq(120), CrsqlSeq(130)..=CrsqlSeq(132)]
622                    },
623                    SyncNeedV1::Full {
624                        versions: CrsqlDbVersion(11)..=CrsqlDbVersion(13)
625                    }
626                ]
627            )]
628            .into()
629        );
630
631        other_state.partial_need.insert(
632            actor1,
633            [(
634                CrsqlDbVersion(9),
635                vec![CrsqlSeq(100)..=CrsqlSeq(110), CrsqlSeq(130)..=CrsqlSeq(130)],
636            )]
637            .into(),
638        );
639
640        assert_eq!(
641            our_state.compute_available_needs(&other_state),
642            [(
643                actor1,
644                vec![
645                    SyncNeedV1::Full {
646                        versions: CrsqlDbVersion(2)..=CrsqlDbVersion(5)
647                    },
648                    SyncNeedV1::Full {
649                        versions: CrsqlDbVersion(7)..=CrsqlDbVersion(7)
650                    },
651                    SyncNeedV1::Partial {
652                        version: CrsqlDbVersion(9),
653                        seqs: vec![CrsqlSeq(111)..=CrsqlSeq(120), CrsqlSeq(131)..=CrsqlSeq(132)]
654                    },
655                    SyncNeedV1::Full {
656                        versions: CrsqlDbVersion(11)..=CrsqlDbVersion(13)
657                    }
658                ]
659            )]
660            .into()
661        );
662    }
663
664    #[test]
665    fn test_sync_state_v1_serialization() {
666        use speedy::{Readable, Writable};
667        use std::collections::HashMap;
668
669        let actor1 = ActorId(Uuid::new_v4());
670        let actor2 = ActorId(Uuid::new_v4());
671
672        let mut sync_state = SyncStateV1 {
673            actor_id: actor1,
674            heads: HashMap::new(),
675            need: HashMap::new(),
676            partial_need: HashMap::new(),
677            last_cleared_ts: Some(Timestamp::from(12345u64)),
678        };
679
680        // Add some heads
681        sync_state.heads.insert(actor1, CrsqlDbVersion(10));
682        sync_state.heads.insert(actor2, CrsqlDbVersion(20));
683
684        // Add some needs
685        sync_state.need.insert(
686            actor1,
687            vec![
688                CrsqlDbVersion(1)..=CrsqlDbVersion(5),
689                CrsqlDbVersion(8)..=CrsqlDbVersion(9),
690            ],
691        );
692
693        // Add some partial needs
694        let mut partial_map = HashMap::new();
695        partial_map.insert(
696            CrsqlDbVersion(15),
697            vec![CrsqlSeq(100)..=CrsqlSeq(110), CrsqlSeq(120)..=CrsqlSeq(130)],
698        );
699        sync_state.partial_need.insert(actor2, partial_map);
700
701        // Serialize
702        let bytes = sync_state.write_to_vec().unwrap();
703
704        // Deserialize
705        let deserialized = SyncStateV1::read_from_buffer(&bytes).unwrap();
706
707        assert_eq!(sync_state, deserialized);
708    }
709
710    #[test]
711    fn test_sync_state_v1_empty_serialization() {
712        use speedy::{Readable, Writable};
713
714        let sync_state = SyncStateV1 {
715            actor_id: ActorId(Uuid::new_v4()),
716            heads: HashMap::new(),
717            need: HashMap::new(),
718            partial_need: HashMap::new(),
719            last_cleared_ts: None,
720        };
721
722        let bytes = sync_state.write_to_vec().unwrap();
723        let deserialized = SyncStateV1::read_from_buffer(&bytes).unwrap();
724
725        assert_eq!(sync_state, deserialized);
726    }
727
728    #[test]
729    fn test_sync_need_v1_full_serialization() {
730        use speedy::{Readable, Writable};
731
732        let sync_need = SyncNeedV1::Full {
733            versions: CrsqlDbVersion(5)..=CrsqlDbVersion(10),
734        };
735
736        let bytes = sync_need.write_to_vec().unwrap();
737        let deserialized = SyncNeedV1::read_from_buffer(&bytes).unwrap();
738
739        assert_eq!(sync_need, deserialized);
740    }
741
742    #[test]
743    fn test_sync_need_v1_partial_serialization() {
744        use speedy::{Readable, Writable};
745
746        let sync_need = SyncNeedV1::Partial {
747            version: CrsqlDbVersion(42),
748            seqs: vec![
749                CrsqlSeq(0)..=CrsqlSeq(10),
750                CrsqlSeq(20)..=CrsqlSeq(30),
751                CrsqlSeq(50)..=CrsqlSeq(50),
752            ],
753        };
754
755        let bytes = sync_need.write_to_vec().unwrap();
756        let deserialized = SyncNeedV1::read_from_buffer(&bytes).unwrap();
757
758        assert_eq!(sync_need, deserialized);
759    }
760
761    #[test]
762    fn test_sync_need_v1_empty_serialization() {
763        use speedy::{Readable, Writable};
764
765        let sync_need = SyncNeedV1::Empty {
766            ts: Some(Timestamp::from(67890u64)),
767        };
768
769        let bytes = sync_need.write_to_vec().unwrap();
770        let deserialized = SyncNeedV1::read_from_buffer(&bytes).unwrap();
771
772        assert_eq!(sync_need, deserialized);
773    }
774
775    #[test]
776    fn test_sync_need_v1_empty_with_none_ts() {
777        use speedy::{Readable, Writable};
778
779        let sync_need = SyncNeedV1::Empty { ts: None };
780
781        let bytes = sync_need.write_to_vec().unwrap();
782        let deserialized = SyncNeedV1::read_from_buffer(&bytes).unwrap();
783
784        assert_eq!(sync_need, deserialized);
785    }
786
787    #[test]
788    fn test_sync_need_v1_roundtrip_all_variants() {
789        use speedy::{Readable, Writable};
790
791        let test_cases = vec![
792            SyncNeedV1::Full {
793                versions: CrsqlDbVersion(1)..=CrsqlDbVersion(1),
794            },
795            SyncNeedV1::Partial {
796                version: CrsqlDbVersion(10),
797                seqs: vec![],
798            },
799            SyncNeedV1::Partial {
800                version: CrsqlDbVersion(20),
801                seqs: vec![CrsqlSeq(1)..=CrsqlSeq(5)],
802            },
803            SyncNeedV1::Empty { ts: None },
804            SyncNeedV1::Empty {
805                ts: Some(Timestamp::zero()),
806            },
807        ];
808
809        for (i, original) in test_cases.into_iter().enumerate() {
810            let bytes = original.write_to_vec().unwrap();
811            let deserialized = SyncNeedV1::read_from_buffer(&bytes)
812                .unwrap_or_else(|e| panic!("Failed to deserialize test case {}: {}", i, e));
813
814            assert_eq!(original, deserialized, "Test case {} failed", i);
815        }
816    }
817}