Skip to main content

stateset_sync/
state.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4/// Tracks the synchronization state between local and remote stores.
5///
6/// This mirrors the JS `_ves_sync_state` table, keeping track of
7/// local/remote heads, push/pull timestamps, and pending event count.
8///
9/// # Examples
10///
11/// ```
12/// use stateset_sync::SyncState;
13///
14/// let state = SyncState::default();
15/// assert_eq!(state.local_head, 0);
16/// assert_eq!(state.remote_head, 0);
17/// assert_eq!(state.pending_count, 0);
18/// ```
19#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
20pub struct SyncState {
21    /// The local head sequence number (last locally recorded event).
22    pub local_head: u64,
23    /// The remote head sequence number (last known remote sequence).
24    pub remote_head: u64,
25    /// Timestamp of the last successful push, if any.
26    pub last_push: Option<DateTime<Utc>>,
27    /// Timestamp of the last successful pull, if any.
28    pub last_pull: Option<DateTime<Utc>>,
29    /// Number of events pending push.
30    pub pending_count: usize,
31}
32
33impl SyncState {
34    /// Compute the lag (events behind remote).
35    #[must_use]
36    pub const fn lag(&self) -> u64 {
37        self.remote_head.saturating_sub(self.local_head)
38    }
39
40    /// Whether the local store is in sync with remote.
41    #[must_use]
42    pub const fn is_synced(&self) -> bool {
43        self.local_head >= self.remote_head && self.pending_count == 0
44    }
45}
46
47/// Overall sync status reported by the engine.
48///
49/// This is the Rust equivalent of the JS `SyncStatus` typedef.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct SyncStatus {
52    /// Whether the engine has been initialized.
53    pub initialized: bool,
54    /// Current local head sequence.
55    pub local_head: u64,
56    /// Current remote head sequence.
57    pub remote_head: u64,
58    /// Number of events pending push.
59    pub pending: usize,
60    /// Events behind remote.
61    pub lag: u64,
62    /// Timestamp of last push.
63    pub last_push: Option<DateTime<Utc>>,
64    /// Timestamp of last pull.
65    pub last_pull: Option<DateTime<Utc>>,
66    /// Number of events in the buffer.
67    pub buffered_events: usize,
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn default_state() {
76        let state = SyncState::default();
77        assert_eq!(state.local_head, 0);
78        assert_eq!(state.remote_head, 0);
79        assert!(state.last_push.is_none());
80        assert!(state.last_pull.is_none());
81        assert_eq!(state.pending_count, 0);
82    }
83
84    #[test]
85    fn lag_calculation() {
86        let state = SyncState { local_head: 5, remote_head: 15, ..Default::default() };
87        assert_eq!(state.lag(), 10);
88    }
89
90    #[test]
91    fn lag_when_local_ahead() {
92        let state = SyncState { local_head: 20, remote_head: 15, ..Default::default() };
93        assert_eq!(state.lag(), 0);
94    }
95
96    #[test]
97    fn is_synced_when_equal() {
98        let state =
99            SyncState { local_head: 10, remote_head: 10, pending_count: 0, ..Default::default() };
100        assert!(state.is_synced());
101    }
102
103    #[test]
104    fn not_synced_with_pending() {
105        let state =
106            SyncState { local_head: 10, remote_head: 10, pending_count: 3, ..Default::default() };
107        assert!(!state.is_synced());
108    }
109
110    #[test]
111    fn not_synced_when_behind() {
112        let state =
113            SyncState { local_head: 5, remote_head: 10, pending_count: 0, ..Default::default() };
114        assert!(!state.is_synced());
115    }
116
117    #[test]
118    fn state_serde_roundtrip() {
119        let state = SyncState {
120            local_head: 42,
121            remote_head: 100,
122            last_push: Some(Utc::now()),
123            last_pull: Some(Utc::now()),
124            pending_count: 7,
125        };
126        let json = serde_json::to_string(&state).unwrap();
127        let deserialized: SyncState = serde_json::from_str(&json).unwrap();
128        assert_eq!(deserialized.local_head, state.local_head);
129        assert_eq!(deserialized.remote_head, state.remote_head);
130        assert_eq!(deserialized.pending_count, state.pending_count);
131    }
132
133    #[test]
134    fn state_clone_eq() {
135        let state =
136            SyncState { local_head: 10, remote_head: 20, pending_count: 5, ..Default::default() };
137        let cloned = state.clone();
138        assert_eq!(state, cloned);
139    }
140
141    #[test]
142    fn sync_status_debug() {
143        let status = SyncStatus {
144            initialized: true,
145            local_head: 10,
146            remote_head: 20,
147            pending: 5,
148            lag: 10,
149            last_push: None,
150            last_pull: None,
151            buffered_events: 3,
152        };
153        let debug = format!("{status:?}");
154        assert!(debug.contains("SyncStatus"));
155    }
156}