1use mdcs_db::presence::{Cursor, PresenceTracker, UserId, UserInfo, UserStatus};
4use parking_lot::RwLock;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::broadcast;
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
12pub struct CursorInfo {
13 pub user_id: String,
14 pub user_name: String,
15 pub document_id: String,
16 pub position: usize,
17 pub selection_start: Option<usize>,
18 pub selection_end: Option<usize>,
19 pub color: String,
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
24pub struct UserPresenceInfo {
25 pub user_id: String,
26 pub name: String,
27 pub status: UserStatus,
28 pub color: String,
29 pub cursors: HashMap<String, CursorInfo>,
30}
31
32#[derive(Clone, Debug)]
34pub enum AwarenessEvent {
35 UserUpdated(UserPresenceInfo),
37 UserOffline(String),
39 CursorMoved(CursorInfo),
41}
42
43pub struct Awareness {
45 local_user_id: String,
46 local_user_name: String,
47 local_color: String,
48 tracker: Arc<RwLock<PresenceTracker>>,
49 event_tx: broadcast::Sender<AwarenessEvent>,
50}
51
52impl Awareness {
53 pub fn new(local_user_id: impl Into<String>, local_user_name: impl Into<String>) -> Self {
58 let local_user_id = local_user_id.into();
59 let local_user_name = local_user_name.into();
60 let user_id = UserId::new(&local_user_id);
61 let info = UserInfo::new(&local_user_name, "#0066cc");
62
63 let (event_tx, _) = broadcast::channel(100);
64
65 Self {
66 local_user_id,
67 local_user_name,
68 local_color: "#0066cc".to_string(),
69 tracker: Arc::new(RwLock::new(PresenceTracker::new(user_id, info))),
70 event_tx,
71 }
72 }
73
74 pub fn local_user_id(&self) -> &str {
76 &self.local_user_id
77 }
78
79 pub fn local_user_name(&self) -> &str {
81 &self.local_user_name
82 }
83
84 pub fn set_cursor(&self, document_id: &str, position: usize) {
88 let cursor = Cursor::at(position);
89 self.tracker.write().set_cursor(document_id, cursor);
90
91 let cursor_info = CursorInfo {
92 user_id: self.local_user_id.clone(),
93 user_name: self.local_user_name.clone(),
94 document_id: document_id.to_string(),
95 position,
96 selection_start: None,
97 selection_end: None,
98 color: self.local_color.clone(),
99 };
100
101 let _ = self.event_tx.send(AwarenessEvent::CursorMoved(cursor_info));
102 }
103
104 pub fn set_selection(&self, document_id: &str, start: usize, end: usize) {
108 let cursor = Cursor::with_selection(start, end);
109 self.tracker.write().set_cursor(document_id, cursor);
110
111 let cursor_info = CursorInfo {
112 user_id: self.local_user_id.clone(),
113 user_name: self.local_user_name.clone(),
114 document_id: document_id.to_string(),
115 position: end,
116 selection_start: Some(start),
117 selection_end: Some(end),
118 color: self.local_color.clone(),
119 };
120
121 let _ = self.event_tx.send(AwarenessEvent::CursorMoved(cursor_info));
122 }
123
124 pub fn set_status(&self, status: UserStatus) {
126 self.tracker.write().set_status(status);
127 }
128
129 pub fn get_users(&self) -> Vec<UserPresenceInfo> {
131 let tracker = self.tracker.read();
132
133 tracker
134 .all_users()
135 .map(|presence| {
136 let cursors: HashMap<String, CursorInfo> = presence
137 .cursors
138 .iter()
139 .map(|(doc_id, cursor): (&String, &Cursor)| {
140 let (sel_start, sel_end) = cursor
141 .selection_range()
142 .map(|(s, e)| (Some(s), Some(e)))
143 .unwrap_or((None, None));
144 (
145 doc_id.clone(),
146 CursorInfo {
147 user_id: presence.user_id.0.clone(),
148 user_name: presence.info.name.clone(),
149 document_id: doc_id.clone(),
150 position: cursor.position,
151 selection_start: sel_start,
152 selection_end: sel_end,
153 color: presence.info.color.clone(),
154 },
155 )
156 })
157 .collect();
158
159 UserPresenceInfo {
160 user_id: presence.user_id.0.clone(),
161 name: presence.info.name.clone(),
162 status: presence.status.clone(),
163 color: presence.info.color.clone(),
164 cursors,
165 }
166 })
167 .collect()
168 }
169
170 pub fn get_cursors(&self, document_id: &str) -> Vec<CursorInfo> {
172 self.get_users()
173 .into_iter()
174 .filter_map(|u| u.cursors.get(document_id).cloned())
175 .collect()
176 }
177
178 pub fn get_local_color(&self) -> &str {
180 &self.local_color
181 }
182
183 pub fn subscribe(&self) -> broadcast::Receiver<AwarenessEvent> {
188 self.event_tx.subscribe()
189 }
190
191 pub fn cleanup_stale(&self) {
193 self.tracker.write().cleanup_stale();
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_awareness_basic() {
203 let awareness = Awareness::new("user-1", "Alice");
204
205 awareness.set_cursor("doc-1", 42);
206 awareness.set_status(UserStatus::Online);
207
208 let users = awareness.get_users();
209 assert_eq!(users.len(), 1);
210 assert_eq!(users[0].user_id, "user-1");
211 }
212
213 #[test]
214 fn test_cursor_tracking() {
215 let awareness = Awareness::new("user-1", "Alice");
216
217 awareness.set_cursor("doc-1", 10);
218 awareness.set_selection("doc-1", 10, 20);
219
220 let cursors = awareness.get_cursors("doc-1");
221 assert_eq!(cursors.len(), 1);
222 }
223}