Skip to main content

saorsa_gossip_crdt_sync/
lib.rs

1#![warn(missing_docs)]
2
3//! Production-Ready Delta-CRDT synchronization with anti-entropy
4//!
5//! Implements:
6//! - Delta-CRDTs for bandwidth efficiency
7//! - OR-Set with proper concurrent add/remove semantics
8//! - LWW-Register with vector clocks
9//! - Anti-entropy reconciliation
10//! - IBLT reconciliation for large sets (future)
11
12use anyhow::Result;
13use saorsa_gossip_types::{LogPeerId, PeerId};
14use serde::{de::DeserializeOwned, Deserialize, Serialize};
15use std::collections::{BTreeMap, HashMap, HashSet};
16use std::hash::Hash;
17
18/// CRDT types
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub enum CrdtType {
21    /// Observed-Remove Set
22    OrSet,
23    /// Last-Writer-Wins Register
24    LwwRegister,
25    /// Replicated Growable Array
26    Rga,
27}
28
29/// Delta-CRDT trait for bandwidth-efficient synchronization
30pub trait DeltaCrdt: Send + Sync {
31    /// Type of the delta
32    type Delta: Clone + Serialize + for<'de> Deserialize<'de> + Send;
33
34    /// Merge a delta into this CRDT
35    fn merge(&mut self, delta: &Self::Delta) -> Result<()>;
36
37    /// Generate a delta for changes since a given version.
38    ///
39    /// Returns `None` if there are no changes or retained history cannot
40    /// produce a complete delta for `since_version`.
41    fn delta(&self, since_version: u64) -> Option<Self::Delta>;
42
43    /// Get current version
44    fn version(&self) -> u64;
45}
46
47/// Unique tag for OR-Set elements: (PeerId, sequence_number)
48pub type UniqueTag = (PeerId, u64);
49
50/// Production-ready OR-Set (Observed-Remove Set)
51///
52/// Properly handles concurrent add/remove operations from multiple replicas.
53/// Uses (element, Set\<UniqueTag\>) to track all concurrent adds.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct OrSet<T: Hash + Eq + Clone> {
56    /// Elements with their unique tags (peer_id, seq) per concurrent add
57    elements: HashMap<T, HashSet<UniqueTag>>,
58    /// Tombstones: tags that have been removed
59    tombstones: HashMap<T, HashSet<UniqueTag>>,
60    /// Local changelog version for delta generation
61    version: u64,
62    /// Oldest source version that can still produce a complete delta
63    #[serde(default)]
64    changelog_floor: u64,
65    /// Local version -> (additions, removals) for delta tracking
66    changelog: HashMap<u64, ChangelogEntry<T>>,
67}
68
69/// Deterministic, bincode-layout-compatible retained state for an [`OrSet`].
70///
71/// It preserves every live tag and tombstone while normalizing local delta
72/// generation bookkeeping. Serializing this value can be deserialized
73/// directly as `OrSet<T>` by existing receivers.
74#[derive(Debug, Clone, Serialize)]
75pub struct CanonicalRetainedOrSet<T: Ord> {
76    elements: BTreeMap<T, Vec<UniqueTag>>,
77    tombstones: BTreeMap<T, Vec<UniqueTag>>,
78    version: u64,
79    changelog_floor: u64,
80    changelog: CanonicalChangelog<T>,
81}
82
83type CanonicalChangelog<T> =
84    BTreeMap<u64, (BTreeMap<T, Vec<UniqueTag>>, BTreeMap<T, Vec<UniqueTag>>)>;
85
86/// Type alias for changelog entries to reduce complexity
87type ChangelogEntry<T> = (
88    HashMap<T, HashSet<UniqueTag>>,
89    HashMap<T, HashSet<UniqueTag>>,
90);
91
92impl<T: Hash + Eq + Clone> OrSet<T> {
93    /// Return all merge-relevant state in deterministic wire order.
94    ///
95    /// The result intentionally clears version/changelog fields: those are
96    /// replica-local delta bookkeeping and [`merge_state`](Self::merge_state)
97    /// consumes only live tags and tombstones.
98    pub fn canonical_retained_wire(&self) -> CanonicalRetainedOrSet<T>
99    where
100        T: Ord,
101    {
102        let canonical_tags = |tags: &HashSet<UniqueTag>| {
103            let mut tags: Vec<_> = tags.iter().copied().collect();
104            tags.sort_unstable_by(|(left_peer, left_sequence), (right_peer, right_sequence)| {
105                left_peer
106                    .as_bytes()
107                    .cmp(right_peer.as_bytes())
108                    .then_with(|| left_sequence.cmp(right_sequence))
109            });
110            tags
111        };
112        CanonicalRetainedOrSet {
113            elements: self
114                .elements
115                .iter()
116                .map(|(element, tags)| (element.clone(), canonical_tags(tags)))
117                .collect(),
118            tombstones: self
119                .tombstones
120                .iter()
121                .map(|(element, tags)| (element.clone(), canonical_tags(tags)))
122                .collect(),
123            version: 0,
124            changelog_floor: 0,
125            changelog: BTreeMap::new(),
126        }
127    }
128    /// Create a new OR-Set
129    pub fn new() -> Self {
130        Self {
131            elements: HashMap::new(),
132            tombstones: HashMap::new(),
133            version: 0,
134            changelog_floor: 0,
135            changelog: HashMap::new(),
136        }
137    }
138
139    /// Add an element with a unique tag
140    ///
141    /// The tag should be (peer_id, sequence_number) to ensure uniqueness
142    /// across all replicas.
143    pub fn add(&mut self, element: T, tag: UniqueTag) -> Result<()> {
144        // Add to elements
145        self.elements
146            .entry(element.clone())
147            .or_default()
148            .insert(tag);
149
150        // Remove from tombstones if it was previously removed
151        if let Some(ts) = self.tombstones.get_mut(&element) {
152            ts.remove(&tag);
153            if ts.is_empty() {
154                self.tombstones.remove(&element);
155            }
156        }
157
158        // Record in changelog
159        self.version += 1;
160        let changelog_entry = self
161            .changelog
162            .entry(self.version)
163            .or_insert((HashMap::new(), HashMap::new()));
164        changelog_entry.0.entry(element).or_default().insert(tag);
165
166        Ok(())
167    }
168
169    /// Remove an element
170    ///
171    /// Removes all tags associated with this element, preventing
172    /// concurrent adds from being visible.
173    pub fn remove(&mut self, element: &T) -> Result<()> {
174        if let Some(tags) = self.elements.get(element).cloned() {
175            // Move all tags to tombstones
176            for tag in &tags {
177                self.tombstones
178                    .entry(element.clone())
179                    .or_default()
180                    .insert(*tag);
181            }
182
183            // Remove from elements
184            self.elements.remove(element);
185
186            // Record in changelog
187            self.version += 1;
188            let changelog_entry = self
189                .changelog
190                .entry(self.version)
191                .or_insert((HashMap::new(), HashMap::new()));
192            changelog_entry
193                .1
194                .entry(element.clone())
195                .or_default()
196                .extend(tags);
197        }
198
199        Ok(())
200    }
201
202    /// Check if element exists (not removed)
203    pub fn contains(&self, element: &T) -> bool {
204        self.elements.contains_key(element)
205    }
206
207    /// Get all elements (not removed)
208    pub fn elements(&self) -> Vec<&T> {
209        self.elements.keys().collect()
210    }
211
212    /// Get element count
213    pub fn len(&self) -> usize {
214        self.elements.len()
215    }
216
217    /// Check if empty
218    pub fn is_empty(&self) -> bool {
219        self.elements.is_empty()
220    }
221
222    fn record_changelog_entry(
223        &mut self,
224        added: HashMap<T, HashSet<UniqueTag>>,
225        removed: HashMap<T, HashSet<UniqueTag>>,
226    ) {
227        if added.is_empty() && removed.is_empty() {
228            return;
229        }
230
231        self.version += 1;
232        self.changelog.insert(self.version, (added, removed));
233    }
234
235    fn apply_delta_parts(
236        &mut self,
237        incoming_added: &HashMap<T, HashSet<UniqueTag>>,
238        incoming_removed: &HashMap<T, HashSet<UniqueTag>>,
239    ) {
240        let mut added: HashMap<T, HashSet<UniqueTag>> = HashMap::new();
241        let mut removed: HashMap<T, HashSet<UniqueTag>> = HashMap::new();
242
243        // Merge elements
244        for (elem, tags) in incoming_added {
245            let tombstoned_tags = self.tombstones.get(elem);
246            let mut live_tags = tags
247                .iter()
248                .filter(|tag| !tombstoned_tags.is_some_and(|tombstones| tombstones.contains(*tag)))
249                .copied()
250                .peekable();
251
252            if live_tags.peek().is_none() {
253                continue;
254            }
255
256            let our_tags = self.elements.entry(elem.clone()).or_default();
257            for tag in live_tags {
258                if our_tags.insert(tag) {
259                    added.entry(elem.clone()).or_default().insert(tag);
260                }
261            }
262        }
263
264        // Merge tombstones
265        for (elem, tags) in incoming_removed {
266            let mut new_tombstones = HashSet::new();
267            {
268                let our_tombstones = self.tombstones.entry(elem.clone()).or_default();
269                for tag in tags {
270                    if our_tombstones.insert(*tag) {
271                        new_tombstones.insert(*tag);
272                    }
273                }
274            }
275
276            if !new_tombstones.is_empty() {
277                removed.insert(elem.clone(), new_tombstones);
278            }
279
280            // Remove tombstoned tags from our elements
281            let remove_element = if let Some(our_tags) = self.elements.get_mut(elem) {
282                if let Some(our_tombstones) = self.tombstones.get(elem) {
283                    our_tags.retain(|tag| !our_tombstones.contains(tag));
284                }
285                our_tags.is_empty()
286            } else {
287                false
288            };
289
290            if remove_element {
291                self.elements.remove(elem);
292            }
293        }
294
295        self.record_changelog_entry(added, removed);
296    }
297
298    /// Merge another OR-Set into this one
299    ///
300    /// This implements the merge operation for state-based CRDTs.
301    /// Preserves all concurrent adds and removes.
302    pub fn merge_state(&mut self, other: &OrSet<T>) -> Result<()> {
303        self.apply_delta_parts(&other.elements, &other.tombstones);
304
305        Ok(())
306    }
307
308    /// Clear old changelog entries to prevent unbounded growth
309    pub fn compact(&mut self, keep_versions: u64) {
310        if self.version > keep_versions {
311            let min_version = self.version - keep_versions;
312            self.changelog.retain(|v, _| *v > min_version);
313            self.changelog_floor = self.changelog_floor.max(min_version);
314        }
315    }
316
317    /// Return the greatest retained sequence number issued by `peer`.
318    ///
319    /// Both visible element tags and removal tombstones are retained state;
320    /// changelog compaction does not discard either set. `None` means this
321    /// peer has no retained tag. This reports observed state only: callers
322    /// remain responsible for allocator exhaustion and reservations that were
323    /// never inserted into the set.
324    #[must_use]
325    pub fn max_retained_sequence_for_peer(&self, peer: &PeerId) -> Option<u64> {
326        self.elements
327            .values()
328            .chain(self.tombstones.values())
329            .flat_map(HashSet::iter)
330            .filter(|(tag_peer, _)| tag_peer == peer)
331            .map(|(_, sequence)| *sequence)
332            .max()
333    }
334}
335
336impl<T: Hash + Eq + Clone> Default for OrSet<T> {
337    fn default() -> Self {
338        Self::new()
339    }
340}
341
342/// Delta for OR-Set (changes since a version)
343#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(bound = "T: Serialize + DeserializeOwned")]
345pub struct OrSetDelta<T: Hash + Eq + Clone + Serialize + DeserializeOwned> {
346    /// Added elements with their tags
347    pub added: HashMap<T, HashSet<UniqueTag>>,
348    /// Removed tags (tombstones)
349    pub removed: HashMap<T, HashSet<UniqueTag>>,
350    /// Version this delta represents
351    pub version: u64,
352}
353
354impl<T: Hash + Eq + Clone + Send + Sync + Serialize + DeserializeOwned> DeltaCrdt for OrSet<T> {
355    type Delta = OrSetDelta<T>;
356
357    fn merge(&mut self, delta: &Self::Delta) -> Result<()> {
358        self.apply_delta_parts(&delta.added, &delta.removed);
359
360        Ok(())
361    }
362
363    fn delta(&self, since_version: u64) -> Option<Self::Delta> {
364        if since_version >= self.version {
365            return None;
366        }
367        if since_version < self.changelog_floor {
368            return None;
369        }
370
371        let mut added: HashMap<T, HashSet<UniqueTag>> = HashMap::new();
372        let mut removed: HashMap<T, HashSet<UniqueTag>> = HashMap::new();
373
374        // Collect changes from changelog
375        for version in (since_version + 1)..=self.version {
376            let (adds, removes) = self.changelog.get(&version)?;
377            for (elem, tags) in adds {
378                added.entry(elem.clone()).or_default().extend(tags);
379            }
380            for (elem, tags) in removes {
381                removed.entry(elem.clone()).or_default().extend(tags);
382            }
383        }
384
385        Some(OrSetDelta {
386            added,
387            removed,
388            version: self.version,
389        })
390    }
391
392    fn version(&self) -> u64 {
393        self.version
394    }
395}
396
397/// Vector Clock for causality tracking
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
399pub struct VectorClock {
400    clocks: HashMap<PeerId, u64>,
401}
402
403impl VectorClock {
404    /// Create a new vector clock
405    pub fn new() -> Self {
406        Self {
407            clocks: HashMap::new(),
408        }
409    }
410
411    /// Increment the clock for a peer
412    pub fn increment(&mut self, peer: PeerId) {
413        *self.clocks.entry(peer).or_insert(0) += 1;
414    }
415
416    /// Get the clock value for a peer
417    pub fn get(&self, peer: &PeerId) -> u64 {
418        self.clocks.get(peer).copied().unwrap_or(0)
419    }
420
421    /// Merge another vector clock (take maximum of each entry)
422    pub fn merge(&mut self, other: &VectorClock) {
423        for (peer, &time) in &other.clocks {
424            let our_time = self.clocks.entry(*peer).or_insert(0);
425            *our_time = (*our_time).max(time);
426        }
427    }
428
429    /// Check if this clock happens before another
430    pub fn happens_before(&self, other: &VectorClock) -> bool {
431        let mut strictly_less = false;
432
433        // Check all our entries
434        for (peer, &our_time) in &self.clocks {
435            let other_time = other.get(peer);
436            if our_time > other_time {
437                return false; // We have a greater timestamp
438            }
439            if our_time < other_time {
440                strictly_less = true;
441            }
442        }
443
444        // Check for entries only in other
445        for peer in other.clocks.keys() {
446            if !self.clocks.contains_key(peer) && other.get(peer) > 0 {
447                strictly_less = true;
448            }
449        }
450
451        strictly_less
452    }
453
454    /// Check if two clocks are concurrent (neither happens before the other)
455    pub fn concurrent(&self, other: &VectorClock) -> bool {
456        !self.happens_before(other) && !other.happens_before(self) && self != other
457    }
458}
459
460impl Default for VectorClock {
461    fn default() -> Self {
462        Self::new()
463    }
464}
465
466/// LWW Register with Vector Clock
467#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct LwwRegister<T: Clone> {
469    value: T,
470    clock: VectorClock,
471}
472
473impl<T: Clone> LwwRegister<T> {
474    /// Create a new LWW register
475    pub fn new(value: T) -> Self {
476        Self {
477            value,
478            clock: VectorClock::new(),
479        }
480    }
481
482    /// Set value with vector clock
483    pub fn set(&mut self, value: T, peer: PeerId) {
484        self.clock.increment(peer);
485        self.value = value;
486    }
487
488    /// Get the current value
489    pub fn get(&self) -> &T {
490        &self.value
491    }
492
493    /// Get the vector clock
494    pub fn clock(&self) -> &VectorClock {
495        &self.clock
496    }
497
498    /// Merge with another register (keep value with later clock)
499    pub fn merge(&mut self, other: &LwwRegister<T>) {
500        if other.clock.happens_before(&self.clock) {
501            // Keep our value
502            self.clock.merge(&other.clock);
503        } else if self.clock.happens_before(&other.clock) {
504            // Take their value
505            self.value = other.value.clone();
506            self.clock = other.clock.clone();
507        } else if self.clock.concurrent(&other.clock) {
508            // Concurrent: use deterministic tiebreaker (smaller hash wins)
509            let our_hash = self.hash_clock();
510            let their_hash = other.hash_clock();
511
512            if their_hash < our_hash {
513                self.value = other.value.clone();
514            }
515            self.clock.merge(&other.clock);
516        }
517    }
518
519    fn hash_clock(&self) -> u64 {
520        use std::collections::hash_map::DefaultHasher;
521        use std::hash::{Hash, Hasher};
522
523        let mut hasher = DefaultHasher::new();
524        // Hash peer IDs and values in sorted order for determinism
525        let mut sorted: Vec<_> = self.clock.clocks.iter().collect();
526        sorted.sort_by_key(|(peer, _)| peer.as_bytes());
527        sorted.hash(&mut hasher);
528        hasher.finish()
529    }
530}
531
532/// Anti-Entropy Manager for periodic CRDT reconciliation
533///
534/// Manages periodic delta synchronization between peers to ensure
535/// eventual consistency. Uses exponential backoff for failed peers.
536pub struct AntiEntropyManager<T>
537where
538    T: DeltaCrdt + Clone + Send + Sync + 'static,
539{
540    /// The CRDT being synchronized
541    crdt: std::sync::Arc<tokio::sync::RwLock<T>>,
542    /// Local CRDT versions successfully delivered to each peer
543    peer_versions: std::sync::Arc<tokio::sync::RwLock<HashMap<PeerId, u64>>>,
544    /// Sync interval in seconds
545    interval_secs: u64,
546    /// Background task handle
547    task_handle: std::sync::Arc<tokio::sync::RwLock<Option<tokio::task::JoinHandle<()>>>>,
548    /// Shutdown signal
549    shutdown_tx: std::sync::Arc<tokio::sync::RwLock<Option<tokio::sync::mpsc::Sender<()>>>>,
550}
551
552impl<T> AntiEntropyManager<T>
553where
554    T: DeltaCrdt + Clone + Send + Sync + 'static,
555{
556    /// Create a new Anti-Entropy Manager
557    pub fn new(crdt: std::sync::Arc<tokio::sync::RwLock<T>>, interval_secs: u64) -> Self {
558        Self {
559            crdt,
560            peer_versions: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
561            interval_secs,
562            task_handle: std::sync::Arc::new(tokio::sync::RwLock::new(None)),
563            shutdown_tx: std::sync::Arc::new(tokio::sync::RwLock::new(None)),
564        }
565    }
566
567    /// Start anti-entropy synchronization
568    ///
569    /// Periodically syncs with peers using delta-based reconciliation.
570    /// Callback is called with (peer_id, delta) for each sync.
571    pub async fn start<F>(&self, sync_callback: F) -> Result<()>
572    where
573        F: Fn(
574                PeerId,
575                T::Delta,
576            )
577                -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
578            + Send
579            + Sync
580            + 'static,
581    {
582        let (shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
583
584        // Store shutdown sender
585        {
586            let mut tx = self.shutdown_tx.write().await;
587            *tx = Some(shutdown_tx);
588        }
589
590        let crdt = self.crdt.clone();
591        let peer_versions = self.peer_versions.clone();
592        let interval_secs = self.interval_secs;
593        let sync_callback = std::sync::Arc::new(sync_callback);
594
595        let task = tokio::spawn(async move {
596            let mut interval =
597                tokio::time::interval(tokio::time::Duration::from_secs(interval_secs));
598
599            loop {
600                tokio::select! {
601                    _ = interval.tick() => {
602                        // Get all peers we need to sync with
603                        let peers_to_sync: Vec<(PeerId, u64)> = {
604                            let versions = peer_versions.read().await;
605                            versions.iter().map(|(p, v)| (*p, *v)).collect()
606                        };
607
608                        // Generate and send deltas to each peer
609                        for (peer_id, peer_version) in peers_to_sync {
610                            let delta = {
611                                let crdt_lock = crdt.read().await;
612                                crdt_lock
613                                    .delta(peer_version)
614                                    .map(|delta| (delta, crdt_lock.version()))
615                            };
616
617                            if let Some((delta, sent_version)) = delta {
618                                let callback = sync_callback.clone();
619                                if let Err(e) = callback(peer_id, delta).await {
620                                    tracing::warn!("Failed to sync with peer {}: {}", LogPeerId::from(peer_id), e);
621                                } else {
622                                    let mut versions = peer_versions.write().await;
623                                    versions.insert(peer_id, sent_version);
624                                }
625                            }
626                        }
627                    }
628                    _ = shutdown_rx.recv() => {
629                        tracing::info!("Anti-entropy manager shutting down");
630                        break;
631                    }
632                }
633            }
634        });
635
636        // Store task handle
637        {
638            let mut handle = self.task_handle.write().await;
639            *handle = Some(task);
640        }
641
642        Ok(())
643    }
644
645    /// Stop anti-entropy synchronization
646    pub async fn stop(&self) -> Result<()> {
647        // Send shutdown signal
648        if let Some(tx) = self.shutdown_tx.write().await.take() {
649            let _ = tx.send(()).await;
650        }
651
652        // Wait for task to complete (with timeout)
653        if let Some(handle) = self.task_handle.write().await.take() {
654            match tokio::time::timeout(tokio::time::Duration::from_secs(5), handle).await {
655                Ok(Ok(())) => Ok(()),
656                Ok(Err(e)) => Err(anyhow::anyhow!("Task panicked: {}", e)),
657                Err(_) => Err(anyhow::anyhow!("Shutdown timeout")),
658            }
659        } else {
660            Ok(())
661        }
662    }
663
664    /// Update the local version a peer has acknowledged
665    ///
666    /// Call this after successfully sending a local delta to a peer.
667    pub async fn update_peer_version(&self, peer_id: PeerId, version: u64) {
668        let mut versions = self.peer_versions.write().await;
669        versions.insert(peer_id, version);
670    }
671
672    /// Apply a delta from a peer
673    ///
674    /// Merges the delta and registers the peer without treating its remote
675    /// version as an acknowledgement of our local changelog.
676    pub async fn apply_delta(
677        &self,
678        peer_id: PeerId,
679        delta: &T::Delta,
680        _version: u64,
681    ) -> Result<()> {
682        // Merge delta into our CRDT
683        {
684            let mut crdt = self.crdt.write().await;
685            crdt.merge(delta)?;
686        }
687
688        let mut versions = self.peer_versions.write().await;
689        versions.entry(peer_id).or_insert(0);
690
691        Ok(())
692    }
693
694    /// Add a peer to sync with
695    pub async fn add_peer(&self, peer_id: PeerId) {
696        let mut versions = self.peer_versions.write().await;
697        versions.entry(peer_id).or_insert(0);
698    }
699
700    /// Remove a peer from sync
701    pub async fn remove_peer(&self, peer_id: &PeerId) {
702        let mut versions = self.peer_versions.write().await;
703        versions.remove(peer_id);
704    }
705
706    /// Get current CRDT state
707    pub async fn get_crdt(&self) -> T {
708        self.crdt.read().await.clone()
709    }
710}
711
712#[cfg(test)]
713#[allow(clippy::unwrap_used, clippy::expect_used)]
714mod tests {
715    use super::*;
716
717    fn peer(n: u8) -> PeerId {
718        PeerId::new([n; 32])
719    }
720
721    // OR-Set Tests
722
723    #[test]
724    fn test_or_set_basic_add_remove() {
725        let mut set = OrSet::new();
726        let p1 = peer(1);
727
728        set.add("alice".to_string(), (p1, 1)).ok();
729        assert!(set.contains(&"alice".to_string()));
730        assert_eq!(set.len(), 1);
731
732        set.remove(&"alice".to_string()).ok();
733        assert!(!set.contains(&"alice".to_string()));
734        assert_eq!(set.len(), 0);
735    }
736
737    #[test]
738    fn test_or_set_concurrent_add() {
739        let mut set = OrSet::new();
740        let p1 = peer(1);
741        let p2 = peer(2);
742
743        // Concurrent adds from different peers
744        set.add("alice".to_string(), (p1, 1)).ok();
745        set.add("alice".to_string(), (p2, 1)).ok();
746
747        assert!(set.contains(&"alice".to_string()));
748
749        // Both tags should be present
750        assert_eq!(set.elements.get("alice").unwrap().len(), 2);
751    }
752
753    #[test]
754    fn test_or_set_add_remove_add() {
755        let mut set = OrSet::new();
756        let p1 = peer(1);
757
758        // Add, remove, add again
759        set.add("alice".to_string(), (p1, 1)).ok();
760        set.remove(&"alice".to_string()).ok();
761        set.add("alice".to_string(), (p1, 2)).ok(); // Different sequence number
762
763        assert!(set.contains(&"alice".to_string()));
764    }
765
766    #[test]
767    fn test_or_set_merge_commutativity() {
768        let mut set1 = OrSet::new();
769        let mut set2 = OrSet::new();
770        let p1 = peer(1);
771        let p2 = peer(2);
772
773        set1.add("alice".to_string(), (p1, 1)).ok();
774        set2.add("bob".to_string(), (p2, 1)).ok();
775
776        let mut merged_a = set1.clone();
777        merged_a.merge_state(&set2).ok();
778
779        let mut merged_b = set2.clone();
780        merged_b.merge_state(&set1).ok();
781
782        // Should converge to same state
783        assert_eq!(merged_a.len(), merged_b.len());
784        assert!(merged_a.contains(&"alice".to_string()));
785        assert!(merged_a.contains(&"bob".to_string()));
786        assert!(merged_b.contains(&"alice".to_string()));
787        assert!(merged_b.contains(&"bob".to_string()));
788    }
789
790    #[test]
791    fn test_or_set_merge_idempotence() {
792        let mut set1 = OrSet::new();
793        let mut set2 = OrSet::new();
794        let p1 = peer(1);
795
796        set1.add("alice".to_string(), (p1, 1)).ok();
797        set2.add("bob".to_string(), (p1, 2)).ok();
798
799        set1.merge_state(&set2).ok();
800        let len_after_first = set1.len();
801
802        set1.merge_state(&set2).ok();
803        let len_after_second = set1.len();
804
805        // Multiple merges should not change state
806        assert_eq!(len_after_first, len_after_second);
807    }
808
809    #[test]
810    fn test_or_set_delta_generation() {
811        let mut set = OrSet::new();
812        let p1 = peer(1);
813
814        let v0 = set.version();
815        set.add("alice".to_string(), (p1, 1)).ok();
816        set.add("bob".to_string(), (p1, 2)).ok();
817
818        let delta = set.delta(v0).expect("should have delta");
819        assert_eq!(delta.added.len(), 2);
820        assert!(delta.added.contains_key("alice"));
821        assert!(delta.added.contains_key("bob"));
822    }
823
824    #[test]
825    fn test_or_set_delta_merge() {
826        let mut set1 = OrSet::new();
827        let mut set2 = OrSet::new();
828        let p1 = peer(1);
829        let p2 = peer(2);
830
831        set1.add("alice".to_string(), (p1, 1)).ok();
832
833        let v0 = set2.version();
834        set2.add("bob".to_string(), (p2, 1)).ok();
835        set2.add("charlie".to_string(), (p2, 2)).ok();
836
837        // Get delta and merge
838        let delta = set2.delta(v0).expect("should have delta");
839        set1.merge(&delta).ok();
840
841        assert!(set1.contains(&"alice".to_string()));
842        assert!(set1.contains(&"bob".to_string()));
843        assert!(set1.contains(&"charlie".to_string()));
844    }
845
846    #[test]
847    fn test_or_set_delta_replay_does_not_resurrect_removed_tag() {
848        let mut source = OrSet::new();
849        let mut replica = OrSet::new();
850        let p1 = peer(1);
851        let element = "alice".to_string();
852
853        let v0 = source.version();
854        source.add(element.clone(), (p1, 1)).expect("add element");
855        let add_delta = source.delta(v0).expect("should have add delta");
856
857        replica.merge(&add_delta).expect("apply add delta");
858        assert!(replica.contains(&element));
859
860        replica.remove(&element).expect("remove element");
861        assert!(!replica.contains(&element));
862
863        replica.merge(&add_delta).expect("replay stale add delta");
864        assert!(!replica.contains(&element));
865        assert_eq!(replica.len(), 0);
866    }
867
868    #[test]
869    fn test_or_set_delta_merge_is_available_for_onward_delta() {
870        let mut relay = OrSet::new();
871        let mut source = OrSet::new();
872        let p2 = peer(2);
873
874        source.add("from-source".to_string(), (p2, 1)).ok();
875        let source_delta = source.delta(0).expect("should have source delta");
876
877        relay.merge(&source_delta).ok();
878
879        let onward_delta = relay.delta(0).expect("should have onward delta");
880        assert!(onward_delta
881            .added
882            .get("from-source")
883            .expect("forwarded add should be present")
884            .contains(&(p2, 1)));
885    }
886
887    // Vector Clock Tests
888
889    #[test]
890    fn test_vector_clock_increment() {
891        let mut clock = VectorClock::new();
892        let p1 = peer(1);
893
894        assert_eq!(clock.get(&p1), 0);
895        clock.increment(p1);
896        assert_eq!(clock.get(&p1), 1);
897        clock.increment(p1);
898        assert_eq!(clock.get(&p1), 2);
899    }
900
901    #[test]
902    fn test_vector_clock_merge() {
903        let mut clock1 = VectorClock::new();
904        let mut clock2 = VectorClock::new();
905        let p1 = peer(1);
906        let p2 = peer(2);
907
908        clock1.increment(p1);
909        clock1.increment(p1);
910
911        clock2.increment(p2);
912        clock2.increment(p2);
913        clock2.increment(p2);
914
915        clock1.merge(&clock2);
916
917        assert_eq!(clock1.get(&p1), 2);
918        assert_eq!(clock1.get(&p2), 3);
919    }
920
921    #[test]
922    fn test_vector_clock_happens_before() {
923        let mut clock1 = VectorClock::new();
924        let p1 = peer(1);
925
926        clock1.increment(p1);
927        let mut clock2 = clock1.clone();
928        clock2.increment(p1);
929
930        assert!(clock1.happens_before(&clock2));
931        assert!(!clock2.happens_before(&clock1));
932    }
933
934    #[test]
935    fn test_vector_clock_concurrent() {
936        let mut clock1 = VectorClock::new();
937        let mut clock2 = VectorClock::new();
938        let p1 = peer(1);
939        let p2 = peer(2);
940
941        clock1.increment(p1);
942        clock2.increment(p2);
943
944        assert!(clock1.concurrent(&clock2));
945        assert!(clock2.concurrent(&clock1));
946    }
947
948    // LWW Register Tests
949
950    #[test]
951    fn test_lww_register_basic() {
952        let mut reg = LwwRegister::new(42);
953        let p1 = peer(1);
954
955        assert_eq!(*reg.get(), 42);
956
957        reg.set(100, p1);
958        assert_eq!(*reg.get(), 100);
959    }
960
961    #[test]
962    fn test_lww_register_merge_causality() {
963        let mut reg1 = LwwRegister::new(0);
964        let p1 = peer(1);
965        let p2 = peer(2);
966
967        reg1.set(10, p1);
968        let mut reg2 = reg1.clone();
969        reg2.set(20, p2);
970
971        // reg2 happens after reg1
972        reg1.merge(&reg2);
973        assert_eq!(*reg1.get(), 20);
974    }
975
976    #[test]
977    fn test_lww_register_concurrent_merge() {
978        let mut reg1 = LwwRegister::new(0);
979        let mut reg2 = LwwRegister::new(0);
980        let p1 = peer(1);
981        let p2 = peer(2);
982
983        reg1.set(10, p1);
984        reg2.set(20, p2);
985
986        // Concurrent updates - should use deterministic tiebreaker
987        let mut merged1 = reg1.clone();
988        merged1.merge(&reg2);
989
990        let mut merged2 = reg2.clone();
991        merged2.merge(&reg1);
992
993        // Both should converge to same value
994        assert_eq!(merged1.get(), merged2.get());
995    }
996
997    #[test]
998    fn test_or_set_compact() {
999        let mut set = OrSet::new();
1000        let p1 = peer(1);
1001
1002        // Add many elements to build up changelog
1003        for i in 0..100 {
1004            set.add(format!("elem-{}", i), (p1, i)).ok();
1005        }
1006
1007        let changelog_before = set.changelog.len();
1008        set.compact(10);
1009        let changelog_after = set.changelog.len();
1010
1011        assert!(changelog_after <= 10);
1012        assert!(changelog_after < changelog_before);
1013    }
1014
1015    #[test]
1016    fn test_or_set_compacted_delta_before_floor_is_unavailable() {
1017        let mut set = OrSet::new();
1018        let p1 = peer(1);
1019
1020        set.add("old".to_string(), (p1, 1)).ok();
1021        set.add("new".to_string(), (p1, 2)).ok();
1022
1023        set.compact(1);
1024
1025        assert!(
1026            set.delta(0).is_none(),
1027            "compacted history must not produce a partial current delta"
1028        );
1029
1030        let delta = set.delta(1).expect("retained history should produce delta");
1031        assert!(!delta.added.contains_key("old"));
1032        assert!(delta.added.contains_key("new"));
1033        assert_eq!(delta.version, set.version());
1034    }
1035
1036    #[test]
1037    fn test_max_retained_sequence_empty_other_peer_and_zero() {
1038        let mut set = OrSet::new();
1039        let local = peer(1);
1040        let foreign = peer(2);
1041
1042        assert_eq!(set.max_retained_sequence_for_peer(&local), None);
1043        set.add("foreign".to_string(), (foreign, 7)).unwrap();
1044        assert_eq!(set.max_retained_sequence_for_peer(&local), None);
1045        set.add("zero".to_string(), (local, 0)).unwrap();
1046        assert_eq!(set.max_retained_sequence_for_peer(&local), Some(0));
1047    }
1048
1049    #[test]
1050    fn test_max_retained_sequence_is_peer_scoped_and_order_independent() {
1051        let local = peer(1);
1052        let foreign = peer(2);
1053        let mut first = OrSet::new();
1054        first.add("local-high".to_string(), (local, 100)).unwrap();
1055        first
1056            .add("foreign-max".to_string(), (foreign, u64::MAX))
1057            .unwrap();
1058        first.add("local-low".to_string(), (local, 3)).unwrap();
1059
1060        let mut second = OrSet::new();
1061        second.add("local-low".to_string(), (local, 3)).unwrap();
1062        second
1063            .add("foreign-max".to_string(), (foreign, u64::MAX))
1064            .unwrap();
1065        second.add("local-high".to_string(), (local, 100)).unwrap();
1066
1067        assert_eq!(first.max_retained_sequence_for_peer(&local), Some(100));
1068        assert_eq!(second.max_retained_sequence_for_peer(&local), Some(100));
1069    }
1070
1071    #[test]
1072    fn test_max_retained_sequence_survives_remove_and_compaction() {
1073        let local = peer(1);
1074        let mut set = OrSet::new();
1075        set.add("removed".to_string(), (local, 100)).unwrap();
1076        set.remove(&"removed".to_string()).unwrap();
1077        assert!(!set.contains(&"removed".to_string()));
1078        assert_eq!(set.max_retained_sequence_for_peer(&local), Some(100));
1079
1080        set.compact(0);
1081        assert!(set.delta(0).is_none());
1082        assert_eq!(set.max_retained_sequence_for_peer(&local), Some(100));
1083    }
1084
1085    #[test]
1086    fn test_max_retained_sequence_merges_live_and_removed_tags() {
1087        let local = peer(1);
1088        let mut removed_source = OrSet::new();
1089        removed_source
1090            .add("removed".to_string(), (local, 9))
1091            .unwrap();
1092        removed_source.remove(&"removed".to_string()).unwrap();
1093
1094        let mut live_source = OrSet::new();
1095        live_source.add("live".to_string(), (local, 12)).unwrap();
1096
1097        let mut removed_then_live = removed_source.clone();
1098        removed_then_live.merge_state(&live_source).unwrap();
1099        let mut live_then_removed = live_source.clone();
1100        live_then_removed.merge_state(&removed_source).unwrap();
1101
1102        assert_eq!(
1103            removed_then_live.max_retained_sequence_for_peer(&local),
1104            Some(12)
1105        );
1106        assert_eq!(
1107            live_then_removed.max_retained_sequence_for_peer(&local),
1108            Some(12)
1109        );
1110
1111        let mut lower = OrSet::new();
1112        lower.add("lower".to_string(), (local, 4)).unwrap();
1113        removed_then_live.merge_state(&lower).unwrap();
1114        assert_eq!(
1115            removed_then_live.max_retained_sequence_for_peer(&local),
1116            Some(12)
1117        );
1118    }
1119
1120    #[test]
1121    fn test_max_retained_sequence_survives_serde_after_remove_and_compact() {
1122        let local = peer(1);
1123        let foreign = peer(2);
1124        let mut set = OrSet::new();
1125        set.add("removed".to_string(), (local, 44)).unwrap();
1126        set.remove(&"removed".to_string()).unwrap();
1127        set.add("foreign".to_string(), (foreign, 55)).unwrap();
1128        set.compact(0);
1129
1130        let encoded = postcard::to_stdvec(&set).unwrap();
1131        let restored: OrSet<String> = postcard::from_bytes(&encoded).unwrap();
1132        assert_eq!(restored.max_retained_sequence_for_peer(&local), Some(44));
1133        assert_eq!(restored.max_retained_sequence_for_peer(&foreign), Some(55));
1134    }
1135
1136    #[test]
1137    fn test_max_retained_sequence_preserves_max_without_foreign_poisoning() {
1138        let local = peer(1);
1139        let foreign = peer(2);
1140        let mut set = OrSet::new();
1141        set.add("local-max".to_string(), (local, u64::MAX)).unwrap();
1142        set.add("foreign-max".to_string(), (foreign, u64::MAX))
1143            .unwrap();
1144
1145        assert_eq!(set.max_retained_sequence_for_peer(&local), Some(u64::MAX));
1146        assert_eq!(set.max_retained_sequence_for_peer(&foreign), Some(u64::MAX));
1147    }
1148
1149    // Anti-Entropy Manager Tests
1150
1151    #[tokio::test]
1152    async fn test_anti_entropy_manager_creation() {
1153        let crdt = std::sync::Arc::new(tokio::sync::RwLock::new(OrSet::<String>::new()));
1154        let manager = AntiEntropyManager::new(crdt, 1);
1155
1156        // Manager should be created successfully
1157        let state = manager.get_crdt().await;
1158        assert_eq!(state.len(), 0);
1159    }
1160
1161    #[tokio::test]
1162    async fn test_anti_entropy_add_remove_peer() {
1163        let crdt = std::sync::Arc::new(tokio::sync::RwLock::new(OrSet::<String>::new()));
1164        let manager = AntiEntropyManager::new(crdt, 1);
1165
1166        let p1 = peer(1);
1167        let p2 = peer(2);
1168
1169        // Add peers
1170        manager.add_peer(p1).await;
1171        manager.add_peer(p2).await;
1172
1173        // Update versions
1174        manager.update_peer_version(p1, 5).await;
1175        manager.update_peer_version(p2, 10).await;
1176
1177        // Verify versions are tracked
1178        let versions = manager.peer_versions.read().await;
1179        assert_eq!(versions.get(&p1), Some(&5));
1180        assert_eq!(versions.get(&p2), Some(&10));
1181
1182        // Remove peer
1183        drop(versions);
1184        manager.remove_peer(&p1).await;
1185
1186        let versions = manager.peer_versions.read().await;
1187        assert!(!versions.contains_key(&p1));
1188        assert_eq!(versions.get(&p2), Some(&10));
1189    }
1190
1191    #[tokio::test]
1192    async fn test_anti_entropy_apply_delta() {
1193        let crdt = std::sync::Arc::new(tokio::sync::RwLock::new(OrSet::<String>::new()));
1194        let manager = AntiEntropyManager::new(crdt.clone(), 1);
1195
1196        let p1 = peer(1);
1197
1198        // Create a delta
1199        let mut temp_set = OrSet::new();
1200        temp_set.add("alice".to_string(), (p1, 1)).ok();
1201        temp_set.add("bob".to_string(), (p1, 2)).ok();
1202
1203        let delta = temp_set.delta(0).expect("should have delta");
1204
1205        // Apply delta
1206        manager.apply_delta(p1, &delta, 2).await.ok();
1207
1208        // Verify CRDT state
1209        let state = manager.get_crdt().await;
1210        assert!(state.contains(&"alice".to_string()));
1211        assert!(state.contains(&"bob".to_string()));
1212
1213        // The remote version is not an acknowledgement of our local changelog.
1214        let versions = manager.peer_versions.read().await;
1215        assert_eq!(versions.get(&p1), Some(&0));
1216    }
1217
1218    #[tokio::test]
1219    async fn test_apply_high_remote_version_does_not_ack_local_delta() {
1220        let crdt = std::sync::Arc::new(tokio::sync::RwLock::new(OrSet::<String>::new()));
1221        let manager = AntiEntropyManager::new(crdt.clone(), 1);
1222
1223        let local_peer = peer(1);
1224        let remote_peer = peer(2);
1225        manager.add_peer(remote_peer).await;
1226
1227        {
1228            let mut crdt_lock = crdt.write().await;
1229            crdt_lock.add("local".to_string(), (local_peer, 1)).ok();
1230        }
1231
1232        let mut remote = OrSet::new();
1233        for seq in 1..=10 {
1234            remote
1235                .add(format!("remote-{}", seq), (remote_peer, seq))
1236                .ok();
1237        }
1238        let remote_delta = remote.delta(0).expect("should have remote delta");
1239
1240        manager
1241            .apply_delta(remote_peer, &remote_delta, remote_delta.version)
1242            .await
1243            .ok();
1244
1245        let peer_version = {
1246            let versions = manager.peer_versions.read().await;
1247            *versions
1248                .get(&remote_peer)
1249                .expect("remote peer should be tracked")
1250        };
1251        assert_eq!(peer_version, 0);
1252
1253        let delta_to_remote = {
1254            let crdt_lock = crdt.read().await;
1255            crdt_lock
1256                .delta(peer_version)
1257                .expect("local add should still be sent")
1258        };
1259
1260        assert!(delta_to_remote
1261            .added
1262            .get("local")
1263            .expect("local add should be included")
1264            .contains(&(local_peer, 1)));
1265    }
1266
1267    #[tokio::test]
1268    async fn test_anti_entropy_periodic_sync() {
1269        use std::sync::atomic::{AtomicUsize, Ordering};
1270
1271        let crdt = std::sync::Arc::new(tokio::sync::RwLock::new(OrSet::<String>::new()));
1272        let manager = AntiEntropyManager::new(crdt.clone(), 1); // 1 second interval
1273
1274        let p1 = peer(1);
1275        manager.add_peer(p1).await;
1276
1277        // Add some data to CRDT
1278        {
1279            let mut crdt_lock = crdt.write().await;
1280            crdt_lock.add("test".to_string(), (p1, 1)).ok();
1281        }
1282
1283        // Track sync calls
1284        let sync_count = std::sync::Arc::new(AtomicUsize::new(0));
1285        let sync_count_clone = sync_count.clone();
1286
1287        // Start syncing
1288        manager
1289            .start(move |peer_id, delta: <OrSet<String> as DeltaCrdt>::Delta| {
1290                let count = sync_count_clone.clone();
1291                Box::pin(async move {
1292                    count.fetch_add(1, Ordering::SeqCst);
1293                    // Verify we got the right peer
1294                    assert_eq!(peer_id, peer(1));
1295                    // Verify delta has our element
1296                    assert!(delta.added.contains_key("test"));
1297                    Ok(())
1298                })
1299            })
1300            .await
1301            .ok();
1302
1303        // Wait for at least 2 sync intervals
1304        tokio::time::sleep(tokio::time::Duration::from_millis(2100)).await;
1305
1306        // Stop syncing
1307        manager.stop().await.ok();
1308
1309        // Should have synced at least once
1310        assert!(sync_count.load(Ordering::SeqCst) >= 1);
1311        let versions = manager.peer_versions.read().await;
1312        assert_eq!(versions.get(&p1), Some(&1));
1313    }
1314
1315    #[tokio::test]
1316    async fn test_anti_entropy_shutdown() {
1317        let crdt = std::sync::Arc::new(tokio::sync::RwLock::new(OrSet::<String>::new()));
1318        let manager = AntiEntropyManager::new(crdt, 1);
1319
1320        // Start syncing
1321        manager
1322            .start(|_peer_id, _delta: <OrSet<String> as DeltaCrdt>::Delta| {
1323                Box::pin(async { Ok(()) })
1324            })
1325            .await
1326            .ok();
1327
1328        // Stop should complete within timeout
1329        let result = manager.stop().await;
1330        assert!(result.is_ok());
1331
1332        // Second stop should be idempotent
1333        let result = manager.stop().await;
1334        assert!(result.is_ok());
1335    }
1336
1337    #[tokio::test]
1338    async fn test_anti_entropy_multi_peer_sync() {
1339        let crdt = std::sync::Arc::new(tokio::sync::RwLock::new(OrSet::<String>::new()));
1340        let manager = AntiEntropyManager::new(crdt.clone(), 1);
1341
1342        let p1 = peer(1);
1343        let p2 = peer(2);
1344        let p3 = peer(3);
1345
1346        manager.add_peer(p1).await;
1347        manager.add_peer(p2).await;
1348        manager.add_peer(p3).await;
1349
1350        // Add data
1351        {
1352            let mut crdt_lock = crdt.write().await;
1353            crdt_lock.add("elem1".to_string(), (p1, 1)).ok();
1354            crdt_lock.add("elem2".to_string(), (p1, 2)).ok();
1355            crdt_lock.add("elem3".to_string(), (p1, 3)).ok();
1356        }
1357
1358        // Track which peers got synced
1359        let synced_peers = std::sync::Arc::new(tokio::sync::Mutex::new(HashSet::new()));
1360        let synced_peers_clone = synced_peers.clone();
1361
1362        manager
1363            .start(
1364                move |peer_id, _delta: <OrSet<String> as DeltaCrdt>::Delta| {
1365                    let peers = synced_peers_clone.clone();
1366                    Box::pin(async move {
1367                        peers.lock().await.insert(peer_id);
1368                        Ok(())
1369                    })
1370                },
1371            )
1372            .await
1373            .ok();
1374
1375        // Wait for sync
1376        tokio::time::sleep(tokio::time::Duration::from_millis(1100)).await;
1377
1378        manager.stop().await.ok();
1379
1380        // All peers should have been synced
1381        let peers = synced_peers.lock().await;
1382        assert!(peers.contains(&p1));
1383        assert!(peers.contains(&p2));
1384        assert!(peers.contains(&p3));
1385    }
1386
1387    #[test]
1388    fn canonical_retained_wire_roundtrips_and_blocks_stale_resurrection() {
1389        let origin = peer(7);
1390        let mut left = OrSet::new();
1391        left.add("visible".to_string(), (origin, 1))
1392            .expect("visible");
1393        left.add("removed".to_string(), (origin, 2))
1394            .expect("removed");
1395        let stale = left.clone();
1396        left.remove(&"removed".to_string()).expect("tombstone");
1397
1398        let encoded = bincode::serialize(&left.canonical_retained_wire()).expect("canonical wire");
1399        let mut restored: OrSet<String> = bincode::deserialize(&encoded).expect("old OrSet decode");
1400        assert!(restored.contains(&"visible".to_string()));
1401        assert!(!restored.contains(&"removed".to_string()));
1402
1403        restored.merge_state(&stale).expect("merge stale state");
1404        assert!(
1405            !restored.contains(&"removed".to_string()),
1406            "canonical tombstone must block a stale add tag"
1407        );
1408
1409        let mut right = OrSet::new();
1410        right
1411            .add("removed".to_string(), (origin, 2))
1412            .expect("removed");
1413        right
1414            .add("visible".to_string(), (origin, 1))
1415            .expect("visible");
1416        right.remove(&"removed".to_string()).expect("tombstone");
1417        assert_eq!(
1418            encoded,
1419            bincode::serialize(&right.canonical_retained_wire()).expect("reordered wire")
1420        );
1421    }
1422}