Skip to main content

calimero_primitives/
crdt.rs

1//! CRDT (Conflict-free Replicated Data Type) primitives.
2//!
3//! This module provides the unified `CrdtType` enum used across the codebase
4//! for identifying CRDT semantics during storage and synchronization.
5
6#[cfg(feature = "borsh")]
7use borsh::{BorshDeserialize, BorshSerialize};
8use serde::{Deserialize, Serialize};
9
10/// CRDT type indicator for merge semantics.
11///
12/// Identifies the conflict resolution strategy used when merging replicated data.
13/// This enum is used both by the storage layer (for persistence metadata) and
14/// the sync protocol (for wire-format entity classification).
15///
16/// # Merge Semantics
17///
18/// Each variant defines specific merge behavior:
19/// - **Registers**: LwwRegister (timestamp-based)
20/// - **Counters**: GCounter (grow-only), PnCounter (increment/decrement)
21/// - **Collections**: Rga, UnorderedMap, UnorderedSet, Vector
22/// - **Special**: UserStorage, FrozenStorage, Custom
23#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
24#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
25pub enum CrdtType {
26    // =========================================================================
27    // REGISTERS
28    // =========================================================================
29    /// Last-Writer-Wins Register.
30    ///
31    /// Wraps primitive types with timestamp-based conflict resolution.
32    /// Merge: Higher HLC timestamp wins, with node ID as tie-breaker.
33    ///
34    /// The inner type name enables proper deserialization during merge.
35    LwwRegister {
36        /// Inner type name (e.g., "String", "u64", "MyStruct")
37        inner_type: String,
38    },
39
40    // =========================================================================
41    // COUNTERS
42    // =========================================================================
43    /// Grow-only Counter.
44    ///
45    /// Supports only increment operations; value can never decrease.
46    /// Internally tracks increments per executor.
47    /// Merge: Take maximum of each executor's count.
48    GCounter,
49
50    /// Positive-Negative Counter.
51    ///
52    /// Supports both increment and decrement operations.
53    /// Internally uses two maps: positive and negative counts per executor.
54    /// Merge: Union of positive maps, union of negative maps, then compute difference.
55    PnCounter,
56
57    // =========================================================================
58    // COLLECTIONS
59    // =========================================================================
60    /// Replicated Growable Array.
61    ///
62    /// CRDT for collaborative text editing and ordered sequences.
63    /// Supports concurrent insertions and deletions with causal ordering.
64    /// Merge: Interleave elements by (timestamp, node_id) ordering.
65    Rga,
66
67    /// Unordered Map.
68    ///
69    /// Key-value store with add-wins semantics for keys.
70    /// Keys are never lost once added (tombstoned but retained).
71    /// Values are merged recursively if they implement Mergeable.
72    /// Merge: Union of keys, recursive merge of values.
73    UnorderedMap {
74        /// Key type name
75        key_type: String,
76        /// Value type name (may be a nested CRDT)
77        value_type: String,
78    },
79
80    /// Unordered Set.
81    ///
82    /// Collection of unique values with add-wins semantics.
83    /// Elements are never lost once added.
84    /// Merge: Union of all elements from both sets.
85    UnorderedSet {
86        /// Element type name
87        element_type: String,
88    },
89
90    /// Vector (ordered collection).
91    ///
92    /// Ordered list with append operations.
93    /// Elements are identified by index + timestamp for ordering.
94    /// Merge: Element-wise merge by index with timestamp ordering.
95    Vector {
96        /// Element type name (may be a nested CRDT)
97        element_type: String,
98    },
99
100    // =========================================================================
101    // SPECIAL STORAGE
102    // =========================================================================
103    /// User Storage.
104    ///
105    /// Per-user data storage with signature-based access control.
106    /// Only the owning user (identified by executor ID) can modify their data.
107    /// Merge: Latest update per user based on nonce/timestamp.
108    UserStorage,
109
110    /// Frozen Storage.
111    ///
112    /// Write-once storage for immutable data.
113    /// Data can be written once and never modified or deleted.
114    /// Merge: First-write-wins (subsequent writes are no-ops).
115    FrozenStorage,
116
117    /// Custom CRDT with app-defined merge.
118    ///
119    /// For types annotated with `#[derive(CrdtState)]` that define custom merge logic.
120    /// The string identifies the custom type name within the application.
121    /// Merge: Dispatched to WASM runtime to call the app's merge function.
122    Custom(String),
123}
124
125impl Default for CrdtType {
126    fn default() -> Self {
127        Self::LwwRegister {
128            inner_type: String::new(),
129        }
130    }
131}
132
133impl CrdtType {
134    /// Create an LwwRegister with a known inner type.
135    #[must_use]
136    pub fn lww_register(inner_type: impl Into<String>) -> Self {
137        Self::LwwRegister {
138            inner_type: inner_type.into(),
139        }
140    }
141
142    /// Create an UnorderedMap with known key and value types.
143    #[must_use]
144    pub fn unordered_map(key_type: impl Into<String>, value_type: impl Into<String>) -> Self {
145        Self::UnorderedMap {
146            key_type: key_type.into(),
147            value_type: value_type.into(),
148        }
149    }
150
151    /// Create an UnorderedSet with a known element type.
152    #[must_use]
153    pub fn unordered_set(element_type: impl Into<String>) -> Self {
154        Self::UnorderedSet {
155            element_type: element_type.into(),
156        }
157    }
158
159    /// Create a Vector with a known element type.
160    #[must_use]
161    pub fn vector(element_type: impl Into<String>) -> Self {
162        Self::Vector {
163            element_type: element_type.into(),
164        }
165    }
166
167    /// Returns `true` if this is a counter type (GCounter or PnCounter).
168    #[must_use]
169    pub const fn is_counter(&self) -> bool {
170        matches!(self, Self::GCounter | Self::PnCounter)
171    }
172
173    /// Returns `true` if this is a set type.
174    #[must_use]
175    pub const fn is_set(&self) -> bool {
176        matches!(self, Self::UnorderedSet { .. })
177    }
178
179    /// Returns `true` if this is a collection type (map, set, vector, or array).
180    #[must_use]
181    pub const fn is_collection(&self) -> bool {
182        matches!(
183            self,
184            Self::UnorderedMap { .. } | Self::UnorderedSet { .. } | Self::Vector { .. } | Self::Rga
185        )
186    }
187
188    /// Returns `true` if this is a custom CRDT type.
189    #[must_use]
190    pub const fn is_custom(&self) -> bool {
191        matches!(self, Self::Custom(_))
192    }
193
194    /// Returns `true` if this type requires special storage handling.
195    #[must_use]
196    pub const fn is_special_storage(&self) -> bool {
197        matches!(self, Self::UserStorage | Self::FrozenStorage)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_default_is_lww_register() {
207        assert!(matches!(CrdtType::default(), CrdtType::LwwRegister { .. }));
208    }
209
210    #[test]
211    fn test_lww_register_constructor() {
212        let lww = CrdtType::lww_register("String");
213        assert_eq!(
214            lww,
215            CrdtType::LwwRegister {
216                inner_type: "String".to_string()
217            }
218        );
219    }
220
221    #[test]
222    fn test_is_counter() {
223        assert!(CrdtType::GCounter.is_counter());
224        assert!(CrdtType::PnCounter.is_counter());
225        assert!(!CrdtType::lww_register("u64").is_counter());
226        assert!(!CrdtType::unordered_map("String", "u64").is_counter());
227    }
228
229    #[test]
230    fn test_is_set() {
231        assert!(CrdtType::unordered_set("String").is_set());
232        assert!(!CrdtType::unordered_map("String", "u64").is_set());
233        assert!(!CrdtType::vector("u64").is_set());
234    }
235
236    #[test]
237    fn test_is_collection() {
238        assert!(CrdtType::unordered_map("String", "u64").is_collection());
239        assert!(CrdtType::unordered_set("String").is_collection());
240        assert!(CrdtType::vector("u64").is_collection());
241        assert!(CrdtType::Rga.is_collection());
242        assert!(!CrdtType::lww_register("u64").is_collection());
243        assert!(!CrdtType::GCounter.is_collection());
244        assert!(!CrdtType::PnCounter.is_collection());
245    }
246
247    #[test]
248    fn test_is_custom() {
249        assert!(CrdtType::Custom("test".to_string()).is_custom());
250        assert!(!CrdtType::lww_register("u64").is_custom());
251    }
252
253    #[test]
254    fn test_is_special_storage() {
255        assert!(CrdtType::UserStorage.is_special_storage());
256        assert!(CrdtType::FrozenStorage.is_special_storage());
257        assert!(!CrdtType::lww_register("u64").is_special_storage());
258        assert!(!CrdtType::GCounter.is_special_storage());
259    }
260
261    #[test]
262    fn test_serde_roundtrip() {
263        let types = [
264            CrdtType::lww_register("String"),
265            CrdtType::lww_register("u64"),
266            CrdtType::GCounter,
267            CrdtType::PnCounter,
268            CrdtType::Rga,
269            CrdtType::unordered_map("String", "u64"),
270            CrdtType::unordered_set("String"),
271            CrdtType::vector("u64"),
272            CrdtType::UserStorage,
273            CrdtType::FrozenStorage,
274            CrdtType::Custom("my_type".to_string()),
275        ];
276
277        for crdt_type in &types {
278            let json = serde_json::to_string(crdt_type).unwrap();
279            let decoded: CrdtType = serde_json::from_str(&json).unwrap();
280            assert_eq!(*crdt_type, decoded);
281        }
282    }
283
284    #[cfg(feature = "borsh")]
285    #[test]
286    fn test_borsh_roundtrip() {
287        let types = [
288            CrdtType::lww_register("String"),
289            CrdtType::lww_register("u64"),
290            CrdtType::GCounter,
291            CrdtType::PnCounter,
292            CrdtType::Rga,
293            CrdtType::unordered_map("String", "u64"),
294            CrdtType::unordered_set("String"),
295            CrdtType::vector("u64"),
296            CrdtType::UserStorage,
297            CrdtType::FrozenStorage,
298            CrdtType::Custom("my_type".to_string()),
299        ];
300
301        for crdt_type in &types {
302            let bytes = borsh::to_vec(crdt_type).unwrap();
303            let decoded: CrdtType = borsh::from_slice(&bytes).unwrap();
304            assert_eq!(*crdt_type, decoded);
305        }
306    }
307}