Skip to main content

tpcp_core/
lwwmap.rs

1use alloc::collections::BTreeMap;
2use alloc::string::String;
3use serde_json::Value;
4
5/// Single LWW entry with value, timestamp, and writer identity.
6#[derive(Debug, Clone)]
7pub struct LWWEntry {
8    pub value: Value,
9    pub timestamp_ms: i64,
10    pub writer_id: String,
11}
12
13/// Last-Write-Wins CRDT map using BTreeMap (no_std compatible).
14/// Tie-breaking: timestamp > writer_id (lexicographic), matching Python's LWWMap.
15#[derive(Debug, Clone, Default)]
16pub struct LWWMap {
17    map: BTreeMap<String, LWWEntry>,
18}
19
20impl LWWMap {
21    /// Creates an empty LWWMap.
22    pub fn new() -> Self {
23        Self { map: BTreeMap::new() }
24    }
25
26    /// Writes a value with the given timestamp and writer ID.
27    /// No-op if a newer value exists. Equal timestamps are broken by writer_id (higher wins).
28    pub fn set(&mut self, key: &str, value: Value, timestamp_ms: i64, writer_id: &str) {
29        if let Some(existing) = self.map.get(key) {
30            if existing.timestamp_ms > timestamp_ms {
31                return;
32            }
33            if existing.timestamp_ms == timestamp_ms && existing.writer_id.as_str() >= writer_id {
34                return;
35            }
36        }
37        self.map.insert(key.into(), LWWEntry { value, timestamp_ms, writer_id: writer_id.into() });
38    }
39
40    /// Returns the value for a key, or None if absent.
41    pub fn get(&self, key: &str) -> Option<&Value> {
42        self.map.get(key).map(|e| &e.value)
43    }
44
45    /// Merges another LWWMap into this one using LWW semantics.
46    pub fn merge(&mut self, other: &LWWMap) {
47        for (k, entry) in &other.map {
48            self.set(k, entry.value.clone(), entry.timestamp_ms, &entry.writer_id);
49        }
50    }
51
52    /// Returns a plain BTreeMap snapshot of all current values.
53    pub fn to_map(&self) -> BTreeMap<String, Value> {
54        self.map.iter().map(|(k, v)| (k.clone(), v.value.clone())).collect()
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn test_set_get() {
64        let mut m = LWWMap::new();
65        m.set("key", Value::String("hello".into()), 100, "agent-a");
66        assert_eq!(m.get("key"), Some(&Value::String("hello".into())));
67        assert_eq!(m.get("missing"), None);
68    }
69
70    #[test]
71    fn test_last_writer_wins() {
72        let mut m = LWWMap::new();
73        m.set("x", Value::Number(1.into()), 100, "agent-a");
74        // Older timestamp — must NOT overwrite.
75        m.set("x", Value::Number(0.into()), 50, "agent-b");
76        assert_eq!(m.get("x"), Some(&Value::Number(1.into())), "older write must not overwrite newer value");
77
78        // Equal timestamp, same writer — must NOT overwrite (>= guard).
79        m.set("x", Value::Number(99.into()), 100, "agent-a");
80        assert_eq!(m.get("x"), Some(&Value::Number(1.into())), "equal timestamp+writer must not overwrite");
81
82        // Equal timestamp, higher writer_id — MUST overwrite (tie-break).
83        m.set("x", Value::Number(77.into()), 100, "agent-z");
84        assert_eq!(m.get("x"), Some(&Value::Number(77.into())), "equal timestamp with higher writer_id must overwrite");
85
86        // Newer timestamp — must overwrite.
87        m.set("x", Value::Number(2.into()), 200, "agent-a");
88        assert_eq!(m.get("x"), Some(&Value::Number(2.into())), "newer write must overwrite");
89    }
90
91    #[test]
92    fn test_merge() {
93        let mut a = LWWMap::new();
94        a.set("shared", Value::String("from_a".into()), 100, "agent-a");
95        a.set("only_a", Value::Bool(true), 50, "agent-a");
96
97        let mut b = LWWMap::new();
98        b.set("shared", Value::String("from_b".into()), 200, "agent-b"); // newer
99        b.set("only_b", Value::Number(42.into()), 75, "agent-b");
100
101        a.merge(&b);
102
103        // "shared" should now hold b's newer value.
104        assert_eq!(
105            a.get("shared"),
106            Some(&Value::String("from_b".into())),
107            "merge must pick the newer value for conflicting keys"
108        );
109        // Keys unique to each map must both be present.
110        assert_eq!(a.get("only_a"), Some(&Value::Bool(true)));
111        assert_eq!(a.get("only_b"), Some(&Value::Number(42.into())));
112    }
113}