Skip to main content

oxigdal_sync/crdt/
g_counter.rs

1//! Grow-only Counter CRDT
2//!
3//! A counter that can only be incremented, never decremented.
4
5use crate::crdt::{Crdt, DeviceAware};
6use crate::{DeviceId, SyncResult};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Grow-only Counter
11///
12/// A CRDT that represents a counter that can only increase.
13/// Each device maintains its own increment count, and the total
14/// is the sum of all device counts.
15///
16/// # Example
17///
18/// ```rust
19/// use oxigdal_sync::crdt::{GCounter, Crdt};
20///
21/// let mut counter1 = GCounter::new("device-1".to_string());
22/// counter1.increment(5);
23///
24/// let mut counter2 = GCounter::new("device-2".to_string());
25/// counter2.increment(3);
26///
27/// counter1.merge(&counter2).ok();
28/// assert_eq!(counter1.value(), 8);
29/// ```
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub struct GCounter {
32    /// Per-device increment counts
33    counts: HashMap<DeviceId, u64>,
34    /// This device's ID
35    device_id: DeviceId,
36}
37
38impl GCounter {
39    /// Creates a new G-Counter
40    ///
41    /// # Arguments
42    ///
43    /// * `device_id` - The device ID
44    pub fn new(device_id: DeviceId) -> Self {
45        let mut counts = HashMap::new();
46        counts.insert(device_id.clone(), 0);
47
48        Self { counts, device_id }
49    }
50
51    /// Increments the counter by the given amount
52    ///
53    /// # Arguments
54    ///
55    /// * `delta` - The amount to increment by
56    ///
57    /// # Returns
58    ///
59    /// The new total value
60    pub fn increment(&mut self, delta: u64) -> u64 {
61        let entry = self.counts.entry(self.device_id.clone()).or_insert(0);
62        *entry = entry.saturating_add(delta);
63        self.value()
64    }
65
66    /// Increments the counter by 1
67    ///
68    /// # Returns
69    ///
70    /// The new total value
71    pub fn inc(&mut self) -> u64 {
72        self.increment(1)
73    }
74
75    /// Gets the current total value
76    ///
77    /// This is the sum of all device counts.
78    pub fn value(&self) -> u64 {
79        self.counts.values().sum()
80    }
81
82    /// Gets the count for a specific device
83    ///
84    /// # Arguments
85    ///
86    /// * `device_id` - The device to query
87    ///
88    /// # Returns
89    ///
90    /// The count for that device, or 0 if not present
91    pub fn get_device_count(&self, device_id: &DeviceId) -> u64 {
92        self.counts.get(device_id).copied().unwrap_or(0)
93    }
94
95    /// Gets all device counts
96    pub fn counts(&self) -> &HashMap<DeviceId, u64> {
97        &self.counts
98    }
99
100    /// Gets the number of devices that have contributed
101    pub fn device_count(&self) -> usize {
102        self.counts.len()
103    }
104}
105
106impl Crdt for GCounter {
107    fn merge(&mut self, other: &Self) -> SyncResult<()> {
108        for (device_id, &count) in &other.counts {
109            let entry = self.counts.entry(device_id.clone()).or_insert(0);
110            *entry = (*entry).max(count);
111        }
112        Ok(())
113    }
114
115    fn dominated_by(&self, other: &Self) -> bool {
116        for (device_id, &count) in &self.counts {
117            let other_count = other.counts.get(device_id).copied().unwrap_or(0);
118            if count > other_count {
119                return false;
120            }
121        }
122        true
123    }
124}
125
126impl DeviceAware for GCounter {
127    fn device_id(&self) -> &DeviceId {
128        &self.device_id
129    }
130
131    fn set_device_id(&mut self, device_id: DeviceId) {
132        // Transfer count from old device to new device
133        if let Some(count) = self.counts.remove(&self.device_id) {
134            self.counts.insert(device_id.clone(), count);
135        }
136        self.device_id = device_id;
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_g_counter_creation() {
146        let counter = GCounter::new("device-1".to_string());
147        assert_eq!(counter.value(), 0);
148        assert_eq!(counter.device_id(), "device-1");
149    }
150
151    #[test]
152    fn test_g_counter_increment() {
153        let mut counter = GCounter::new("device-1".to_string());
154        counter.increment(5);
155        assert_eq!(counter.value(), 5);
156        counter.increment(3);
157        assert_eq!(counter.value(), 8);
158    }
159
160    #[test]
161    fn test_g_counter_inc() {
162        let mut counter = GCounter::new("device-1".to_string());
163        counter.inc();
164        counter.inc();
165        counter.inc();
166        assert_eq!(counter.value(), 3);
167    }
168
169    #[test]
170    fn test_g_counter_merge() {
171        let mut counter1 = GCounter::new("device-1".to_string());
172        let mut counter2 = GCounter::new("device-2".to_string());
173
174        counter1.increment(5);
175        counter2.increment(3);
176
177        counter1.merge(&counter2).ok();
178
179        assert_eq!(counter1.value(), 8);
180        assert_eq!(counter1.get_device_count(&"device-1".to_string()), 5);
181        assert_eq!(counter1.get_device_count(&"device-2".to_string()), 3);
182    }
183
184    #[test]
185    fn test_g_counter_merge_same_device() {
186        let mut counter1 = GCounter::new("device-1".to_string());
187        let mut counter2 = GCounter::new("device-1".to_string());
188
189        counter1.increment(5);
190        counter2.increment(3);
191
192        counter1.merge(&counter2).ok();
193
194        // Should take max
195        assert_eq!(counter1.value(), 5);
196    }
197
198    #[test]
199    fn test_g_counter_dominated_by() {
200        let mut counter1 = GCounter::new("device-1".to_string());
201        let mut counter2 = GCounter::new("device-1".to_string());
202
203        counter1.increment(3);
204        counter2.increment(5);
205
206        assert!(counter1.dominated_by(&counter2));
207        assert!(!counter2.dominated_by(&counter1));
208    }
209
210    #[test]
211    fn test_g_counter_device_count() {
212        let mut counter1 = GCounter::new("device-1".to_string());
213        let mut counter2 = GCounter::new("device-2".to_string());
214        let mut counter3 = GCounter::new("device-3".to_string());
215
216        counter1.increment(1);
217        counter2.increment(1);
218        counter3.increment(1);
219
220        counter1.merge(&counter2).ok();
221        counter1.merge(&counter3).ok();
222
223        assert_eq!(counter1.device_count(), 3);
224    }
225}