Skip to main content

synckit_core/
document.rs

1//! Document structure with field-level Last-Write-Wins
2//!
3//! This implementation follows the TLA+ verified specification in
4//! protocol/tla/lww_merge.tla
5//!
6//! Properties verified:
7//! - Convergence: All replicas reach identical state
8//! - Determinism: Same inputs always produce same output
9//! - Idempotence: Applying operation twice has no effect
10//! - Commutativity: Order of merges doesn't matter
11
12use crate::sync::{Timestamp, VectorClock};
13use crate::{ClientID, DocumentID, FieldPath};
14// TODO: Will be used when implementing full error handling
15// use crate::error::{Result, SyncError};
16use serde::{Deserialize, Serialize};
17use serde_json::Value as JsonValue;
18use std::collections::HashMap;
19
20/// A document with field-level LWW conflict resolution
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Document {
23    /// Unique document identifier
24    pub id: DocumentID,
25
26    /// Document fields with LWW metadata
27    pub fields: HashMap<FieldPath, Field>,
28
29    /// Vector clock for causality tracking
30    pub version: VectorClock,
31}
32
33/// A single field with LWW metadata
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct Field {
36    /// Field value (JSON-like)
37    pub value: JsonValue,
38
39    /// Timestamp for LWW conflict resolution
40    pub timestamp: Timestamp,
41}
42
43impl Document {
44    /// Create a new empty document
45    pub fn new(id: DocumentID) -> Self {
46        Self {
47            id,
48            fields: HashMap::new(),
49            version: VectorClock::new(),
50        }
51    }
52
53    /// Set a field value (creates new timestamp)
54    ///
55    /// This method uses LWW merge logic, so if there's already a value
56    /// with a newer timestamp, it won't be overwritten.
57    pub fn set_field(
58        &mut self,
59        field_path: FieldPath,
60        value: JsonValue,
61        clock: u64,
62        client_id: ClientID,
63    ) {
64        let timestamp = Timestamp::new(clock, client_id);
65        let new_field = Field { value, timestamp };
66
67        // Use merge_field to respect LWW semantics
68        self.merge_field(field_path, new_field);
69    }
70
71    /// Get a field value
72    pub fn get_field(&self, field_path: &FieldPath) -> Option<&JsonValue> {
73        self.fields.get(field_path).map(|f| &f.value)
74    }
75
76    /// Merge a remote field using LWW algorithm
77    ///
78    /// This is the core LWW merge algorithm verified by TLA+.
79    /// Returns true if the local field was updated.
80    ///
81    /// Comparison order:
82    /// 1. Higher timestamp wins
83    /// 2. If timestamps equal, higher client_id wins
84    /// 3. If both equal (duplicate), use value comparison for determinism
85    pub fn merge_field(&mut self, field_path: FieldPath, remote_field: Field) -> bool {
86        match self.fields.get(&field_path) {
87            Some(local_field) => {
88                // Compare timestamps for LWW
89                match remote_field.timestamp.compare_lww(&local_field.timestamp) {
90                    std::cmp::Ordering::Greater => {
91                        // Remote wins (newer timestamp or higher client_id)
92                        self.fields.insert(field_path, remote_field);
93                        true
94                    }
95                    std::cmp::Ordering::Less => {
96                        // Local wins (newer timestamp or higher client_id)
97                        false
98                    }
99                    std::cmp::Ordering::Equal => {
100                        // Exact same timestamp - use value comparison for determinism
101                        // This handles the edge case where same client writes same timestamp
102                        // with different values (which shouldn't happen in practice, but
103                        // we handle it for total ordering)
104                        let local_json = serde_json::to_string(&local_field.value).unwrap();
105                        let remote_json = serde_json::to_string(&remote_field.value).unwrap();
106
107                        if remote_json > local_json {
108                            self.fields.insert(field_path, remote_field);
109                            true
110                        } else {
111                            // Keep local (or keep existing if values are also equal)
112                            false
113                        }
114                    }
115                }
116            }
117            None => {
118                // No local value, remote wins
119                self.fields.insert(field_path, remote_field);
120                true
121            }
122        }
123    }
124
125    /// Merge an entire remote document
126    ///
127    /// Merges all fields and vector clocks.
128    /// Returns the number of fields updated.
129    pub fn merge(&mut self, remote: &Document) -> usize {
130        let mut updated_count = 0;
131
132        // Merge each remote field
133        for (field_path, remote_field) in &remote.fields {
134            if self.merge_field(field_path.clone(), remote_field.clone()) {
135                updated_count += 1;
136            }
137        }
138
139        // Merge vector clocks
140        self.version.merge(&remote.version);
141
142        updated_count
143    }
144
145    /// Convert document to JSON for serialization
146    pub fn to_json(&self) -> JsonValue {
147        let mut obj = serde_json::Map::new();
148
149        for (field_path, field) in &self.fields {
150            obj.insert(field_path.clone(), field.value.clone());
151        }
152
153        JsonValue::Object(obj)
154    }
155
156    /// Get all field paths
157    pub fn field_paths(&self) -> Vec<&FieldPath> {
158        self.fields.keys().collect()
159    }
160
161    /// Check if document has any fields
162    pub fn is_empty(&self) -> bool {
163        self.fields.is_empty()
164    }
165
166    /// Get number of fields
167    pub fn field_count(&self) -> usize {
168        self.fields.len()
169    }
170
171    /// Get document ID
172    pub fn id(&self) -> &DocumentID {
173        &self.id
174    }
175
176    /// Get document version (vector clock)
177    pub fn version(&self) -> &VectorClock {
178        &self.version
179    }
180
181    /// Get all fields with metadata
182    pub fn fields(&self) -> &HashMap<FieldPath, Field> {
183        &self.fields
184    }
185
186    /// Delete a field
187    pub fn delete_field(&mut self, field_path: &FieldPath) {
188        self.fields.remove(field_path);
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use serde_json::json;
196
197    #[test]
198    fn test_document_creation() {
199        let doc = Document::new("doc-123".to_string());
200        assert_eq!(doc.id, "doc-123");
201        assert!(doc.is_empty());
202    }
203
204    #[test]
205    fn test_set_and_get_field() {
206        let mut doc = Document::new("doc-123".to_string());
207
208        doc.set_field(
209            "title".to_string(),
210            json!("Hello World"),
211            1,
212            "client1".to_string(),
213        );
214
215        assert_eq!(
216            doc.get_field(&"title".to_string()),
217            Some(&json!("Hello World"))
218        );
219        assert_eq!(doc.field_count(), 1);
220    }
221
222    #[test]
223    fn test_lww_merge_remote_wins() {
224        let mut doc = Document::new("doc-123".to_string());
225
226        // Local writes at timestamp 1
227        doc.set_field(
228            "title".to_string(),
229            json!("Local Title"),
230            1,
231            "client1".to_string(),
232        );
233
234        // Remote writes at timestamp 2 (newer)
235        let remote_field = Field {
236            value: json!("Remote Title"),
237            timestamp: Timestamp::new(2, "client2".to_string()),
238        };
239
240        let updated = doc.merge_field("title".to_string(), remote_field);
241
242        assert!(updated);
243        assert_eq!(
244            doc.get_field(&"title".to_string()),
245            Some(&json!("Remote Title"))
246        );
247    }
248
249    #[test]
250    fn test_lww_merge_local_wins() {
251        let mut doc = Document::new("doc-123".to_string());
252
253        // Local writes at timestamp 2
254        doc.set_field(
255            "title".to_string(),
256            json!("Local Title"),
257            2,
258            "client1".to_string(),
259        );
260
261        // Remote writes at timestamp 1 (older)
262        let remote_field = Field {
263            value: json!("Remote Title"),
264            timestamp: Timestamp::new(1, "client2".to_string()),
265        };
266
267        let updated = doc.merge_field("title".to_string(), remote_field);
268
269        assert!(!updated);
270        assert_eq!(
271            doc.get_field(&"title".to_string()),
272            Some(&json!("Local Title"))
273        );
274    }
275
276    #[test]
277    fn test_lww_merge_tie_breaking() {
278        let mut doc = Document::new("doc-123".to_string());
279
280        // Local writes at timestamp 1 with client1
281        doc.set_field(
282            "title".to_string(),
283            json!("Local Title"),
284            1,
285            "client1".to_string(),
286        );
287
288        // Remote writes at timestamp 1 with client2 (client2 > client1)
289        let remote_field = Field {
290            value: json!("Remote Title"),
291            timestamp: Timestamp::new(1, "client2".to_string()),
292        };
293
294        let updated = doc.merge_field("title".to_string(), remote_field);
295
296        // client2 > client1, so remote wins
297        assert!(updated);
298        assert_eq!(
299            doc.get_field(&"title".to_string()),
300            Some(&json!("Remote Title"))
301        );
302    }
303
304    #[test]
305    fn test_merge_entire_document() {
306        let mut doc1 = Document::new("doc-123".to_string());
307        doc1.set_field(
308            "field1".to_string(),
309            json!("value1"),
310            1,
311            "client1".to_string(),
312        );
313        doc1.set_field(
314            "field2".to_string(),
315            json!("value2"),
316            1,
317            "client1".to_string(),
318        );
319
320        let mut doc2 = Document::new("doc-123".to_string());
321        doc2.set_field(
322            "field1".to_string(),
323            json!("new_value1"),
324            2,
325            "client2".to_string(),
326        );
327        doc2.set_field(
328            "field3".to_string(),
329            json!("value3"),
330            1,
331            "client2".to_string(),
332        );
333
334        // Merge doc2 into doc1
335        let updated_count = doc1.merge(&doc2);
336
337        // field1 should be updated (newer timestamp)
338        // field3 should be added (new field)
339        // field2 should remain unchanged
340        assert_eq!(updated_count, 2);
341        assert_eq!(
342            doc1.get_field(&"field1".to_string()),
343            Some(&json!("new_value1"))
344        );
345        assert_eq!(
346            doc1.get_field(&"field2".to_string()),
347            Some(&json!("value2"))
348        );
349        assert_eq!(
350            doc1.get_field(&"field3".to_string()),
351            Some(&json!("value3"))
352        );
353    }
354
355    #[test]
356    fn test_document_to_json() {
357        let mut doc = Document::new("doc-123".to_string());
358        doc.set_field(
359            "title".to_string(),
360            json!("Hello"),
361            1,
362            "client1".to_string(),
363        );
364        doc.set_field("count".to_string(), json!(42), 1, "client1".to_string());
365
366        let json = doc.to_json();
367        assert_eq!(json["title"], json!("Hello"));
368        assert_eq!(json["count"], json!(42));
369    }
370
371    #[test]
372    fn test_convergence_property() {
373        // Test convergence: two replicas merging in different orders reach same state
374
375        let mut replica1 = Document::new("doc-123".to_string());
376        let mut replica2 = Document::new("doc-123".to_string());
377
378        // Client1 writes
379        let client1_update = Document {
380            id: "doc-123".to_string(),
381            fields: {
382                let mut map = HashMap::new();
383                map.insert(
384                    "field1".to_string(),
385                    Field {
386                        value: json!("A"),
387                        timestamp: Timestamp::new(1, "client1".to_string()),
388                    },
389                );
390                map
391            },
392            version: VectorClock::new(),
393        };
394
395        // Client2 writes
396        let client2_update = Document {
397            id: "doc-123".to_string(),
398            fields: {
399                let mut map = HashMap::new();
400                map.insert(
401                    "field1".to_string(),
402                    Field {
403                        value: json!("B"),
404                        timestamp: Timestamp::new(2, "client2".to_string()),
405                    },
406                );
407                map
408            },
409            version: VectorClock::new(),
410        };
411
412        // Replica1 merges in order: client1, then client2
413        replica1.merge(&client1_update);
414        replica1.merge(&client2_update);
415
416        // Replica2 merges in reverse order: client2, then client1
417        replica2.merge(&client2_update);
418        replica2.merge(&client1_update);
419
420        // Both replicas should converge to same state
421        assert_eq!(
422            replica1.get_field(&"field1".to_string()),
423            replica2.get_field(&"field1".to_string())
424        );
425
426        // Should be client2's value (timestamp 2 > timestamp 1)
427        assert_eq!(replica1.get_field(&"field1".to_string()), Some(&json!("B")));
428        assert_eq!(replica2.get_field(&"field1".to_string()), Some(&json!("B")));
429    }
430}