Skip to main content

ipfrs_network/
peer_sync_protocol.rs

1//! Peer Sync Protocol — bidirectional state synchronisation using vector clocks and CRDTs.
2//!
3//! [`PeerSyncProtocol`] implements a production-grade, conflict-free replicated data type
4//! (CRDT) engine.  Each node maintains a [`VectorClock`] that it increments on every local
5//! write.  Remote operations are applied through [`PeerSyncProtocol::apply_remote_op`], which
6//! resolves write–write conflicts according to a pluggable [`ConflictPolicy`].
7//!
8//! ## Design goals
9//! - **Convergence**: any two nodes that have received the same set of operations end up in
10//!   identical state regardless of delivery order.
11//! - **No data loss by default**: `LastWriteWins` and `HighestClock` policies always keep
12//!   one version; `MergeBytes` retains both.
13//! - **Tombstoning**: deleted keys are permanently suppressed so late-arriving puts for the
14//!   same key are silently discarded.
15//! - **Delta sync**: [`PeerSyncProtocol::generate_delta`] returns only the entries that a
16//!   remote peer is missing, batched into a single [`SyncOperation::Merge`].
17//!
18//! ## Example
19//! ```rust
20//! use ipfrs_network::peer_sync_protocol::{
21//!     PeerSyncProtocol, ConflictPolicy, SyncOperation, VectorClock,
22//! };
23//!
24//! let mut node_a = PeerSyncProtocol::new("node-a".to_string(), ConflictPolicy::LastWriteWins);
25//! let entry = node_a.local_put("greeting".to_string(), b"hello".to_vec(), 1000);
26//!
27//! let mut node_b = PeerSyncProtocol::new("node-b".to_string(), ConflictPolicy::LastWriteWins);
28//! let op = SyncOperation::Put { entry };
29//! node_b.apply_remote_op("node-a", op, 1001).unwrap();
30//!
31//! assert_eq!(node_b.get("greeting").unwrap().value, b"hello");
32//! ```
33
34use std::collections::{HashMap, HashSet, VecDeque};
35
36use thiserror::Error;
37
38// ─── Error type ─────────────────────────────────────────────────────────────
39
40/// Errors returned by [`PeerSyncProtocol`] operations.
41#[derive(Clone, Debug, PartialEq, Eq, Error)]
42pub enum SyncError {
43    /// The key has already been deleted and its tombstone is permanent.
44    #[error("key '{0}' is tombstoned and cannot be re-inserted")]
45    Tombstoned(String),
46
47    /// A conflict was detected but the active policy is [`ConflictPolicy::RejectConflict`].
48    #[error("conflict rejected for key '{key}'")]
49    ConflictRejected { key: String },
50
51    /// The operation is structurally invalid (e.g. an empty Merge).
52    #[error("invalid sync operation")]
53    InvalidOperation,
54}
55
56// ─── VectorClock ─────────────────────────────────────────────────────────────
57
58/// A logical clock that tracks causality per node.
59///
60/// Each node maintains a counter that is incremented whenever the node produces
61/// a new event.  The happened-before relation (→) is defined by Lamport/Mattern
62/// vector clock rules.
63#[derive(Clone, Debug, Default, PartialEq, Eq)]
64pub struct VectorClock {
65    /// Map from `node_id` to that node's logical counter.
66    pub entries: HashMap<String, u64>,
67}
68
69impl VectorClock {
70    /// Create a new, zeroed vector clock.
71    #[must_use]
72    pub fn new() -> Self {
73        Self {
74            entries: HashMap::new(),
75        }
76    }
77
78    /// Increment the counter for `node_id` by one.
79    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    /// Element-wise maximum of `self` and `other`.
85    ///
86    /// The result is the smallest vector clock that *dominates* both inputs,
87    /// i.e. `self.merge(other).dominates(self) && self.merge(other).dominates(other)`.
88    #[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    /// `self → other`: every component of `self` is ≤ the corresponding component
101    /// of `other`, and at least one is strictly less.
102    #[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        // Also check nodes that appear only in `other`.
115        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    /// Two clocks are concurrent when neither happens-before the other.
124    #[must_use]
125    pub fn concurrent_with(&self, other: &VectorClock) -> bool {
126        !self.happens_before(other) && !other.happens_before(self)
127    }
128
129    /// `self` dominates `other`: every component of `self` is ≥ the corresponding
130    /// component of `other` (i.e. `other → self` or `other == self`).
131    ///
132    /// Note: this is not strict; equal clocks both dominate each other.
133    #[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    /// The maximum counter value across all nodes, or `0` if the clock is empty.
145    #[must_use]
146    pub fn max_value(&self) -> u64 {
147        self.entries.values().copied().max().unwrap_or(0)
148    }
149
150    /// Return the counter for `node_id`, or `0` if not present.
151    #[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// ─── SyncEntry ───────────────────────────────────────────────────────────────
158
159/// A key-value record annotated with causal metadata.
160#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct SyncEntry {
162    /// The logical key for this entry.
163    pub key: String,
164    /// Arbitrary payload bytes.
165    pub value: Vec<u8>,
166    /// The vector clock at the time of the last write.
167    pub clock: VectorClock,
168    /// The node that produced this entry.
169    pub node_id: String,
170    /// Wall-clock timestamp (milliseconds since Unix epoch, supplied by the caller).
171    pub timestamp: u64,
172}
173
174// ─── SyncOperation ───────────────────────────────────────────────────────────
175
176/// An atomic operation that can be applied to a [`PeerSyncProtocol`].
177#[derive(Clone, Debug)]
178pub enum SyncOperation {
179    /// Insert or update a single entry.
180    Put { entry: SyncEntry },
181    /// Delete a key, recording the causal context at which deletion occurred.
182    Delete { key: String, clock: VectorClock },
183    /// Apply a batch of entries (used for delta and full sync).
184    Merge { entries: Vec<SyncEntry> },
185}
186
187// ─── SyncState ───────────────────────────────────────────────────────────────
188
189/// The replicated state held by a single node.
190#[derive(Clone, Debug)]
191pub struct SyncState {
192    /// Live entries indexed by key.
193    pub entries: HashMap<String, SyncEntry>,
194    /// Keys that have been deleted; these keys can never be re-inserted.
195    pub tombstones: HashSet<String>,
196    /// This node's current vector clock (incremented on every local write/delete).
197    pub local_clock: VectorClock,
198    /// Stable identifier for this node.
199    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// ─── ConflictPolicy ──────────────────────────────────────────────────────────
214
215/// Strategy used to resolve write–write conflicts (concurrent updates to the same key).
216#[derive(Clone, Debug, PartialEq, Eq)]
217pub enum ConflictPolicy {
218    /// Keep the entry with the higher `timestamp`; on tie, prefer the incoming entry.
219    LastWriteWins,
220    /// Keep the entry whose clock *dominates* the other.  When both clocks are
221    /// concurrent, prefer the incoming entry.
222    HighestClock,
223    /// Concatenate both values separated by `separator`.
224    MergeBytes { separator: u8 },
225    /// Reject the incoming entry and keep the existing one unchanged.
226    RejectConflict,
227}
228
229// ─── PeerSyncProtocol ────────────────────────────────────────────────────────
230
231/// Bidirectional state-synchronisation protocol backed by vector clocks and CRDTs.
232pub struct PeerSyncProtocol {
233    /// The local replicated state.
234    pub state: SyncState,
235    /// How concurrent writes to the same key are resolved.
236    pub conflict_policy: ConflictPolicy,
237    /// Operations that have been enqueued but not yet dispatched to remote peers.
238    pub pending_ops: VecDeque<SyncOperation>,
239    /// Audit log: `(peer_id, operation, wall_timestamp)`.
240    pub sync_log: VecDeque<(String, SyncOperation, u64)>,
241}
242
243// ─── SyncStats ───────────────────────────────────────────────────────────────
244
245/// A snapshot of key metrics for a [`PeerSyncProtocol`] instance.
246#[derive(Clone, Debug, PartialEq, Eq)]
247pub struct PspSyncStats {
248    /// Number of live (non-tombstoned) entries.
249    pub total_entries: usize,
250    /// Number of tombstoned (deleted) keys.
251    pub tombstone_count: usize,
252    /// Number of pending outbound operations.
253    pub pending_ops: usize,
254    /// Number of entries in the sync audit log.
255    pub sync_log_size: usize,
256    /// The highest counter value in the local vector clock.
257    pub local_clock_max: u64,
258}
259
260// ─── Implementation ──────────────────────────────────────────────────────────
261
262impl PeerSyncProtocol {
263    /// Create a new protocol instance for the given node.
264    ///
265    /// # Arguments
266    /// * `node_id` — a stable, globally-unique identifier for this node.
267    /// * `conflict_policy` — the policy applied when concurrent writes collide.
268    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    // ── Local mutations ───────────────────────────────────────────────────
278
279    /// Write `value` at `key` and return the resulting [`SyncEntry`].
280    ///
281    /// The local vector clock is incremented before the entry is stored so that
282    /// the returned entry strictly succeeds every previously stored entry from
283    /// this node.
284    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    /// Mark `key` as deleted.
303    ///
304    /// The key is added to the tombstone set and any live entry for it is removed.
305    /// The local clock is incremented so that the deletion causally succeeds any
306    /// previous entry.
307    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    // ── Remote operations ─────────────────────────────────────────────────
321
322    /// Apply an operation received from a remote peer.
323    ///
324    /// The sync audit log is updated regardless of whether the operation caused
325    /// a state change.
326    ///
327    /// # Errors
328    /// - [`SyncError::Tombstoned`] — a `Put` arrived for a tombstoned key.
329    /// - [`SyncError::ConflictRejected`] — [`ConflictPolicy::RejectConflict`] rejected a
330    ///   conflicting `Put`.
331    /// - [`SyncError::InvalidOperation`] — a `Merge` with zero entries was supplied.
332    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                    // Individual tombstone / conflict errors are silently skipped
354                    // during a bulk merge to preserve convergence.
355                    let _ = self.apply_put(entry);
356                }
357            }
358        }
359        Ok(())
360    }
361
362    // ── Internal apply helpers ────────────────────────────────────────────
363
364    fn apply_put(&mut self, incoming: &SyncEntry) -> Result<(), SyncError> {
365        let key = &incoming.key;
366
367        // Tombstoned keys are permanently suppressed.
368        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                // Fresh key — insert directly and advance local clock.
375                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                // Identical clock → idempotent no-op.
380                if existing.clock == incoming.clock {
381                    return Ok(());
382                }
383
384                // Incoming is strictly older than existing → discard.
385                if incoming.clock.happens_before(&existing.clock) {
386                    return Ok(());
387                }
388
389                // Existing is strictly older → replace with incoming.
390                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                // Concurrent writes — apply conflict policy.
397                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    // ── Conflict resolution ───────────────────────────────────────────────
413
414    /// Resolve a write–write conflict between `existing` and `incoming` according
415    /// to the active [`ConflictPolicy`].
416    ///
417    /// # Errors
418    /// Returns [`SyncError::ConflictRejected`] when the policy is
419    /// [`ConflictPolicy::RejectConflict`].
420    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                // Higher timestamp wins; on tie prefer incoming.
436                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                    // existing strictly dominates
447                    Ok(existing.clone())
448                } else {
449                    // incoming dominates or they are concurrent → prefer incoming
450                    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    // ── Delta / full sync ─────────────────────────────────────────────────
474
475    /// Generate the set of local entries that a peer is missing.
476    ///
477    /// An entry is considered missing from the peer when its clock does **not**
478    /// happen-before `since_clock` (i.e. the peer has not yet observed it).
479    /// All matching entries are returned as a single [`SyncOperation::Merge`].
480    /// An empty `Vec` is returned when nothing needs to be sent.
481    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    /// Return all live (non-tombstoned) entries as a single
497    /// [`SyncOperation::Merge`] suitable for a full-state transfer.
498    pub fn full_sync(&self) -> SyncOperation {
499        SyncOperation::Merge {
500            entries: self.state.entries.values().cloned().collect(),
501        }
502    }
503
504    // ── Read accessors ────────────────────────────────────────────────────
505
506    /// Look up a live entry by key.
507    #[must_use]
508    pub fn get(&self, key: &str) -> Option<&SyncEntry> {
509        self.state.entries.get(key)
510    }
511
512    /// Returns `true` if `key` exists as a live entry.
513    #[must_use]
514    pub fn contains(&self, key: &str) -> bool {
515        self.state.entries.contains_key(key)
516    }
517
518    /// Returns `true` if `key` has been deleted (tombstoned).
519    #[must_use]
520    pub fn is_tombstoned(&self, key: &str) -> bool {
521        self.state.tombstones.contains(key)
522    }
523
524    /// Number of live (non-tombstoned) entries.
525    #[must_use]
526    pub fn entry_count(&self) -> usize {
527        self.state.entries.len()
528    }
529
530    /// The current vector-clock counter for `node_id`, or `0` if unknown.
531    #[must_use]
532    pub fn clock_value(&self, node_id: &str) -> u64 {
533        self.state.local_clock.get(node_id)
534    }
535
536    /// Return a statistics snapshot for this instance.
537    #[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// ─── Tests ───────────────────────────────────────────────────────────────────
550
551#[cfg(test)]
552mod tests {
553    use crate::peer_sync_protocol::{
554        ConflictPolicy, PeerSyncProtocol, PspSyncStats, SyncEntry, SyncError, SyncOperation,
555        VectorClock,
556    };
557
558    // ── VectorClock ──────────────────────────────────────────────────────
559
560    #[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"); // x=2
596        a.increment("y"); // y=1
597
598        let mut b = VectorClock::new();
599        b.increment("x"); // x=1
600        b.increment("z"); // z=1
601
602        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    // ── PeerSyncProtocol — local operations ──────────────────────────────
698
699    #[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    // ── apply_remote_op — Put ────────────────────────────────────────────
753
754    #[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        // Insert a newer entry locally.
794        node.local_put("k".to_string(), b"new".to_vec(), 100);
795
796        // Simulate an older remote entry.
797        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        // Value must not be overwritten by an older entry.
809        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    // ── Conflict resolution policies ─────────────────────────────────────
867
868    #[test]
869    fn conflict_last_write_wins_picks_higher_timestamp() {
870        let mut proto = PeerSyncProtocol::new("n".to_string(), ConflictPolicy::LastWriteWins);
871        // Insert existing entry.
872        proto.local_put("k".to_string(), b"existing".to_vec(), 50);
873
874        // Incoming has higher timestamp → should win.
875        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        // Build an incoming clock that dominates local clock.
928        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        // Existing value is preserved.
996        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    // ── Delta / full sync ────────────────────────────────────────────────
1006
1007    #[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        // Empty peer clock — peer has seen nothing.
1014        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        // Peer clock that dominates all entries.
1030        let current_clock = node_a.state.local_clock.clone();
1031        // A clock that is strictly greater than the entry's clock.
1032        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            // Only "a" is live; "b" was tombstoned.
1048            assert_eq!(entries.len(), 1);
1049            assert_eq!(entries[0].key, "a");
1050        } else {
1051            panic!("expected Merge");
1052        }
1053    }
1054
1055    // ── Stats ────────────────────────────────────────────────────────────
1056
1057    #[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); // put, put, delete
1083        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    // ── Convergence / CRDT invariants ────────────────────────────────────
1099
1100    #[test]
1101    fn convergence_two_nodes_put_same_key() {
1102        // Both nodes write concurrently; after exchanging ops they should converge.
1103        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        // Exchange
1110        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        // Both should agree on the same value (higher timestamp wins).
1114        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        // Entry count must be 1, not 2.
1141        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        // A merge containing a put for the tombstoned key should be silently skipped.
1150        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        // Merge silently skips tombstoned entries (does not return error for batch).
1160        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}