Skip to main content

peat_lite/
lww.rs

1// Copyright (c) 2025-2026 Defense Unicorns, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Last-Writer-Wins Register CRDT.
5//!
6//! A simple register that resolves conflicts by keeping the value
7//! with the highest timestamp. If timestamps are equal, the higher
8//! node ID wins (deterministic tiebreaker).
9
10use crate::node_id::NodeId;
11
12/// A Last-Writer-Wins Register.
13///
14/// Stores a single value with timestamp-based conflict resolution.
15/// Memory usage: `sizeof(T) + 12 bytes` (timestamp + node_id).
16///
17/// # Example
18///
19/// ```rust
20/// use peat_lite::{LwwRegister, NodeId};
21///
22/// let mut reg = LwwRegister::new(42i32, NodeId::new(1), 1000);
23///
24/// // Update from same node with newer timestamp
25/// assert!(reg.update(100, NodeId::new(1), 2000));
26/// assert_eq!(reg.value(), &100);
27///
28/// // Update from different node with older timestamp - rejected
29/// assert!(!reg.update(200, NodeId::new(2), 500));
30/// assert_eq!(reg.value(), &100);
31/// ```
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct LwwRegister<T> {
34    value: T,
35    timestamp: u64,
36    node_id: NodeId,
37}
38
39impl<T> LwwRegister<T> {
40    /// Create a new register with initial value.
41    pub const fn new(value: T, node_id: NodeId, timestamp: u64) -> Self {
42        Self {
43            value,
44            timestamp,
45            node_id,
46        }
47    }
48
49    /// Get the current value.
50    #[inline]
51    pub const fn value(&self) -> &T {
52        &self.value
53    }
54
55    /// Get the timestamp of the current value.
56    #[inline]
57    pub const fn timestamp(&self) -> u64 {
58        self.timestamp
59    }
60
61    /// Get the node that wrote the current value.
62    #[inline]
63    pub const fn node_id(&self) -> NodeId {
64        self.node_id
65    }
66
67    /// Update the value if the new write wins.
68    ///
69    /// Returns `true` if the value was updated, `false` if the
70    /// existing value wins.
71    ///
72    /// LWW rules:
73    /// 1. Higher timestamp wins
74    /// 2. If timestamps equal, higher node_id wins (deterministic)
75    pub fn update(&mut self, value: T, node_id: NodeId, timestamp: u64) -> bool {
76        if self.should_accept(timestamp, node_id) {
77            self.value = value;
78            self.timestamp = timestamp;
79            self.node_id = node_id;
80            true
81        } else {
82            false
83        }
84    }
85
86    /// Merge with another register.
87    ///
88    /// Takes the winning value based on LWW rules.
89    pub fn merge(&mut self, other: Self) {
90        if self.should_accept(other.timestamp, other.node_id) {
91            *self = other;
92        }
93    }
94
95    /// Check if an update with given timestamp/node_id should be accepted.
96    fn should_accept(&self, timestamp: u64, node_id: NodeId) -> bool {
97        timestamp > self.timestamp
98            || (timestamp == self.timestamp && node_id.as_u32() > self.node_id.as_u32())
99    }
100}
101
102impl<T: Default> Default for LwwRegister<T> {
103    fn default() -> Self {
104        Self {
105            value: T::default(),
106            timestamp: 0,
107            node_id: NodeId::NULL,
108        }
109    }
110}
111
112impl<T: Clone> LwwRegister<T> {
113    /// Get a clone of the current value.
114    pub fn value_cloned(&self) -> T {
115        self.value.clone()
116    }
117}
118
119/// A position value suitable for LwwRegister.
120///
121/// Uses fixed-point representation for no_std compatibility.
122/// Latitude/longitude stored as microdegrees (degrees × 1,000,000).
123/// Altitude stored as centimeters.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
125pub struct Position {
126    /// Latitude in microdegrees (degrees × 1,000,000)
127    pub lat_microdeg: i32,
128    /// Longitude in microdegrees (degrees × 1,000,000)
129    pub lon_microdeg: i32,
130    /// Altitude in centimeters above WGS84 ellipsoid
131    pub alt_cm: i32,
132}
133
134impl Position {
135    /// Create from floating-point degrees.
136    #[cfg(feature = "std")]
137    pub fn from_degrees(lat: f64, lon: f64, alt_m: f32) -> Self {
138        Self {
139            lat_microdeg: (lat * 1_000_000.0) as i32,
140            lon_microdeg: (lon * 1_000_000.0) as i32,
141            alt_cm: (alt_m * 100.0) as i32,
142        }
143    }
144
145    /// Convert to floating-point degrees.
146    #[cfg(feature = "std")]
147    pub fn to_degrees(&self) -> (f64, f64, f32) {
148        (
149            self.lat_microdeg as f64 / 1_000_000.0,
150            self.lon_microdeg as f64 / 1_000_000.0,
151            self.alt_cm as f32 / 100.0,
152        )
153    }
154
155    /// Encode to 12 bytes.
156    pub fn encode(&self) -> [u8; 12] {
157        let mut buf = [0u8; 12];
158        buf[0..4].copy_from_slice(&self.lat_microdeg.to_le_bytes());
159        buf[4..8].copy_from_slice(&self.lon_microdeg.to_le_bytes());
160        buf[8..12].copy_from_slice(&self.alt_cm.to_le_bytes());
161        buf
162    }
163
164    /// Decode from 12 bytes.
165    pub fn decode(data: &[u8; 12]) -> Self {
166        Self {
167            lat_microdeg: i32::from_le_bytes([data[0], data[1], data[2], data[3]]),
168            lon_microdeg: i32::from_le_bytes([data[4], data[5], data[6], data[7]]),
169            alt_cm: i32::from_le_bytes([data[8], data[9], data[10], data[11]]),
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_lww_basic() {
180        let mut reg = LwwRegister::new(10, NodeId::new(1), 100);
181        assert_eq!(reg.value(), &10);
182
183        // Newer timestamp wins
184        assert!(reg.update(20, NodeId::new(2), 200));
185        assert_eq!(reg.value(), &20);
186
187        // Older timestamp loses
188        assert!(!reg.update(30, NodeId::new(3), 150));
189        assert_eq!(reg.value(), &20);
190    }
191
192    #[test]
193    fn test_lww_tiebreaker() {
194        let mut reg = LwwRegister::new(10, NodeId::new(1), 100);
195
196        // Same timestamp, higher node_id wins
197        assert!(reg.update(20, NodeId::new(5), 100));
198        assert_eq!(reg.value(), &20);
199        assert_eq!(reg.node_id(), NodeId::new(5));
200
201        // Same timestamp, lower node_id loses
202        assert!(!reg.update(30, NodeId::new(3), 100));
203        assert_eq!(reg.value(), &20);
204    }
205
206    #[test]
207    fn test_lww_merge() {
208        let mut reg1 = LwwRegister::new(10, NodeId::new(1), 100);
209        let reg2 = LwwRegister::new(20, NodeId::new(2), 200);
210
211        reg1.merge(reg2);
212        assert_eq!(reg1.value(), &20);
213    }
214
215    #[test]
216    fn test_position_encode_decode() {
217        let pos = Position {
218            lat_microdeg: 37_774_929,   // ~37.774929° (San Francisco)
219            lon_microdeg: -122_419_416, // ~-122.419416°
220            alt_cm: 1000,               // 10m
221        };
222
223        let encoded = pos.encode();
224        let decoded = Position::decode(&encoded);
225        assert_eq!(pos, decoded);
226    }
227
228    #[cfg(feature = "std")]
229    #[test]
230    fn test_position_degrees() {
231        let pos = Position::from_degrees(37.774929, -122.419416, 10.0);
232        let (lat, lon, alt) = pos.to_degrees();
233
234        assert!((lat - 37.774929).abs() < 0.000001);
235        assert!((lon - (-122.419416)).abs() < 0.000001);
236        assert!((alt - 10.0).abs() < 0.01);
237    }
238}