1use std::sync::Arc;
8
9use dashmap::DashMap;
10use tokio::sync::broadcast;
11
12use crate::config::WsConfig;
13use crate::error::WsError;
14
15pub struct Room {
17 pub id: String,
19 tx: broadcast::Sender<String>,
20 participants: DashMap<u32, String>,
22}
23
24impl std::fmt::Debug for Room {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("Room")
27 .field("id", &self.id)
28 .field("participants", &self.participant_count())
29 .field("receiver_count", &self.receiver_count())
30 .finish()
31 }
32}
33
34impl Room {
35 pub fn new(id: impl Into<String>, capacity: usize) -> Self {
37 let (tx, _) = broadcast::channel(capacity);
38 Self {
39 id: id.into(),
40 tx,
41 participants: DashMap::new(),
42 }
43 }
44
45 pub fn subscribe(&self) -> broadcast::Receiver<String> {
47 self.tx.subscribe()
48 }
49
50 pub fn broadcast(&self, msg: String) -> Result<usize, WsError> {
52 self.tx.send(msg).map_err(|_| WsError::BroadcastFull)
53 }
54
55 pub fn join(&self, participant_id: u32, name: String) {
57 self.participants.insert(participant_id, name);
58 }
59
60 pub fn leave(&self, participant_id: u32) -> bool {
62 self.participants.remove(&participant_id).is_some()
63 }
64
65 pub fn contains(&self, participant_id: u32) -> bool {
67 self.participants.contains_key(&participant_id)
68 }
69
70 pub fn participants(&self) -> Vec<(u32, String)> {
72 self.participants
73 .iter()
74 .map(|r| (*r.key(), r.value().clone()))
75 .collect()
76 }
77
78 pub fn participant_names(&self) -> Vec<String> {
80 self.participants
81 .iter()
82 .map(|r| r.value().clone())
83 .collect()
84 }
85
86 pub fn participant_count(&self) -> usize {
88 self.participants.len()
89 }
90
91 pub fn receiver_count(&self) -> usize {
93 self.tx.receiver_count()
94 }
95
96 pub fn is_empty(&self) -> bool {
98 self.participants.is_empty() && self.tx.receiver_count() == 0
99 }
100
101 pub fn sender(&self) -> &broadcast::Sender<String> {
103 &self.tx
104 }
105}
106
107#[derive(Debug)]
109pub struct RoomManager {
110 rooms: DashMap<String, Arc<Room>>,
111 default_capacity: usize,
112}
113
114impl Default for RoomManager {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120impl RoomManager {
121 pub fn new() -> Self {
123 Self {
124 rooms: DashMap::new(),
125 default_capacity: 1024,
126 }
127 }
128
129 pub fn with_config(config: &WsConfig) -> Self {
131 Self {
132 rooms: DashMap::new(),
133 default_capacity: config.broadcast_capacity,
134 }
135 }
136
137 pub fn get_or_create(&self, room_id: &str) -> Arc<Room> {
139 self.get_or_create_with_capacity(room_id, self.default_capacity)
140 }
141
142 pub fn get_or_create_with_capacity(&self, room_id: &str, capacity: usize) -> Arc<Room> {
144 if let Some(r) = self.rooms.get(room_id) {
145 return Arc::clone(&r);
146 }
147 let room = Arc::new(Room::new(room_id, capacity));
148 let entry = self
150 .rooms
151 .entry(room_id.to_string())
152 .or_insert_with(|| Arc::clone(&room));
153 Arc::clone(entry.value())
154 }
155
156 pub fn get(&self, room_id: &str) -> Option<Arc<Room>> {
158 self.rooms.get(room_id).map(|r| Arc::clone(&r))
159 }
160
161 pub fn remove(&self, room_id: &str) -> Option<Arc<Room>> {
163 self.rooms.remove(room_id).map(|(_, v)| v)
164 }
165
166 pub fn room_count(&self) -> usize {
168 self.rooms.len()
169 }
170
171 pub fn total_participants(&self) -> usize {
173 self.rooms
174 .iter()
175 .map(|e| e.value().participant_count())
176 .sum()
177 }
178
179 pub fn cleanup(&self) -> usize {
181 let mut to_remove = Vec::new();
182 for entry in self.rooms.iter() {
183 if entry.value().is_empty() {
184 to_remove.push(entry.key().clone());
185 }
186 }
187 let n = to_remove.len();
188 for k in to_remove {
189 self.rooms.remove(&k);
190 }
191 n
192 }
193
194 pub fn cleanup_empty(&self) -> usize {
197 let to_remove: Vec<String> = self
198 .rooms
199 .iter()
200 .filter(|e| e.value().participant_count() == 0)
201 .map(|e| e.key().clone())
202 .collect();
203 let n = to_remove.len();
204 for k in to_remove {
205 self.rooms.remove(&k);
206 }
207 n
208 }
209
210 pub fn room_ids(&self) -> Vec<String> {
212 self.rooms.iter().map(|e| e.key().clone()).collect()
213 }
214}
215
216#[allow(
220 clippy::unwrap_used,
221 clippy::expect_used,
222 clippy::indexing_slicing,
223 clippy::panic
224)]
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 fn name(s: &str) -> String {
230 s.to_string()
231 }
232
233 #[test]
234 fn room_join_leave() {
235 let room = Room::new("lobby", 16);
236 assert_eq!(room.participant_count(), 0);
237 room.join(1, name("alice"));
238 room.join(2, name("bob"));
239 assert_eq!(room.participant_count(), 2);
240 assert!(room.contains(1));
241 assert!(room.leave(1));
242 assert!(!room.contains(1));
243 assert_eq!(room.participant_count(), 1);
244 }
245
246 #[test]
247 fn room_rejoin_updates_name() {
248 let room = Room::new("lobby", 16);
249 room.join(1, name("alice"));
250 room.join(1, name("alice2"));
251 assert_eq!(room.participant_count(), 1);
252 assert_eq!(room.participant_names(), vec!["alice2".to_string()]);
253 }
254
255 #[test]
256 fn room_participant_snapshots() {
257 let room = Room::new("lobby", 16);
258 room.join(7, name("alice"));
259 room.join(3, name("bob"));
260 let mut pairs = room.participants();
261 pairs.sort();
262 assert_eq!(
263 pairs,
264 vec![(3, "bob".to_string()), (7, "alice".to_string())]
265 );
266 let mut names = room.participant_names();
267 names.sort();
268 assert_eq!(names, vec!["alice".to_string(), "bob".to_string()]);
269 assert_eq!(room.participant_count(), 2);
270 }
271
272 #[tokio::test]
273 async fn room_broadcast() {
274 let room = Room::new("chat", 16);
275 let mut rx = room.subscribe();
276 let n = room.broadcast("hello".to_string()).unwrap();
277 assert_eq!(n, 1);
278 assert_eq!(rx.recv().await.unwrap(), "hello");
279 }
280
281 #[tokio::test]
282 async fn room_broadcast_no_receiver() {
283 let room = Room::new("empty", 16);
284 let err = room.broadcast("hi".to_string()).unwrap_err();
285 assert_eq!(err, WsError::BroadcastFull);
286 }
287
288 #[test]
289 fn manager_get_or_create_idempotent() {
290 let m = RoomManager::new();
291 let r1 = m.get_or_create("room1");
292 let r2 = m.get_or_create("room1");
293 assert!(Arc::ptr_eq(&r1, &r2));
294 assert_eq!(m.room_count(), 1);
295 }
296
297 #[test]
298 fn manager_with_config_capacity() {
299 let cfg = WsConfig::builder().broadcast_capacity(2048).build();
300 let m = RoomManager::with_config(&cfg);
301 let r = m.get_or_create("room1");
302 let r2 = m.get_or_create("room1");
304 assert!(Arc::ptr_eq(&r, &r2));
305 assert_eq!(m.room_count(), 1);
306 }
307
308 #[test]
309 fn manager_cleanup_removes_empty() {
310 let m = RoomManager::new();
311 let _r = m.get_or_create("a");
312 assert_eq!(m.cleanup(), 1);
314 assert_eq!(m.room_count(), 0);
315
316 let r = m.get_or_create("b");
317 r.join(1, name("user1"));
318 assert_eq!(m.cleanup(), 0);
319 assert_eq!(m.room_count(), 1);
320 }
321
322 #[test]
323 fn manager_cleanup_empty_removes_zero_participant_rooms() {
324 let m = RoomManager::new();
325 let _a = m.get_or_create("a");
326 assert_eq!(m.cleanup_empty(), 1);
328 assert_eq!(m.room_count(), 0);
329
330 let a = m.get_or_create("a");
332 a.join(1, name("alice"));
333 assert_eq!(m.cleanup_empty(), 0);
334 assert_eq!(m.room_count(), 1);
335 let kept = m.get_or_create("a");
336 assert!(Arc::ptr_eq(&a, &kept));
337
338 a.leave(1);
339 assert_eq!(m.cleanup_empty(), 1);
340 assert_eq!(m.room_count(), 0);
341 }
342
343 #[test]
344 fn manager_total_participants() {
345 let m = RoomManager::new();
346 let r1 = m.get_or_create("r1");
347 let r2 = m.get_or_create("r2");
348 assert_eq!(m.total_participants(), 0);
349 r1.join(1, name("alice"));
350 r1.join(2, name("bob"));
351 r2.join(3, name("carol"));
352 assert_eq!(m.total_participants(), 3);
353 r2.leave(3);
354 assert_eq!(m.total_participants(), 2);
355 }
356
357 #[test]
358 fn manager_get_and_remove() {
359 let m = RoomManager::new();
360 assert!(m.get("nonexistent").is_none());
361 let r = m.get_or_create("x");
362 assert!(m.get("x").is_some());
363 let removed = m.remove("x").unwrap();
364 assert!(Arc::ptr_eq(&r, &removed));
365 assert!(m.get("x").is_none());
366 }
367
368 #[tokio::test]
369 async fn manager_broadcast_isolation() {
370 let m = RoomManager::new();
371 let r1 = m.get_or_create("r1");
372 let r2 = m.get_or_create("r2");
373 let mut rx1 = r1.subscribe();
374 let mut rx2 = r2.subscribe();
375 r1.broadcast("for r1".to_string()).unwrap();
376 assert_eq!(rx1.recv().await.unwrap(), "for r1");
377 use tokio::time::{timeout, Duration};
379 let res = timeout(Duration::from_millis(10), rx2.recv()).await;
380 assert!(res.is_err());
381 }
382}