pub mod room;
pub use room::{EmitTarget, RoomBroadcast, RoomCmd, RoomHandle, SessionGuard, StateOp};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct WsHub {
rooms: Arc<Mutex<HashMap<String, Arc<RoomHandle>>>>,
}
impl WsHub {
pub fn new() -> Self {
Self {
rooms: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn join_room(&self, room_key: &str) -> (Arc<RoomHandle>, SessionGuard) {
let mut rooms = self.rooms.lock().unwrap_or_else(|e| e.into_inner());
let room = rooms
.entry(room_key.to_string())
.or_insert_with(RoomHandle::spawn)
.clone();
let guard = room.join_session();
(room, guard)
}
pub fn get_room(&self, room_key: &str) -> Option<Arc<RoomHandle>> {
self.rooms
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(room_key)
.cloned()
}
pub fn list_rooms(&self) -> Vec<String> {
self.rooms
.lock()
.unwrap_or_else(|e| e.into_inner())
.keys()
.cloned()
.collect()
}
pub fn leave_room(&self, room_key: &str, guard: SessionGuard) {
let mut rooms = self.rooms.lock().unwrap_or_else(|e| e.into_inner());
drop(guard);
if rooms.get(room_key).map(|room| room.session_count() == 0).unwrap_or(false) {
if let Some(room) = rooms.remove(room_key) {
room.send_cmd(RoomCmd::Shutdown);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn the_last_session_leaving_disposes_the_room() {
let hub = WsHub::new();
let (_room, a) = hub.join_room("o/p/lobby");
let (_room, b) = hub.join_room("o/p/lobby");
hub.leave_room("o/p/lobby", a);
assert_eq!(hub.list_rooms(), vec!["o/p/lobby".to_string()], "one session is still there");
hub.leave_room("o/p/lobby", b);
assert!(hub.list_rooms().is_empty(), "the last leave disposes the room");
}
#[tokio::test]
async fn a_room_rejoined_after_disposal_starts_empty() {
let hub = WsHub::new();
let (room, a) = hub.join_room("o/p/lobby");
room.send_cmd(RoomCmd::PatchState { op: StateOp::Set, path: "/players/x".into(), value: Some(serde_json::json!({ "n": 1 })) });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert_eq!(room.get_state()["players"]["x"]["n"], 1);
hub.leave_room("o/p/lobby", a);
let (fresh, _b) = hub.join_room("o/p/lobby");
assert_eq!(fresh.get_state(), serde_json::json!({}), "no ghost state survives disposal");
}
}
impl Default for WsHub {
fn default() -> Self {
Self::new()
}
}