Skip to main content

synckit_core/awareness/
state.rs

1/// Awareness State Management
2///
3/// Tracks ephemeral state for all connected clients.
4/// State is stored as arbitrary JSON and merged at the field level.
5use super::clock::IncreasingClock;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9// Time tracking only available on non-WASM targets
10#[cfg(not(target_arch = "wasm32"))]
11use std::time::{Duration, Instant};
12
13/// Awareness state for a single client
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AwarenessState {
16    /// Client identifier
17    pub client_id: String,
18
19    /// Arbitrary JSON state (user info, cursor, selection, etc.)
20    pub state: serde_json::Value,
21
22    /// Logical clock for conflict resolution
23    pub clock: u64,
24
25    /// Last update timestamp (for timeout detection)
26    /// Not available in WASM builds
27    #[cfg(not(target_arch = "wasm32"))]
28    #[serde(skip)]
29    pub last_updated: Option<Instant>,
30}
31
32/// Update message for awareness state changes
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct AwarenessUpdate {
35    pub client_id: String,
36    pub state: Option<serde_json::Value>, // None = client left
37    pub clock: u64,
38}
39
40/// Awareness manager tracking all client states
41#[derive(Debug)]
42pub struct Awareness {
43    client_id: String,
44    states: HashMap<String, AwarenessState>,
45    clock: IncreasingClock,
46}
47
48impl Awareness {
49    /// Create new awareness instance
50    pub fn new(client_id: String) -> Self {
51        Self {
52            client_id,
53            states: HashMap::new(),
54            clock: IncreasingClock::new(),
55        }
56    }
57
58    /// Get the local client ID
59    pub fn client_id(&self) -> &str {
60        &self.client_id
61    }
62
63    /// Get all current client states
64    pub fn get_states(&self) -> &HashMap<String, AwarenessState> {
65        &self.states
66    }
67
68    /// Get state for a specific client
69    pub fn get_state(&self, client_id: &str) -> Option<&AwarenessState> {
70        self.states.get(client_id)
71    }
72
73    /// Get local client's state
74    pub fn get_local_state(&self) -> Option<&AwarenessState> {
75        self.get_state(&self.client_id)
76    }
77
78    /// Set local client's state (returns update to broadcast)
79    pub fn set_local_state(&mut self, state: serde_json::Value) -> AwarenessUpdate {
80        let clock = self.clock.increment();
81
82        let awareness_state = AwarenessState {
83            client_id: self.client_id.clone(),
84            state: state.clone(),
85            clock,
86            #[cfg(not(target_arch = "wasm32"))]
87            last_updated: Some(Instant::now()),
88        };
89
90        self.states.insert(self.client_id.clone(), awareness_state);
91
92        AwarenessUpdate {
93            client_id: self.client_id.clone(),
94            state: Some(state),
95            clock,
96        }
97    }
98
99    /// Apply remote awareness update
100    pub fn apply_update(&mut self, update: AwarenessUpdate) {
101        // Update our clock to maintain monotonicity
102        self.clock.update_to_max(update.clock);
103
104        match update.state {
105            Some(state) => {
106                // Client is online with new state
107                let should_update = self
108                    .states
109                    .get(&update.client_id)
110                    .map(|existing| update.clock > existing.clock)
111                    .unwrap_or(true);
112
113                if should_update {
114                    self.states.insert(
115                        update.client_id.clone(),
116                        AwarenessState {
117                            client_id: update.client_id,
118                            state,
119                            clock: update.clock,
120                            #[cfg(not(target_arch = "wasm32"))]
121                            last_updated: Some(Instant::now()),
122                        },
123                    );
124                }
125            }
126            None => {
127                // Client left gracefully
128                self.states.remove(&update.client_id);
129            }
130        }
131    }
132
133    /// Remove clients that haven't updated within timeout
134    /// Returns list of removed client IDs
135    #[cfg(not(target_arch = "wasm32"))]
136    pub fn remove_stale_clients(&mut self, timeout: Duration) -> Vec<String> {
137        let now = Instant::now();
138        let mut removed = Vec::new();
139
140        self.states.retain(|client_id, state| {
141            if let Some(last_updated) = state.last_updated {
142                if now.duration_since(last_updated) > timeout {
143                    removed.push(client_id.clone());
144                    return false;
145                }
146            }
147            true
148        });
149
150        removed
151    }
152
153    /// Remove clients that haven't updated within timeout
154    /// Returns list of removed client IDs
155    /// WASM version: No-op since time tracking is not available
156    #[cfg(target_arch = "wasm32")]
157    pub fn remove_stale_clients(&mut self, _timeout_ms: u64) -> Vec<String> {
158        // Time tracking not available in WASM
159        // Stale client removal should be handled server-side
160        Vec::new()
161    }
162
163    /// Create update to signal local client leaving
164    pub fn create_leave_update(&self) -> AwarenessUpdate {
165        AwarenessUpdate {
166            client_id: self.client_id.clone(),
167            state: None,
168            clock: self.clock.increment(),
169        }
170    }
171
172    /// Get number of online clients (including self)
173    pub fn client_count(&self) -> usize {
174        self.states.len()
175    }
176
177    /// Get number of online clients excluding self
178    pub fn other_client_count(&self) -> usize {
179        self.states
180            .len()
181            .saturating_sub(if self.states.contains_key(&self.client_id) {
182                1
183            } else {
184                0
185            })
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use serde_json::json;
193
194    #[test]
195    fn test_set_local_state() {
196        let mut awareness = Awareness::new("client-1".to_string());
197
198        let state = json!({
199            "name": "Alice",
200            "color": "#FF0000",
201        });
202
203        let update = awareness.set_local_state(state.clone());
204
205        assert_eq!(update.client_id, "client-1");
206        assert_eq!(update.state, Some(state));
207        assert_eq!(update.clock, 1);
208        assert_eq!(awareness.client_count(), 1);
209    }
210
211    #[test]
212    fn test_apply_remote_update() {
213        let mut awareness = Awareness::new("client-1".to_string());
214
215        let update = AwarenessUpdate {
216            client_id: "client-2".to_string(),
217            state: Some(json!({"name": "Bob"})),
218            clock: 5,
219        };
220
221        awareness.apply_update(update);
222
223        assert_eq!(awareness.client_count(), 1);
224        assert!(awareness.get_state("client-2").is_some());
225    }
226
227    #[test]
228    fn test_clock_monotonicity() {
229        let mut awareness = Awareness::new("client-1".to_string());
230
231        // Apply update with high clock value
232        let update = AwarenessUpdate {
233            client_id: "client-2".to_string(),
234            state: Some(json!({})),
235            clock: 100,
236        };
237        awareness.apply_update(update);
238
239        // Local clock should be at least 100
240        let local_update = awareness.set_local_state(json!({}));
241        assert!(local_update.clock > 100);
242    }
243
244    #[test]
245    fn test_client_leaving() {
246        let mut awareness = Awareness::new("client-1".to_string());
247
248        // Add a client
249        awareness.apply_update(AwarenessUpdate {
250            client_id: "client-2".to_string(),
251            state: Some(json!({"name": "Bob"})),
252            clock: 1,
253        });
254        assert_eq!(awareness.client_count(), 1);
255
256        // Client leaves
257        awareness.apply_update(AwarenessUpdate {
258            client_id: "client-2".to_string(),
259            state: None,
260            clock: 2,
261        });
262        assert_eq!(awareness.client_count(), 0);
263    }
264
265    #[test]
266    fn test_other_client_count() {
267        let mut awareness = Awareness::new("client-1".to_string());
268
269        // Add self
270        awareness.set_local_state(json!({}));
271        assert_eq!(awareness.other_client_count(), 0);
272
273        // Add another client
274        awareness.apply_update(AwarenessUpdate {
275            client_id: "client-2".to_string(),
276            state: Some(json!({})),
277            clock: 1,
278        });
279        assert_eq!(awareness.other_client_count(), 1);
280    }
281}