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 )
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: *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 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 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 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 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 let _ = reader.read_u8()?;
402 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#[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 .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 sync_state.heads.insert(actor1, CrsqlDbVersion(10));
682 sync_state.heads.insert(actor2, CrsqlDbVersion(20));
683
684 sync_state.need.insert(
686 actor1,
687 vec![
688 CrsqlDbVersion(1)..=CrsqlDbVersion(5),
689 CrsqlDbVersion(8)..=CrsqlDbVersion(9),
690 ],
691 );
692
693 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 let bytes = sync_state.write_to_vec().unwrap();
703
704 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}