1use std::sync::Arc;
23
24use dashmap::DashMap;
25use tokio::sync::broadcast;
26
27use crate::codec::Frame;
28use crate::config::WsConfig;
29use crate::error::WsError;
30use crate::stats::StatsRecorder;
31
32pub struct Room {
34 pub id: String,
36 tx: broadcast::Sender<Frame>,
37 participants: DashMap<u32, String>,
39 stats: Option<StatsRecorder>,
40}
41
42impl std::fmt::Debug for Room {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("Room")
45 .field("id", &self.id)
46 .field("participants", &self.participant_count())
47 .field("receiver_count", &self.receiver_count())
48 .finish()
49 }
50}
51
52impl Room {
53 pub fn new(id: impl Into<String>, capacity: usize) -> Self {
55 Self::with_stats(id, capacity, None)
56 }
57
58 pub fn with_stats(
61 id: impl Into<String>,
62 capacity: usize,
63 stats: Option<StatsRecorder>,
64 ) -> Self {
65 let (tx, _) = broadcast::channel(capacity);
66 Self {
67 id: id.into(),
68 tx,
69 participants: DashMap::new(),
70 stats,
71 }
72 }
73
74 pub fn subscribe(&self) -> broadcast::Receiver<Frame> {
76 self.tx.subscribe()
77 }
78
79 pub fn broadcast(&self, msg: impl Into<Frame>) -> Result<usize, WsError> {
90 let frame = msg.into();
91 let n = self
92 .tx
93 .send(frame.clone())
94 .map_err(|_| self.record_drop())?;
95 if let Some(stats) = &self.stats {
96 stats.record_room_message_out(&self.id, n as u64, frame.len() as u64 * n as u64);
97 }
98 Ok(n)
99 }
100
101 pub fn join(&self, participant_id: u32, name: String) {
103 self.participants.insert(participant_id, name);
104 }
105
106 pub fn leave(&self, participant_id: u32) -> bool {
108 self.participants.remove(&participant_id).is_some()
109 }
110
111 pub fn contains(&self, participant_id: u32) -> bool {
113 self.participants.contains_key(&participant_id)
114 }
115
116 pub fn participants(&self) -> Vec<(u32, String)> {
118 self.participants
119 .iter()
120 .map(|r| (*r.key(), r.value().clone()))
121 .collect()
122 }
123
124 pub fn participant_names(&self) -> Vec<String> {
126 self.participants
127 .iter()
128 .map(|r| r.value().clone())
129 .collect()
130 }
131
132 pub fn participant_count(&self) -> usize {
134 self.participants.len()
135 }
136
137 pub fn receiver_count(&self) -> usize {
139 self.tx.receiver_count()
140 }
141
142 pub fn is_empty(&self) -> bool {
144 self.participants.is_empty() && self.tx.receiver_count() == 0
145 }
146
147 pub fn sender(&self) -> &broadcast::Sender<Frame> {
149 &self.tx
150 }
151
152 fn record_drop(&self) -> WsError {
153 if let Some(stats) = &self.stats {
154 stats.record_room_drop(&self.id);
155 }
156 WsError::BroadcastFull
157 }
158}
159
160pub trait RoomRegistry: Send + Sync {
169 fn get_or_create(&self, room_id: &str) -> Arc<Room>;
171
172 fn get(&self, room_id: &str) -> Option<Arc<Room>>;
174
175 fn remove(&self, room_id: &str) -> Option<Arc<Room>>;
177
178 fn room_count(&self) -> usize;
180
181 fn room_ids(&self) -> Vec<String>;
183
184 fn total_participants(&self) -> usize;
186
187 fn cleanup(&self) -> usize;
190
191 fn cleanup_empty(&self) -> usize;
194}
195
196#[derive(Debug)]
198pub struct RoomManager {
199 rooms: DashMap<String, Arc<Room>>,
200 default_capacity: usize,
201 stats: Option<StatsRecorder>,
202}
203
204impl Default for RoomManager {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210impl RoomManager {
211 pub fn new() -> Self {
213 Self {
214 rooms: DashMap::new(),
215 default_capacity: 1024,
216 stats: None,
217 }
218 }
219
220 pub fn with_config(config: &WsConfig) -> Self {
222 Self {
223 rooms: DashMap::new(),
224 default_capacity: config.broadcast_capacity,
225 stats: None,
226 }
227 }
228
229 pub fn with_stats(config: &WsConfig, stats: StatsRecorder) -> Self {
233 Self {
234 rooms: DashMap::new(),
235 default_capacity: config.broadcast_capacity,
236 stats: Some(stats),
237 }
238 }
239
240 pub fn stats(&self) -> Option<&StatsRecorder> {
242 self.stats.as_ref()
243 }
244}
245
246impl RoomRegistry for RoomManager {
247 fn get_or_create(&self, room_id: &str) -> Arc<Room> {
250 RoomManager::get_or_create(self, room_id)
251 }
252
253 fn get(&self, room_id: &str) -> Option<Arc<Room>> {
254 RoomManager::get(self, room_id)
255 }
256
257 fn remove(&self, room_id: &str) -> Option<Arc<Room>> {
258 RoomManager::remove(self, room_id)
259 }
260
261 fn room_count(&self) -> usize {
262 RoomManager::room_count(self)
263 }
264
265 fn room_ids(&self) -> Vec<String> {
266 RoomManager::room_ids(self)
267 }
268
269 fn total_participants(&self) -> usize {
270 RoomManager::total_participants(self)
271 }
272
273 fn cleanup(&self) -> usize {
274 RoomManager::cleanup(self)
275 }
276
277 fn cleanup_empty(&self) -> usize {
278 RoomManager::cleanup_empty(self)
279 }
280}
281
282impl RoomManager {
283 pub fn get_or_create(&self, room_id: &str) -> Arc<Room> {
285 self.get_or_create_with_capacity(room_id, self.default_capacity)
286 }
287
288 pub fn get(&self, room_id: &str) -> Option<Arc<Room>> {
290 self.rooms.get(room_id).map(|r| Arc::clone(&r))
291 }
292
293 pub fn remove(&self, room_id: &str) -> Option<Arc<Room>> {
295 let removed = self.rooms.remove(room_id).map(|(_, v)| v);
296 if removed.is_some() {
297 if let Some(stats) = &self.stats {
298 stats.room_closed();
299 }
300 }
301 removed
302 }
303
304 pub fn room_count(&self) -> usize {
306 self.rooms.len()
307 }
308
309 pub fn total_participants(&self) -> usize {
311 self.rooms
312 .iter()
313 .map(|e| e.value().participant_count())
314 .sum()
315 }
316
317 pub fn cleanup(&self) -> usize {
319 let mut to_remove = Vec::new();
320 for entry in self.rooms.iter() {
321 if entry.value().is_empty() {
322 to_remove.push(entry.key().clone());
323 }
324 }
325 let n = to_remove.len();
326 for k in to_remove {
327 self.rooms.remove(&k);
328 }
329 if n > 0 {
330 if let Some(stats) = &self.stats {
331 for _ in 0..n {
332 stats.room_closed();
333 }
334 }
335 }
336 n
337 }
338
339 pub fn cleanup_empty(&self) -> usize {
342 let to_remove: Vec<String> = self
343 .rooms
344 .iter()
345 .filter(|e| e.value().participant_count() == 0)
346 .map(|e| e.key().clone())
347 .collect();
348 let n = to_remove.len();
349 for k in to_remove {
350 self.rooms.remove(&k);
351 }
352 if n > 0 {
353 if let Some(stats) = &self.stats {
354 for _ in 0..n {
355 stats.room_closed();
356 }
357 }
358 }
359 n
360 }
361
362 pub fn room_ids(&self) -> Vec<String> {
364 self.rooms.iter().map(|e| e.key().clone()).collect()
365 }
366
367 pub fn get_or_create_with_capacity(&self, room_id: &str, capacity: usize) -> Arc<Room> {
369 if let Some(r) = self.rooms.get(room_id) {
370 return Arc::clone(&r);
371 }
372 let room = Arc::new(Room::with_stats(room_id, capacity, self.stats.clone()));
373 if let Some(stats) = &self.stats {
374 stats.room_opened();
375 }
376 let entry = self
381 .rooms
382 .entry(room_id.to_string())
383 .or_insert_with(|| Arc::clone(&room));
384 Arc::clone(entry.value())
385 }
386}
387
388#[allow(
392 clippy::unwrap_used,
393 clippy::expect_used,
394 clippy::indexing_slicing,
395 clippy::panic
396)]
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use bytes::Bytes;
401
402 fn name(s: &str) -> String {
403 s.to_string()
404 }
405
406 #[test]
407 fn room_join_leave() {
408 let room = Room::new("lobby", 16);
409 assert_eq!(room.participant_count(), 0);
410 room.join(1, name("alice"));
411 room.join(2, name("bob"));
412 assert_eq!(room.participant_count(), 2);
413 assert!(room.contains(1));
414 assert!(room.leave(1));
415 assert!(!room.contains(1));
416 assert_eq!(room.participant_count(), 1);
417 }
418
419 #[test]
420 fn room_rejoin_updates_name() {
421 let room = Room::new("lobby", 16);
422 room.join(1, name("alice"));
423 room.join(1, name("alice2"));
424 assert_eq!(room.participant_count(), 1);
425 assert_eq!(room.participant_names(), vec!["alice2".to_string()]);
426 }
427
428 #[test]
429 fn room_participant_snapshots() {
430 let room = Room::new("lobby", 16);
431 room.join(7, name("alice"));
432 room.join(3, name("bob"));
433 let mut pairs = room.participants();
434 pairs.sort();
435 assert_eq!(
436 pairs,
437 vec![(3, "bob".to_string()), (7, "alice".to_string())]
438 );
439 let mut names = room.participant_names();
440 names.sort();
441 assert_eq!(names, vec!["alice".to_string(), "bob".to_string()]);
442 assert_eq!(room.participant_count(), 2);
443 }
444
445 #[tokio::test]
446 async fn room_broadcast() {
447 let room = Room::new("chat", 16);
448 let mut rx = room.subscribe();
449 let n = room.broadcast("hello".to_string()).unwrap();
450 assert_eq!(n, 1);
451 assert_eq!(rx.recv().await.unwrap(), Frame::from("hello"));
452 }
453
454 #[tokio::test]
455 async fn room_broadcast_binary() {
456 let room = Room::new("binroom", 16);
457 let mut rx = room.subscribe();
458 let payload = vec![0xde_u8, 0xad, 0xbe, 0xef];
459 let n = room.broadcast(Frame::from(payload.clone())).unwrap();
460 assert_eq!(n, 1);
461 let got = rx.recv().await.unwrap();
462 assert!(got.is_binary());
463 assert_eq!(got.as_bytes(), &payload[..]);
464 }
465
466 #[tokio::test]
467 async fn room_broadcast_bytes_vec_convenience() {
468 let room = Room::new("binroom", 16);
470 let mut rx = room.subscribe();
471 room.broadcast(vec![1u8, 2, 3]).unwrap();
472 let got = rx.recv().await.unwrap();
473 assert!(got.is_binary());
474 assert_eq!(got.as_bytes(), &[1, 2, 3]);
475 }
476
477 #[tokio::test]
478 async fn room_text_and_binary_share_one_channel_without_cross_talk() {
479 let room = Room::new("mixed", 16);
480 let mut rx = room.subscribe();
481 room.broadcast("text".to_string()).unwrap();
482 room.broadcast(Bytes::from_static(b"\x00\x01")).unwrap();
483 assert!(rx.recv().await.unwrap().is_text());
484 assert!(rx.recv().await.unwrap().is_binary());
485 }
486
487 #[tokio::test]
488 async fn room_broadcast_no_receiver() {
489 let room = Room::new("empty", 16);
490 let err = room.broadcast("hi".to_string()).unwrap_err();
491 assert_eq!(err, WsError::BroadcastFull);
492 }
493
494 #[tokio::test]
495 async fn room_stats_records_outbound_and_drops() {
496 let stats = StatsRecorder::new();
497 let room = Room::with_stats("metrics", 16, Some(stats.clone()));
498 let mut rx = room.subscribe();
499 room.broadcast("12345".to_string()).unwrap(); rx.recv().await.unwrap();
501
502 let s = stats.snapshot();
503 let room_stats = s.per_room.get("metrics").unwrap();
504 assert_eq!(room_stats.messages_out, 1);
505 assert_eq!(room_stats.bytes_out, 5);
506 assert_eq!(s.messages_out, 1);
507
508 drop(rx);
509 room.broadcast("nobody".to_string()).unwrap_err();
510 assert_eq!(stats.snapshot().per_room.get("metrics").unwrap().drops, 1);
511 assert_eq!(stats.snapshot().drops, 1);
512 }
513
514 #[test]
515 fn manager_get_or_create_idempotent() {
516 let m = RoomManager::new();
517 let r1 = m.get_or_create("room1");
518 let r2 = m.get_or_create("room1");
519 assert!(Arc::ptr_eq(&r1, &r2));
520 assert_eq!(m.room_count(), 1);
521 }
522
523 #[test]
524 fn manager_with_config_capacity() {
525 let cfg = WsConfig::builder().broadcast_capacity(2048).build();
526 let m = RoomManager::with_config(&cfg);
527 let r = m.get_or_create("room1");
528 let r2 = m.get_or_create("room1");
530 assert!(Arc::ptr_eq(&r, &r2));
531 assert_eq!(m.room_count(), 1);
532 }
533
534 #[test]
535 fn manager_cleanup_removes_empty() {
536 let m = RoomManager::new();
537 let _r = m.get_or_create("a");
538 assert_eq!(m.cleanup(), 1);
540 assert_eq!(m.room_count(), 0);
541
542 let r = m.get_or_create("b");
543 r.join(1, name("user1"));
544 assert_eq!(m.cleanup(), 0);
545 assert_eq!(m.room_count(), 1);
546 }
547
548 #[test]
549 fn manager_cleanup_empty_removes_zero_participant_rooms() {
550 let m = RoomManager::new();
551 let _a = m.get_or_create("a");
552 assert_eq!(m.cleanup_empty(), 1);
554 assert_eq!(m.room_count(), 0);
555
556 let a = m.get_or_create("a");
558 a.join(1, name("alice"));
559 assert_eq!(m.cleanup_empty(), 0);
560 assert_eq!(m.room_count(), 1);
561 let kept = m.get_or_create("a");
562 assert!(Arc::ptr_eq(&a, &kept));
563
564 a.leave(1);
565 assert_eq!(m.cleanup_empty(), 1);
566 assert_eq!(m.room_count(), 0);
567 }
568
569 #[test]
570 fn manager_total_participants() {
571 let m = RoomManager::new();
572 let r1 = m.get_or_create("r1");
573 let r2 = m.get_or_create("r2");
574 assert_eq!(m.total_participants(), 0);
575 r1.join(1, name("alice"));
576 r1.join(2, name("bob"));
577 r2.join(3, name("carol"));
578 assert_eq!(m.total_participants(), 3);
579 r2.leave(3);
580 assert_eq!(m.total_participants(), 2);
581 }
582
583 #[test]
584 fn manager_get_and_remove() {
585 let m = RoomManager::new();
586 assert!(m.get("nonexistent").is_none());
587 let r = m.get_or_create("x");
588 assert!(m.get("x").is_some());
589 let removed = m.remove("x").unwrap();
590 assert!(Arc::ptr_eq(&r, &removed));
591 assert!(m.get("x").is_none());
592 }
593
594 #[test]
595 fn manager_stats_room_lifecycle() {
596 let stats = StatsRecorder::new();
597 let cfg = WsConfig::default();
598 let m = RoomManager::with_stats(&cfg, stats.clone());
599 let _r = m.get_or_create("a");
600 assert_eq!(stats.snapshot().rooms, 1);
601 m.get_or_create("a"); assert_eq!(stats.snapshot().rooms, 1);
603 m.remove("a");
604 assert_eq!(stats.snapshot().rooms, 0);
605 m.get_or_create("b");
606 m.get_or_create("c");
607 m.cleanup();
608 assert_eq!(stats.snapshot().rooms, 0);
609 }
610
611 #[test]
612 fn trait_object_room_registry() {
613 let manager = RoomManager::new();
616 let registry: &dyn RoomRegistry = &manager;
617 let r = registry.get_or_create("dyn_room");
618 r.join(1, name("alice"));
619 assert_eq!(registry.room_count(), 1);
620 assert_eq!(registry.total_participants(), 1);
621 assert_eq!(registry.room_ids(), vec!["dyn_room".to_string()]);
622 assert!(registry.get("dyn_room").is_some());
623 assert!(registry.remove("dyn_room").is_some());
624 assert_eq!(registry.cleanup(), 0);
625 assert_eq!(registry.cleanup_empty(), 0);
626 }
627
628 #[tokio::test]
629 async fn manager_broadcast_isolation() {
630 let m = RoomManager::new();
631 let r1 = m.get_or_create("r1");
632 let r2 = m.get_or_create("r2");
633 let mut rx1 = r1.subscribe();
634 let mut rx2 = r2.subscribe();
635 r1.broadcast("for r1".to_string()).unwrap();
636 assert_eq!(rx1.recv().await.unwrap(), Frame::from("for r1"));
637 use tokio::time::{timeout, Duration};
639 let res = timeout(Duration::from_millis(10), rx2.recv()).await;
640 assert!(res.is_err());
641 }
642}