1use mdcs_core::lattice::Lattice;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct UserId(pub String);
16
17impl UserId {
18 pub fn new(id: impl Into<String>) -> Self {
19 Self(id.into())
20 }
21}
22
23impl std::fmt::Display for UserId {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(f, "{}", self.0)
26 }
27}
28
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31pub struct Cursor {
32 pub position: usize,
34 pub anchor: Option<usize>,
36}
37
38impl Cursor {
39 pub fn at(position: usize) -> Self {
41 Self {
42 position,
43 anchor: None,
44 }
45 }
46
47 pub fn with_selection(anchor: usize, position: usize) -> Self {
49 Self {
50 position,
51 anchor: Some(anchor),
52 }
53 }
54
55 pub fn has_selection(&self) -> bool {
57 self.anchor.is_some() && self.anchor != Some(self.position)
58 }
59
60 pub fn selection_range(&self) -> Option<(usize, usize)> {
62 self.anchor.map(|anchor| {
63 if anchor < self.position {
64 (anchor, self.position)
65 } else {
66 (self.position, anchor)
67 }
68 })
69 }
70
71 pub fn selection_length(&self) -> usize {
73 self.selection_range()
74 .map(|(start, end)| end - start)
75 .unwrap_or(0)
76 }
77}
78
79#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
81pub enum UserStatus {
82 #[default]
84 Online,
85 Idle,
87 Typing,
89 Away,
91 Offline,
93 Custom(String),
95}
96
97#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
99pub struct UserInfo {
100 pub name: String,
102 pub color: String,
104 pub avatar: Option<String>,
106}
107
108impl UserInfo {
109 pub fn new(name: impl Into<String>, color: impl Into<String>) -> Self {
110 Self {
111 name: name.into(),
112 color: color.into(),
113 avatar: None,
114 }
115 }
116
117 pub fn with_avatar(mut self, avatar: impl Into<String>) -> Self {
118 self.avatar = Some(avatar.into());
119 self
120 }
121}
122
123#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
125pub struct UserPresence {
126 pub user_id: UserId,
128 pub info: UserInfo,
130 pub status: UserStatus,
132 pub cursors: HashMap<String, Cursor>,
134 pub state: HashMap<String, String>,
136 pub last_updated: u64,
138 pub timestamp: u64,
140}
141
142impl UserPresence {
143 pub fn new(user_id: UserId, info: UserInfo) -> Self {
145 Self {
146 user_id,
147 info,
148 status: UserStatus::Online,
149 cursors: HashMap::new(),
150 state: HashMap::new(),
151 last_updated: now_millis(),
152 timestamp: 0,
153 }
154 }
155
156 pub fn set_cursor(&mut self, document_id: impl Into<String>, cursor: Cursor) {
158 self.cursors.insert(document_id.into(), cursor);
159 self.touch();
160 }
161
162 pub fn remove_cursor(&mut self, document_id: &str) {
164 self.cursors.remove(document_id);
165 self.touch();
166 }
167
168 pub fn get_cursor(&self, document_id: &str) -> Option<&Cursor> {
170 self.cursors.get(document_id)
171 }
172
173 pub fn set_status(&mut self, status: UserStatus) {
175 self.status = status;
176 self.touch();
177 }
178
179 pub fn set_state(&mut self, key: impl Into<String>, value: impl Into<String>) {
181 self.state.insert(key.into(), value.into());
182 self.touch();
183 }
184
185 pub fn get_state(&self, key: &str) -> Option<&String> {
187 self.state.get(key)
188 }
189
190 fn touch(&mut self) {
192 self.last_updated = now_millis();
193 self.timestamp += 1;
194 }
195
196 pub fn is_stale(&self, timeout_ms: u64) -> bool {
198 let now = now_millis();
199 now.saturating_sub(self.last_updated) > timeout_ms
200 }
201}
202
203#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
205pub struct PresenceDelta {
206 pub updates: Vec<UserPresence>,
208 pub removals: Vec<UserId>,
210}
211
212impl PresenceDelta {
213 pub fn new() -> Self {
214 Self {
215 updates: Vec::new(),
216 removals: Vec::new(),
217 }
218 }
219
220 pub fn is_empty(&self) -> bool {
221 self.updates.is_empty() && self.removals.is_empty()
222 }
223}
224
225impl Default for PresenceDelta {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231#[derive(Clone, Debug, PartialEq)]
235pub struct PresenceTracker {
236 local_user: UserId,
238 users: HashMap<UserId, UserPresence>,
240 stale_timeout: u64,
242 pending_delta: Option<PresenceDelta>,
244}
245
246impl PresenceTracker {
247 pub fn new(local_user: UserId, info: UserInfo) -> Self {
249 let mut tracker = Self {
250 local_user: local_user.clone(),
251 users: HashMap::new(),
252 stale_timeout: 30_000, pending_delta: None,
254 };
255
256 let presence = UserPresence::new(local_user, info);
258 tracker.users.insert(presence.user_id.clone(), presence);
259
260 tracker
261 }
262
263 pub fn local_user(&self) -> &UserId {
265 &self.local_user
266 }
267
268 pub fn set_stale_timeout(&mut self, timeout_ms: u64) {
270 self.stale_timeout = timeout_ms;
271 }
272
273 pub fn local_presence(&self) -> Option<&UserPresence> {
275 self.users.get(&self.local_user)
276 }
277
278 pub fn set_cursor(&mut self, document_id: impl Into<String>, cursor: Cursor) {
282 let doc_id = document_id.into();
283 let local_user = self.local_user.clone();
284 if let Some(presence) = self.users.get_mut(&local_user) {
285 presence.set_cursor(&doc_id, cursor);
286 let presence_clone = presence.clone();
287 let delta = self.pending_delta.get_or_insert_with(PresenceDelta::new);
288 delta.updates.push(presence_clone);
289 }
290 }
291
292 pub fn remove_cursor(&mut self, document_id: &str) {
294 let local_user = self.local_user.clone();
295 if let Some(presence) = self.users.get_mut(&local_user) {
296 presence.remove_cursor(document_id);
297 let presence_clone = presence.clone();
298 let delta = self.pending_delta.get_or_insert_with(PresenceDelta::new);
299 delta.updates.push(presence_clone);
300 }
301 }
302
303 pub fn set_status(&mut self, status: UserStatus) {
305 let local_user = self.local_user.clone();
306 if let Some(presence) = self.users.get_mut(&local_user) {
307 presence.set_status(status);
308 let presence_clone = presence.clone();
309 let delta = self.pending_delta.get_or_insert_with(PresenceDelta::new);
310 delta.updates.push(presence_clone);
311 }
312 }
313
314 pub fn set_state(&mut self, key: impl Into<String>, value: impl Into<String>) {
316 let local_user = self.local_user.clone();
317 if let Some(presence) = self.users.get_mut(&local_user) {
318 presence.set_state(key, value);
319 let presence_clone = presence.clone();
320 let delta = self.pending_delta.get_or_insert_with(PresenceDelta::new);
321 delta.updates.push(presence_clone);
322 }
323 }
324
325 pub fn heartbeat(&mut self) {
327 let local_user = self.local_user.clone();
328 if let Some(presence) = self.users.get_mut(&local_user) {
329 presence.touch();
330 let presence_clone = presence.clone();
331 let delta = self.pending_delta.get_or_insert_with(PresenceDelta::new);
332 delta.updates.push(presence_clone);
333 }
334 }
335
336 pub fn get_user(&self, user_id: &UserId) -> Option<&UserPresence> {
340 self.users.get(user_id)
341 }
342
343 pub fn all_users(&self) -> impl Iterator<Item = &UserPresence> + '_ {
345 self.users.values()
346 }
347
348 pub fn online_users(&self) -> impl Iterator<Item = &UserPresence> + '_ {
350 self.users
351 .values()
352 .filter(|p| !p.is_stale(self.stale_timeout) && !matches!(p.status, UserStatus::Offline))
353 }
354
355 pub fn users_in_document(&self, document_id: &str) -> Vec<&UserPresence> {
357 self.online_users()
358 .filter(|p| p.cursors.contains_key(document_id))
359 .collect()
360 }
361
362 pub fn cursors_in_document(&self, document_id: &str) -> Vec<(&UserPresence, &Cursor)> {
364 self.online_users()
365 .filter(|p| p.user_id != self.local_user)
366 .filter_map(|p| p.get_cursor(document_id).map(|c| (p, c)))
367 .collect()
368 }
369
370 pub fn online_count(&self) -> usize {
372 self.online_users().count()
373 }
374
375 pub fn take_delta(&mut self) -> Option<PresenceDelta> {
379 self.pending_delta.take()
380 }
381
382 pub fn apply_delta(&mut self, delta: &PresenceDelta) {
384 for presence in &delta.updates {
386 if let Some(existing) = self.users.get(&presence.user_id) {
388 if presence.timestamp <= existing.timestamp {
389 continue;
390 }
391 }
392 self.users
393 .insert(presence.user_id.clone(), presence.clone());
394 }
395
396 for user_id in &delta.removals {
398 if *user_id != self.local_user {
399 self.users.remove(user_id);
400 }
401 }
402 }
403
404 pub fn cleanup_stale(&mut self) -> Vec<UserId> {
406 let stale: Vec<_> = self
407 .users
408 .iter()
409 .filter(|(id, p)| *id != &self.local_user && p.is_stale(self.stale_timeout))
410 .map(|(id, _)| id.clone())
411 .collect();
412
413 for id in &stale {
414 self.users.remove(id);
415 }
416
417 if !stale.is_empty() {
418 let delta = self.pending_delta.get_or_insert_with(PresenceDelta::new);
419 delta.removals.extend(stale.clone());
420 }
421
422 stale
423 }
424
425 pub fn leave(&mut self) {
427 let delta = self.pending_delta.get_or_insert_with(PresenceDelta::new);
428 delta.removals.push(self.local_user.clone());
429 }
430}
431
432impl Lattice for PresenceTracker {
433 fn bottom() -> Self {
434 Self {
435 local_user: UserId::new(""),
436 users: HashMap::new(),
437 stale_timeout: 30_000,
438 pending_delta: None,
439 }
440 }
441
442 fn join(&self, other: &Self) -> Self {
443 let mut result = self.clone();
444
445 for (user_id, other_presence) in &other.users {
446 result
447 .users
448 .entry(user_id.clone())
449 .and_modify(|p| {
450 if other_presence.timestamp > p.timestamp {
451 *p = other_presence.clone();
452 }
453 })
454 .or_insert_with(|| other_presence.clone());
455 }
456
457 result
458 }
459}
460
461fn now_millis() -> u64 {
463 std::time::SystemTime::now()
464 .duration_since(std::time::UNIX_EPOCH)
465 .unwrap_or_default()
466 .as_millis() as u64
467}
468
469pub struct CursorBuilder {
471 document_id: String,
472}
473
474impl CursorBuilder {
475 pub fn for_document(id: impl Into<String>) -> Self {
476 Self {
477 document_id: id.into(),
478 }
479 }
480
481 pub fn at(self, position: usize) -> (String, Cursor) {
482 (self.document_id, Cursor::at(position))
483 }
484
485 pub fn selection(self, anchor: usize, head: usize) -> (String, Cursor) {
486 (self.document_id, Cursor::with_selection(anchor, head))
487 }
488}
489
490pub struct CursorColors;
492
493impl CursorColors {
494 pub const COLORS: [&'static str; 12] = [
495 "#E91E63", "#9C27B0", "#3F51B5", "#2196F3", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FF9800", "#FF5722", "#795548", ];
508
509 pub fn color_for_user(user_id: &UserId) -> &'static str {
511 let hash: usize = user_id.0.bytes().map(|b| b as usize).sum();
512 Self::COLORS[hash % Self::COLORS.len()]
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_cursor_creation() {
522 let cursor = Cursor::at(10);
523 assert_eq!(cursor.position, 10);
524 assert!(!cursor.has_selection());
525
526 let selection = Cursor::with_selection(5, 15);
527 assert!(selection.has_selection());
528 assert_eq!(selection.selection_range(), Some((5, 15)));
529 assert_eq!(selection.selection_length(), 10);
530 }
531
532 #[test]
533 fn test_cursor_selection_backwards() {
534 let selection = Cursor::with_selection(15, 5);
535 assert_eq!(selection.selection_range(), Some((5, 15)));
536 assert_eq!(selection.selection_length(), 10);
537 }
538
539 #[test]
540 fn test_presence_tracker() {
541 let user_id = UserId::new("user1");
542 let info = UserInfo::new("Alice", "#E91E63");
543 let tracker = PresenceTracker::new(user_id.clone(), info);
544
545 assert_eq!(tracker.local_user(), &user_id);
546 assert!(tracker.local_presence().is_some());
547 }
548
549 #[test]
550 fn test_cursor_tracking() {
551 let user_id = UserId::new("user1");
552 let info = UserInfo::new("Alice", "#E91E63");
553 let mut tracker = PresenceTracker::new(user_id, info);
554
555 tracker.set_cursor("doc1", Cursor::at(42));
556
557 let presence = tracker.local_presence().unwrap();
558 let cursor = presence.get_cursor("doc1").unwrap();
559 assert_eq!(cursor.position, 42);
560 }
561
562 #[test]
563 fn test_status_changes() {
564 let user_id = UserId::new("user1");
565 let info = UserInfo::new("Alice", "#E91E63");
566 let mut tracker = PresenceTracker::new(user_id, info);
567
568 tracker.set_status(UserStatus::Typing);
569
570 let presence = tracker.local_presence().unwrap();
571 assert_eq!(presence.status, UserStatus::Typing);
572 }
573
574 #[test]
575 fn test_presence_sync() {
576 let user1 = UserId::new("user1");
577 let user2 = UserId::new("user2");
578
579 let mut tracker1 = PresenceTracker::new(user1.clone(), UserInfo::new("Alice", "#E91E63"));
580 let mut tracker2 = PresenceTracker::new(user2.clone(), UserInfo::new("Bob", "#2196F3"));
581
582 tracker1.set_cursor("doc1", Cursor::at(10));
584
585 let delta = tracker1.take_delta().unwrap();
587 tracker2.apply_delta(&delta);
588
589 let users = tracker2.users_in_document("doc1");
591 assert_eq!(users.len(), 1);
592 assert_eq!(users[0].user_id, user1);
593 }
594
595 #[test]
596 fn test_multiple_users() {
597 let user1 = UserId::new("user1");
598 let info1 = UserInfo::new("Alice", "#E91E63");
599 let mut tracker = PresenceTracker::new(user1.clone(), info1);
600
601 let user2 = UserId::new("user2");
603 let presence2 = UserPresence::new(user2.clone(), UserInfo::new("Bob", "#2196F3"));
604 tracker.users.insert(user2.clone(), presence2);
605
606 let user3 = UserId::new("user3");
607 let presence3 = UserPresence::new(user3.clone(), UserInfo::new("Charlie", "#4CAF50"));
608 tracker.users.insert(user3.clone(), presence3);
609
610 assert_eq!(tracker.online_count(), 3);
611 }
612
613 #[test]
614 fn test_cursors_in_document() {
615 let user1 = UserId::new("user1");
616 let info1 = UserInfo::new("Alice", "#E91E63");
617 let mut tracker = PresenceTracker::new(user1, info1);
618
619 let user2 = UserId::new("user2");
621 let mut presence2 = UserPresence::new(user2.clone(), UserInfo::new("Bob", "#2196F3"));
622 presence2.set_cursor("doc1", Cursor::at(50));
623 tracker.users.insert(user2, presence2);
624
625 let cursors = tracker.cursors_in_document("doc1");
627 assert_eq!(cursors.len(), 1);
628 assert_eq!(cursors[0].1.position, 50);
629 }
630
631 #[test]
632 fn test_color_assignment() {
633 let user1 = UserId::new("alice");
634 let user2 = UserId::new("bob");
635
636 let color1 = CursorColors::color_for_user(&user1);
637 let color2 = CursorColors::color_for_user(&user2);
638
639 assert!(CursorColors::COLORS.contains(&color1));
641 assert!(CursorColors::COLORS.contains(&color2));
642
643 assert_eq!(color1, CursorColors::color_for_user(&user1));
645 }
646
647 #[test]
648 fn test_custom_state() {
649 let user_id = UserId::new("user1");
650 let info = UserInfo::new("Alice", "#E91E63");
651 let mut tracker = PresenceTracker::new(user_id, info);
652
653 tracker.set_state("view", "editor");
654 tracker.set_state("zoom", "100%");
655
656 let presence = tracker.local_presence().unwrap();
657 assert_eq!(presence.get_state("view"), Some(&"editor".to_string()));
658 assert_eq!(presence.get_state("zoom"), Some(&"100%".to_string()));
659 }
660
661 #[test]
662 fn test_cursor_builder() {
663 let (doc, cursor) = CursorBuilder::for_document("doc1").at(42);
664 assert_eq!(doc, "doc1");
665 assert_eq!(cursor.position, 42);
666
667 let (doc, cursor) = CursorBuilder::for_document("doc2").selection(10, 20);
668 assert_eq!(doc, "doc2");
669 assert_eq!(cursor.selection_range(), Some((10, 20)));
670 }
671}