1use std::collections::{HashMap, HashSet, VecDeque};
35
36use thiserror::Error;
37
38#[derive(Clone, Debug, PartialEq, Eq, Error)]
42pub enum SyncError {
43 #[error("key '{0}' is tombstoned and cannot be re-inserted")]
45 Tombstoned(String),
46
47 #[error("conflict rejected for key '{key}'")]
49 ConflictRejected { key: String },
50
51 #[error("invalid sync operation")]
53 InvalidOperation,
54}
55
56#[derive(Clone, Debug, Default, PartialEq, Eq)]
64pub struct VectorClock {
65 pub entries: HashMap<String, u64>,
67}
68
69impl VectorClock {
70 #[must_use]
72 pub fn new() -> Self {
73 Self {
74 entries: HashMap::new(),
75 }
76 }
77
78 pub fn increment(&mut self, node_id: &str) {
80 let counter = self.entries.entry(node_id.to_string()).or_insert(0);
81 *counter = counter.saturating_add(1);
82 }
83
84 #[must_use]
89 pub fn merge(&self, other: &VectorClock) -> VectorClock {
90 let mut result = self.entries.clone();
91 for (node, &count) in &other.entries {
92 let entry = result.entry(node.clone()).or_insert(0);
93 if count > *entry {
94 *entry = count;
95 }
96 }
97 VectorClock { entries: result }
98 }
99
100 #[must_use]
103 pub fn happens_before(&self, other: &VectorClock) -> bool {
104 let mut strictly_less = false;
105 for (node, &self_count) in &self.entries {
106 let other_count = other.entries.get(node).copied().unwrap_or(0);
107 if self_count > other_count {
108 return false;
109 }
110 if self_count < other_count {
111 strictly_less = true;
112 }
113 }
114 for (node, &other_count) in &other.entries {
116 if !self.entries.contains_key(node) && other_count > 0 {
117 strictly_less = true;
118 }
119 }
120 strictly_less
121 }
122
123 #[must_use]
125 pub fn concurrent_with(&self, other: &VectorClock) -> bool {
126 !self.happens_before(other) && !other.happens_before(self)
127 }
128
129 #[must_use]
134 pub fn dominates(&self, other: &VectorClock) -> bool {
135 for (node, &other_count) in &other.entries {
136 let self_count = self.entries.get(node).copied().unwrap_or(0);
137 if self_count < other_count {
138 return false;
139 }
140 }
141 true
142 }
143
144 #[must_use]
146 pub fn max_value(&self) -> u64 {
147 self.entries.values().copied().max().unwrap_or(0)
148 }
149
150 #[must_use]
152 pub fn get(&self, node_id: &str) -> u64 {
153 self.entries.get(node_id).copied().unwrap_or(0)
154 }
155}
156
157#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct SyncEntry {
162 pub key: String,
164 pub value: Vec<u8>,
166 pub clock: VectorClock,
168 pub node_id: String,
170 pub timestamp: u64,
172}
173
174#[derive(Clone, Debug)]
178pub enum SyncOperation {
179 Put { entry: SyncEntry },
181 Delete { key: String, clock: VectorClock },
183 Merge { entries: Vec<SyncEntry> },
185}
186
187#[derive(Clone, Debug)]
191pub struct SyncState {
192 pub entries: HashMap<String, SyncEntry>,
194 pub tombstones: HashSet<String>,
196 pub local_clock: VectorClock,
198 pub node_id: String,
200}
201
202impl SyncState {
203 fn new(node_id: String) -> Self {
204 Self {
205 entries: HashMap::new(),
206 tombstones: HashSet::new(),
207 local_clock: VectorClock::new(),
208 node_id,
209 }
210 }
211}
212
213#[derive(Clone, Debug, PartialEq, Eq)]
217pub enum ConflictPolicy {
218 LastWriteWins,
220 HighestClock,
223 MergeBytes { separator: u8 },
225 RejectConflict,
227}
228
229pub struct PeerSyncProtocol {
233 pub state: SyncState,
235 pub conflict_policy: ConflictPolicy,
237 pub pending_ops: VecDeque<SyncOperation>,
239 pub sync_log: VecDeque<(String, SyncOperation, u64)>,
241}
242
243#[derive(Clone, Debug, PartialEq, Eq)]
247pub struct PspSyncStats {
248 pub total_entries: usize,
250 pub tombstone_count: usize,
252 pub pending_ops: usize,
254 pub sync_log_size: usize,
256 pub local_clock_max: u64,
258}
259
260impl PeerSyncProtocol {
263 pub fn new(node_id: String, conflict_policy: ConflictPolicy) -> Self {
269 Self {
270 state: SyncState::new(node_id),
271 conflict_policy,
272 pending_ops: VecDeque::new(),
273 sync_log: VecDeque::new(),
274 }
275 }
276
277 pub fn local_put(&mut self, key: String, value: Vec<u8>, now: u64) -> SyncEntry {
285 self.state
286 .local_clock
287 .increment(&self.state.node_id.clone());
288 let entry = SyncEntry {
289 key: key.clone(),
290 value,
291 clock: self.state.local_clock.clone(),
292 node_id: self.state.node_id.clone(),
293 timestamp: now,
294 };
295 self.state.entries.insert(key, entry.clone());
296 self.pending_ops.push_back(SyncOperation::Put {
297 entry: entry.clone(),
298 });
299 entry
300 }
301
302 pub fn local_delete(&mut self, key: String, _now: u64) {
308 self.state
309 .local_clock
310 .increment(&self.state.node_id.clone());
311 let clock = self.state.local_clock.clone();
312 self.state.tombstones.insert(key.clone());
313 self.state.entries.remove(&key);
314 self.pending_ops.push_back(SyncOperation::Delete {
315 key: key.clone(),
316 clock,
317 });
318 }
319
320 pub fn apply_remote_op(
333 &mut self,
334 peer_id: &str,
335 op: SyncOperation,
336 now: u64,
337 ) -> Result<(), SyncError> {
338 self.sync_log
339 .push_back((peer_id.to_string(), op.clone(), now));
340
341 match op {
342 SyncOperation::Put { ref entry } => {
343 self.apply_put(entry)?;
344 }
345 SyncOperation::Delete { ref key, ref clock } => {
346 self.apply_delete(key, clock);
347 }
348 SyncOperation::Merge { ref entries } => {
349 if entries.is_empty() {
350 return Err(SyncError::InvalidOperation);
351 }
352 for entry in entries {
353 let _ = self.apply_put(entry);
356 }
357 }
358 }
359 Ok(())
360 }
361
362 fn apply_put(&mut self, incoming: &SyncEntry) -> Result<(), SyncError> {
365 let key = &incoming.key;
366
367 if self.state.tombstones.contains(key) {
369 return Err(SyncError::Tombstoned(key.clone()));
370 }
371
372 match self.state.entries.get(key) {
373 None => {
374 self.state.local_clock = self.state.local_clock.merge(&incoming.clock);
376 self.state.entries.insert(key.clone(), incoming.clone());
377 }
378 Some(existing) => {
379 if existing.clock == incoming.clock {
381 return Ok(());
382 }
383
384 if incoming.clock.happens_before(&existing.clock) {
386 return Ok(());
387 }
388
389 if existing.clock.happens_before(&incoming.clock) {
391 self.state.local_clock = self.state.local_clock.merge(&incoming.clock);
392 self.state.entries.insert(key.clone(), incoming.clone());
393 return Ok(());
394 }
395
396 let existing_clone = existing.clone();
398 let resolved = self.resolve_conflict_inner(&existing_clone, incoming)?;
399 self.state.local_clock = self.state.local_clock.merge(&resolved.clock);
400 self.state.entries.insert(key.clone(), resolved);
401 }
402 }
403 Ok(())
404 }
405
406 fn apply_delete(&mut self, key: &str, clock: &VectorClock) {
407 self.state.tombstones.insert(key.to_string());
408 self.state.entries.remove(key);
409 self.state.local_clock = self.state.local_clock.merge(clock);
410 }
411
412 pub fn resolve_conflict(
421 &self,
422 existing: &SyncEntry,
423 incoming: &SyncEntry,
424 ) -> Result<SyncEntry, SyncError> {
425 self.resolve_conflict_inner(existing, incoming)
426 }
427
428 fn resolve_conflict_inner(
429 &self,
430 existing: &SyncEntry,
431 incoming: &SyncEntry,
432 ) -> Result<SyncEntry, SyncError> {
433 match &self.conflict_policy {
434 ConflictPolicy::LastWriteWins => {
435 if existing.timestamp > incoming.timestamp {
437 Ok(existing.clone())
438 } else {
439 Ok(incoming.clone())
440 }
441 }
442 ConflictPolicy::HighestClock => {
443 if existing.clock.dominates(&incoming.clock)
444 && !incoming.clock.dominates(&existing.clock)
445 {
446 Ok(existing.clone())
448 } else {
449 Ok(incoming.clone())
451 }
452 }
453 ConflictPolicy::MergeBytes { separator } => {
454 let sep = *separator;
455 let mut merged_value = existing.value.clone();
456 merged_value.push(sep);
457 merged_value.extend_from_slice(&incoming.value);
458 let merged_clock = existing.clock.merge(&incoming.clock);
459 Ok(SyncEntry {
460 key: existing.key.clone(),
461 value: merged_value,
462 clock: merged_clock,
463 node_id: incoming.node_id.clone(),
464 timestamp: existing.timestamp.max(incoming.timestamp),
465 })
466 }
467 ConflictPolicy::RejectConflict => Err(SyncError::ConflictRejected {
468 key: existing.key.clone(),
469 }),
470 }
471 }
472
473 pub fn generate_delta(&self, since_clock: &VectorClock) -> Vec<SyncOperation> {
482 let missing: Vec<SyncEntry> = self
483 .state
484 .entries
485 .values()
486 .filter(|e| !e.clock.happens_before(since_clock))
487 .cloned()
488 .collect();
489
490 if missing.is_empty() {
491 return Vec::new();
492 }
493 vec![SyncOperation::Merge { entries: missing }]
494 }
495
496 pub fn full_sync(&self) -> SyncOperation {
499 SyncOperation::Merge {
500 entries: self.state.entries.values().cloned().collect(),
501 }
502 }
503
504 #[must_use]
508 pub fn get(&self, key: &str) -> Option<&SyncEntry> {
509 self.state.entries.get(key)
510 }
511
512 #[must_use]
514 pub fn contains(&self, key: &str) -> bool {
515 self.state.entries.contains_key(key)
516 }
517
518 #[must_use]
520 pub fn is_tombstoned(&self, key: &str) -> bool {
521 self.state.tombstones.contains(key)
522 }
523
524 #[must_use]
526 pub fn entry_count(&self) -> usize {
527 self.state.entries.len()
528 }
529
530 #[must_use]
532 pub fn clock_value(&self, node_id: &str) -> u64 {
533 self.state.local_clock.get(node_id)
534 }
535
536 #[must_use]
538 pub fn stats(&self) -> PspSyncStats {
539 PspSyncStats {
540 total_entries: self.state.entries.len(),
541 tombstone_count: self.state.tombstones.len(),
542 pending_ops: self.pending_ops.len(),
543 sync_log_size: self.sync_log.len(),
544 local_clock_max: self.state.local_clock.max_value(),
545 }
546 }
547}
548
549#[cfg(test)]
552mod tests {
553 use crate::peer_sync_protocol::{
554 ConflictPolicy, PeerSyncProtocol, PspSyncStats, SyncEntry, SyncError, SyncOperation,
555 VectorClock,
556 };
557
558 #[test]
561 fn vc_new_is_empty() {
562 let vc = VectorClock::new();
563 assert!(vc.entries.is_empty());
564 assert_eq!(vc.max_value(), 0);
565 }
566
567 #[test]
568 fn vc_increment_creates_entry() {
569 let mut vc = VectorClock::new();
570 vc.increment("a");
571 assert_eq!(vc.get("a"), 1);
572 }
573
574 #[test]
575 fn vc_increment_twice() {
576 let mut vc = VectorClock::new();
577 vc.increment("a");
578 vc.increment("a");
579 assert_eq!(vc.get("a"), 2);
580 }
581
582 #[test]
583 fn vc_increment_different_nodes() {
584 let mut vc = VectorClock::new();
585 vc.increment("a");
586 vc.increment("b");
587 assert_eq!(vc.get("a"), 1);
588 assert_eq!(vc.get("b"), 1);
589 }
590
591 #[test]
592 fn vc_merge_element_wise_max() {
593 let mut a = VectorClock::new();
594 a.increment("x");
595 a.increment("x"); a.increment("y"); let mut b = VectorClock::new();
599 b.increment("x"); b.increment("z"); let merged = a.merge(&b);
603 assert_eq!(merged.get("x"), 2);
604 assert_eq!(merged.get("y"), 1);
605 assert_eq!(merged.get("z"), 1);
606 }
607
608 #[test]
609 fn vc_merge_is_commutative() {
610 let mut a = VectorClock::new();
611 a.increment("a");
612 a.increment("a");
613 let mut b = VectorClock::new();
614 b.increment("a");
615 b.increment("b");
616
617 assert_eq!(a.merge(&b), b.merge(&a));
618 }
619
620 #[test]
621 fn vc_happens_before_strict() {
622 let mut old = VectorClock::new();
623 old.increment("n");
624 let mut new = VectorClock::new();
625 new.increment("n");
626 new.increment("n");
627
628 assert!(old.happens_before(&new));
629 assert!(!new.happens_before(&old));
630 }
631
632 #[test]
633 fn vc_equal_clocks_not_happens_before() {
634 let mut a = VectorClock::new();
635 a.increment("n");
636 let b = a.clone();
637 assert!(!a.happens_before(&b));
638 assert!(!b.happens_before(&a));
639 }
640
641 #[test]
642 fn vc_concurrent_with() {
643 let mut a = VectorClock::new();
644 a.increment("a");
645 let mut b = VectorClock::new();
646 b.increment("b");
647
648 assert!(a.concurrent_with(&b));
649 assert!(b.concurrent_with(&a));
650 }
651
652 #[test]
653 fn vc_not_concurrent_when_ordered() {
654 let mut early = VectorClock::new();
655 early.increment("n");
656 let mut late = early.clone();
657 late.increment("n");
658
659 assert!(!early.concurrent_with(&late));
660 assert!(!late.concurrent_with(&early));
661 }
662
663 #[test]
664 fn vc_dominates_self() {
665 let mut vc = VectorClock::new();
666 vc.increment("n");
667 assert!(vc.dominates(&vc.clone()));
668 }
669
670 #[test]
671 fn vc_dominates_older() {
672 let mut old = VectorClock::new();
673 old.increment("n");
674 let mut new = old.clone();
675 new.increment("n");
676
677 assert!(new.dominates(&old));
678 assert!(!old.dominates(&new));
679 }
680
681 #[test]
682 fn vc_get_missing_node_returns_zero() {
683 let vc = VectorClock::new();
684 assert_eq!(vc.get("nonexistent"), 0);
685 }
686
687 #[test]
688 fn vc_max_value_multiple_nodes() {
689 let mut vc = VectorClock::new();
690 vc.increment("a");
691 vc.increment("b");
692 vc.increment("b");
693 vc.increment("b");
694 assert_eq!(vc.max_value(), 3);
695 }
696
697 #[test]
700 fn local_put_stores_entry() {
701 let mut proto = PeerSyncProtocol::new("node-a".to_string(), ConflictPolicy::LastWriteWins);
702 proto.local_put("k1".to_string(), b"hello".to_vec(), 100);
703 assert!(proto.contains("k1"));
704 assert_eq!(
705 proto.get("k1").expect("test: k1 entry must exist").value,
706 b"hello"
707 );
708 }
709
710 #[test]
711 fn local_put_increments_clock() {
712 let mut proto = PeerSyncProtocol::new("node-a".to_string(), ConflictPolicy::LastWriteWins);
713 proto.local_put("k".to_string(), b"v".to_vec(), 1);
714 assert_eq!(proto.clock_value("node-a"), 1);
715 proto.local_put("k2".to_string(), b"v2".to_vec(), 2);
716 assert_eq!(proto.clock_value("node-a"), 2);
717 }
718
719 #[test]
720 fn local_put_returns_correct_entry() {
721 let mut proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
722 let entry = proto.local_put("key".to_string(), b"val".to_vec(), 42);
723 assert_eq!(entry.key, "key");
724 assert_eq!(entry.value, b"val");
725 assert_eq!(entry.timestamp, 42);
726 assert_eq!(entry.node_id, "n");
727 }
728
729 #[test]
730 fn local_put_enqueues_pending_op() {
731 let mut proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
732 proto.local_put("k".to_string(), b"v".to_vec(), 1);
733 assert_eq!(proto.pending_ops.len(), 1);
734 }
735
736 #[test]
737 fn local_delete_tombstones_key() {
738 let mut proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
739 proto.local_put("k".to_string(), b"v".to_vec(), 1);
740 proto.local_delete("k".to_string(), 2);
741 assert!(!proto.contains("k"));
742 assert!(proto.is_tombstoned("k"));
743 }
744
745 #[test]
746 fn local_delete_increments_clock() {
747 let mut proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
748 proto.local_delete("k".to_string(), 5);
749 assert_eq!(proto.clock_value("n"), 1);
750 }
751
752 #[test]
755 fn apply_remote_put_fresh_key() {
756 let mut node_a = PeerSyncProtocol::new("a".to_string(), ConflictPolicy::LastWriteWins);
757 let entry = node_a.local_put("x".to_string(), b"data".to_vec(), 10);
758
759 let mut node_b = PeerSyncProtocol::new("b".to_string(), ConflictPolicy::LastWriteWins);
760 node_b
761 .apply_remote_op("a", SyncOperation::Put { entry }, 11)
762 .expect("test: apply remote put for fresh key x should succeed");
763 assert_eq!(
764 node_b
765 .get("x")
766 .expect("test: x entry must exist after remote put")
767 .value,
768 b"data"
769 );
770 }
771
772 #[test]
773 fn apply_remote_put_tombstoned_returns_error() {
774 let mut node = PeerSyncProtocol::new("a".to_string(), ConflictPolicy::LastWriteWins);
775 node.local_delete("k".to_string(), 1);
776
777 let mut vc = VectorClock::new();
778 vc.increment("b");
779 let entry = SyncEntry {
780 key: "k".to_string(),
781 value: b"late".to_vec(),
782 clock: vc,
783 node_id: "b".to_string(),
784 timestamp: 5,
785 };
786 let result = node.apply_remote_op("b", SyncOperation::Put { entry }, 6);
787 assert_eq!(result, Err(SyncError::Tombstoned("k".to_string())));
788 }
789
790 #[test]
791 fn apply_remote_put_older_clock_discarded() {
792 let mut node = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
793 node.local_put("k".to_string(), b"new".to_vec(), 100);
795
796 let mut old_vc = VectorClock::new();
798 old_vc.increment("remote");
799 let old_entry = SyncEntry {
800 key: "k".to_string(),
801 value: b"old".to_vec(),
802 clock: old_vc,
803 node_id: "remote".to_string(),
804 timestamp: 1,
805 };
806 node.apply_remote_op("remote", SyncOperation::Put { entry: old_entry }, 2)
807 .expect("test: apply older remote put should succeed without error");
808 assert_eq!(
810 node.get("k")
811 .expect("test: k entry must still exist after discarded old entry")
812 .value,
813 b"new"
814 );
815 }
816
817 #[test]
818 fn apply_remote_delete_removes_entry() {
819 let mut node_a = PeerSyncProtocol::new("a".to_string(), ConflictPolicy::LastWriteWins);
820 node_a.local_put("x".to_string(), b"hi".to_vec(), 1);
821
822 let mut del_clock = VectorClock::new();
823 del_clock.increment("b");
824 node_a
825 .apply_remote_op(
826 "b",
827 SyncOperation::Delete {
828 key: "x".to_string(),
829 clock: del_clock,
830 },
831 2,
832 )
833 .expect("test: apply remote delete for key x should succeed");
834 assert!(!node_a.contains("x"));
835 assert!(node_a.is_tombstoned("x"));
836 }
837
838 #[test]
839 fn apply_remote_merge_applies_all_entries() {
840 let mut node_a = PeerSyncProtocol::new("a".to_string(), ConflictPolicy::LastWriteWins);
841 let e1 = node_a.local_put("k1".to_string(), b"v1".to_vec(), 10);
842 let e2 = node_a.local_put("k2".to_string(), b"v2".to_vec(), 11);
843
844 let mut node_b = PeerSyncProtocol::new("b".to_string(), ConflictPolicy::LastWriteWins);
845 node_b
846 .apply_remote_op(
847 "a",
848 SyncOperation::Merge {
849 entries: vec![e1, e2],
850 },
851 12,
852 )
853 .expect("test: apply remote merge with two entries should succeed");
854 assert_eq!(node_b.entry_count(), 2);
855 assert!(node_b.contains("k1"));
856 assert!(node_b.contains("k2"));
857 }
858
859 #[test]
860 fn apply_remote_merge_empty_returns_invalid() {
861 let mut node = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
862 let result = node.apply_remote_op("peer", SyncOperation::Merge { entries: vec![] }, 1);
863 assert_eq!(result, Err(SyncError::InvalidOperation));
864 }
865
866 #[test]
869 fn conflict_last_write_wins_picks_higher_timestamp() {
870 let mut proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
871 proto.local_put("k".to_string(), b"existing".to_vec(), 50);
873
874 let mut incoming_vc = VectorClock::new();
876 incoming_vc.increment("peer");
877 let incoming = SyncEntry {
878 key: "k".to_string(),
879 value: b"incoming".to_vec(),
880 clock: incoming_vc,
881 node_id: "peer".to_string(),
882 timestamp: 100,
883 };
884 proto
885 .apply_remote_op("peer", SyncOperation::Put { entry: incoming }, 101)
886 .expect("test: apply remote put with higher timestamp should succeed");
887 assert_eq!(
888 proto
889 .get("k")
890 .expect("test: k entry must exist after LastWriteWins conflict")
891 .value,
892 b"incoming"
893 );
894 }
895
896 #[test]
897 fn conflict_last_write_wins_keeps_existing_on_higher_ts() {
898 let mut proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
899 proto.local_put("k".to_string(), b"existing".to_vec(), 200);
900
901 let mut incoming_vc = VectorClock::new();
902 incoming_vc.increment("peer");
903 let incoming = SyncEntry {
904 key: "k".to_string(),
905 value: b"stale".to_vec(),
906 clock: incoming_vc,
907 node_id: "peer".to_string(),
908 timestamp: 100,
909 };
910 proto
911 .apply_remote_op("peer", SyncOperation::Put { entry: incoming }, 201)
912 .expect("test: apply remote put with lower timestamp should succeed");
913 assert_eq!(
914 proto
915 .get("k")
916 .expect("test: k entry must exist; existing higher-ts entry retained")
917 .value,
918 b"existing"
919 );
920 }
921
922 #[test]
923 fn conflict_highest_clock_incoming_dominates() {
924 let mut proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::HighestClock);
925 proto.local_put("k".to_string(), b"old".to_vec(), 1);
926
927 let mut big_vc = proto.state.local_clock.clone();
929 big_vc.increment("peer");
930 big_vc.increment("peer");
931 let incoming = SyncEntry {
932 key: "k".to_string(),
933 value: b"newer".to_vec(),
934 clock: big_vc,
935 node_id: "peer".to_string(),
936 timestamp: 1,
937 };
938 proto
939 .apply_remote_op("peer", SyncOperation::Put { entry: incoming }, 2)
940 .expect("test: apply remote put with dominating clock should succeed");
941 assert_eq!(
942 proto
943 .get("k")
944 .expect("test: k entry must exist after HighestClock conflict")
945 .value,
946 b"newer"
947 );
948 }
949
950 #[test]
951 fn conflict_merge_bytes_concatenates() {
952 let mut proto = PeerSyncProtocol::new(
953 "n".to_string(),
954 ConflictPolicy::MergeBytes { separator: b'|' },
955 );
956 proto.local_put("k".to_string(), b"left".to_vec(), 1);
957
958 let mut peer_vc = VectorClock::new();
959 peer_vc.increment("peer");
960 let incoming = SyncEntry {
961 key: "k".to_string(),
962 value: b"right".to_vec(),
963 clock: peer_vc,
964 node_id: "peer".to_string(),
965 timestamp: 1,
966 };
967 proto
968 .apply_remote_op("peer", SyncOperation::Put { entry: incoming }, 2)
969 .expect("test: apply remote put for MergeBytes conflict should succeed");
970 let merged = &proto
971 .get("k")
972 .expect("test: k entry must exist after MergeBytes conflict")
973 .value;
974 assert!(merged.contains(&b'|'));
975 assert!(merged.windows(4).any(|w| w == b"left"));
976 assert!(merged.windows(5).any(|w| w == b"right"));
977 }
978
979 #[test]
980 fn conflict_reject_returns_error() {
981 let mut proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::RejectConflict);
982 proto.local_put("k".to_string(), b"existing".to_vec(), 1);
983
984 let mut peer_vc = VectorClock::new();
985 peer_vc.increment("peer");
986 let incoming = SyncEntry {
987 key: "k".to_string(),
988 value: b"conflict".to_vec(),
989 clock: peer_vc,
990 node_id: "peer".to_string(),
991 timestamp: 2,
992 };
993 let result = proto.apply_remote_op("peer", SyncOperation::Put { entry: incoming }, 3);
994 assert!(matches!(result, Err(SyncError::ConflictRejected { .. })));
995 assert_eq!(
997 proto
998 .get("k")
999 .expect("test: k entry must exist; RejectConflict keeps existing")
1000 .value,
1001 b"existing"
1002 );
1003 }
1004
1005 #[test]
1008 fn generate_delta_returns_missing_entries() {
1009 let mut node_a = PeerSyncProtocol::new("a".to_string(), ConflictPolicy::LastWriteWins);
1010 node_a.local_put("k1".to_string(), b"v1".to_vec(), 1);
1011 node_a.local_put("k2".to_string(), b"v2".to_vec(), 2);
1012
1013 let peer_clock = VectorClock::new();
1015 let delta = node_a.generate_delta(&peer_clock);
1016 assert_eq!(delta.len(), 1);
1017 if let SyncOperation::Merge { entries } = &delta[0] {
1018 assert_eq!(entries.len(), 2);
1019 } else {
1020 panic!("expected Merge");
1021 }
1022 }
1023
1024 #[test]
1025 fn generate_delta_empty_when_peer_up_to_date() {
1026 let mut node_a = PeerSyncProtocol::new("a".to_string(), ConflictPolicy::LastWriteWins);
1027 node_a.local_put("k".to_string(), b"v".to_vec(), 1);
1028
1029 let current_clock = node_a.state.local_clock.clone();
1031 let mut ahead_clock = current_clock.clone();
1033 ahead_clock.increment("a");
1034
1035 let delta = node_a.generate_delta(&ahead_clock);
1036 assert!(delta.is_empty());
1037 }
1038
1039 #[test]
1040 fn full_sync_returns_all_live_entries() {
1041 let mut node = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
1042 node.local_put("a".to_string(), b"1".to_vec(), 1);
1043 node.local_put("b".to_string(), b"2".to_vec(), 2);
1044 node.local_delete("b".to_string(), 3);
1045
1046 if let SyncOperation::Merge { entries } = node.full_sync() {
1047 assert_eq!(entries.len(), 1);
1049 assert_eq!(entries[0].key, "a");
1050 } else {
1051 panic!("expected Merge");
1052 }
1053 }
1054
1055 #[test]
1058 fn stats_initial_state() {
1059 let proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
1060 let s = proto.stats();
1061 assert_eq!(
1062 s,
1063 PspSyncStats {
1064 total_entries: 0,
1065 tombstone_count: 0,
1066 pending_ops: 0,
1067 sync_log_size: 0,
1068 local_clock_max: 0,
1069 }
1070 );
1071 }
1072
1073 #[test]
1074 fn stats_after_operations() {
1075 let mut proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
1076 proto.local_put("k1".to_string(), b"v1".to_vec(), 1);
1077 proto.local_put("k2".to_string(), b"v2".to_vec(), 2);
1078 proto.local_delete("k1".to_string(), 3);
1079 let s = proto.stats();
1080 assert_eq!(s.total_entries, 1);
1081 assert_eq!(s.tombstone_count, 1);
1082 assert_eq!(s.pending_ops, 3); assert_eq!(s.local_clock_max, 3);
1084 }
1085
1086 #[test]
1087 fn stats_sync_log_grows_on_remote_ops() {
1088 let mut node_a = PeerSyncProtocol::new("a".to_string(), ConflictPolicy::LastWriteWins);
1089 let entry = node_a.local_put("k".to_string(), b"v".to_vec(), 1);
1090
1091 let mut node_b = PeerSyncProtocol::new("b".to_string(), ConflictPolicy::LastWriteWins);
1092 node_b
1093 .apply_remote_op("a", SyncOperation::Put { entry }, 2)
1094 .expect("test: apply remote put for sync log growth test should succeed");
1095 assert_eq!(node_b.stats().sync_log_size, 1);
1096 }
1097
1098 #[test]
1101 fn convergence_two_nodes_put_same_key() {
1102 let mut a = PeerSyncProtocol::new("a".to_string(), ConflictPolicy::LastWriteWins);
1104 let mut b = PeerSyncProtocol::new("b".to_string(), ConflictPolicy::LastWriteWins);
1105
1106 let ea = a.local_put("k".to_string(), b"from-a".to_vec(), 10);
1107 let eb = b.local_put("k".to_string(), b"from-b".to_vec(), 20);
1108
1109 let _ = a.apply_remote_op("b", SyncOperation::Put { entry: eb }, 21);
1111 let _ = b.apply_remote_op("a", SyncOperation::Put { entry: ea }, 21);
1112
1113 assert_eq!(
1115 a.get("k")
1116 .expect("test: node a must have k after convergence exchange")
1117 .value,
1118 b.get("k")
1119 .expect("test: node b must have k after convergence exchange")
1120 .value
1121 );
1122 }
1123
1124 #[test]
1125 fn idempotent_apply_put_same_entry_twice() {
1126 let mut a = PeerSyncProtocol::new("a".to_string(), ConflictPolicy::LastWriteWins);
1127 let entry = a.local_put("k".to_string(), b"v".to_vec(), 1);
1128
1129 let mut b = PeerSyncProtocol::new("b".to_string(), ConflictPolicy::LastWriteWins);
1130 b.apply_remote_op(
1131 "a",
1132 SyncOperation::Put {
1133 entry: entry.clone(),
1134 },
1135 2,
1136 )
1137 .expect("test: first idempotent apply of entry should succeed");
1138 b.apply_remote_op("a", SyncOperation::Put { entry }, 3)
1139 .expect("test: second idempotent apply of same entry should succeed");
1140 assert_eq!(b.entry_count(), 1);
1142 }
1143
1144 #[test]
1145 fn tombstone_blocks_late_arriving_put_in_merge() {
1146 let mut node = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
1147 node.local_delete("k".to_string(), 1);
1148
1149 let mut vc = VectorClock::new();
1151 vc.increment("peer");
1152 let late = SyncEntry {
1153 key: "k".to_string(),
1154 value: b"late".to_vec(),
1155 clock: vc,
1156 node_id: "peer".to_string(),
1157 timestamp: 5,
1158 };
1159 let result = node.apply_remote_op(
1161 "peer",
1162 SyncOperation::Merge {
1163 entries: vec![late],
1164 },
1165 6,
1166 );
1167 assert!(result.is_ok());
1168 assert!(!node.contains("k"));
1169 assert!(node.is_tombstoned("k"));
1170 }
1171}