synckit_core/awareness/
state.rs1use super::clock::IncreasingClock;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[cfg(not(target_arch = "wasm32"))]
11use std::time::{Duration, Instant};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AwarenessState {
16 pub client_id: String,
18
19 pub state: serde_json::Value,
21
22 pub clock: u64,
24
25 #[cfg(not(target_arch = "wasm32"))]
28 #[serde(skip)]
29 pub last_updated: Option<Instant>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct AwarenessUpdate {
35 pub client_id: String,
36 pub state: Option<serde_json::Value>, pub clock: u64,
38}
39
40#[derive(Debug)]
42pub struct Awareness {
43 client_id: String,
44 states: HashMap<String, AwarenessState>,
45 clock: IncreasingClock,
46}
47
48impl Awareness {
49 pub fn new(client_id: String) -> Self {
51 Self {
52 client_id,
53 states: HashMap::new(),
54 clock: IncreasingClock::new(),
55 }
56 }
57
58 pub fn client_id(&self) -> &str {
60 &self.client_id
61 }
62
63 pub fn get_states(&self) -> &HashMap<String, AwarenessState> {
65 &self.states
66 }
67
68 pub fn get_state(&self, client_id: &str) -> Option<&AwarenessState> {
70 self.states.get(client_id)
71 }
72
73 pub fn get_local_state(&self) -> Option<&AwarenessState> {
75 self.get_state(&self.client_id)
76 }
77
78 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 pub fn apply_update(&mut self, update: AwarenessUpdate) {
101 self.clock.update_to_max(update.clock);
103
104 match update.state {
105 Some(state) => {
106 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 self.states.remove(&update.client_id);
129 }
130 }
131 }
132
133 #[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 #[cfg(target_arch = "wasm32")]
157 pub fn remove_stale_clients(&mut self, _timeout_ms: u64) -> Vec<String> {
158 Vec::new()
161 }
162
163 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 pub fn client_count(&self) -> usize {
174 self.states.len()
175 }
176
177 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 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 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 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 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 awareness.set_local_state(json!({}));
271 assert_eq!(awareness.other_client_count(), 0);
272
273 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}