bbx_net/websocket/
room.rs

1//! Room management for WebSocket connections.
2
3use std::{
4    collections::HashMap,
5    time::{Duration, Instant},
6};
7
8use bbx_core::random::XorShiftRng;
9
10use crate::{
11    address::NodeId,
12    error::{NetError, Result},
13};
14
15/// Room configuration.
16pub struct RoomConfig {
17    /// Number of digits in room code (4-6).
18    pub code_length: usize,
19    /// Room expiration time.
20    pub expiration: Duration,
21    /// Maximum clients per room.
22    pub max_clients: usize,
23}
24
25impl Default for RoomConfig {
26    fn default() -> Self {
27        Self {
28            code_length: 6,
29            expiration: Duration::from_secs(3600 * 24),
30            max_clients: 100,
31        }
32    }
33}
34
35/// Information about a connected client.
36#[derive(Debug, Clone)]
37pub struct ClientInfo {
38    pub name: Option<String>,
39    pub connected_at: Instant,
40    pub last_activity: Instant,
41}
42
43/// A room that clients can join.
44pub struct Room {
45    pub code: String,
46    pub created_at: Instant,
47    pub clients: HashMap<NodeId, ClientInfo>,
48}
49
50impl Room {
51    fn new(code: String) -> Self {
52        Self {
53            code,
54            created_at: Instant::now(),
55            clients: HashMap::new(),
56        }
57    }
58
59    /// Get the number of connected clients.
60    pub fn client_count(&self) -> usize {
61        self.clients.len()
62    }
63}
64
65/// Manages active rooms and room codes.
66pub struct RoomManager {
67    rooms: HashMap<String, Room>,
68    config: RoomConfig,
69    rng: XorShiftRng,
70}
71
72impl RoomManager {
73    /// Create a new room manager with default configuration.
74    pub fn new() -> Self {
75        Self::with_config(RoomConfig::default())
76    }
77
78    /// Create a new room manager with custom configuration.
79    pub fn with_config(config: RoomConfig) -> Self {
80        let seed = std::time::SystemTime::now()
81            .duration_since(std::time::UNIX_EPOCH)
82            .map(|d| d.as_nanos() as u64)
83            .unwrap_or(1);
84
85        Self {
86            rooms: HashMap::new(),
87            config,
88            rng: XorShiftRng::new(seed),
89        }
90    }
91
92    /// Generate a new room code and create the room.
93    pub fn create_room(&mut self) -> String {
94        loop {
95            let code = self.generate_code();
96            if !self.rooms.contains_key(&code) {
97                self.rooms.insert(code.clone(), Room::new(code.clone()));
98                return code;
99            }
100        }
101    }
102
103    /// Check if a room code is valid.
104    pub fn room_exists(&self, code: &str) -> bool {
105        self.rooms.contains_key(code)
106    }
107
108    /// Get the number of clients in a room.
109    pub fn client_count(&self, code: &str) -> Option<usize> {
110        self.rooms.get(code).map(|r| r.client_count())
111    }
112
113    /// Validate a room code and add client.
114    pub fn join_room(&mut self, code: &str, node_id: NodeId, client_name: Option<String>) -> Result<()> {
115        let room = self.rooms.get_mut(code).ok_or(NetError::InvalidRoomCode)?;
116
117        if room.clients.len() >= self.config.max_clients {
118            return Err(NetError::RoomFull);
119        }
120
121        let now = Instant::now();
122        room.clients.insert(
123            node_id,
124            ClientInfo {
125                name: client_name,
126                connected_at: now,
127                last_activity: now,
128            },
129        );
130
131        Ok(())
132    }
133
134    /// Remove a client from their room.
135    pub fn leave_room(&mut self, code: &str, node_id: NodeId) -> bool {
136        if let Some(room) = self.rooms.get_mut(code) {
137            room.clients.remove(&node_id).is_some()
138        } else {
139            false
140        }
141    }
142
143    /// Update client activity timestamp.
144    pub fn update_activity(&mut self, code: &str, node_id: NodeId) {
145        if let Some(room) = self.rooms.get_mut(code)
146            && let Some(client) = room.clients.get_mut(&node_id)
147        {
148            client.last_activity = Instant::now();
149        }
150    }
151
152    /// Get all node IDs in a room.
153    pub fn get_room_clients(&self, code: &str) -> Vec<NodeId> {
154        self.rooms
155            .get(code)
156            .map(|r| r.clients.keys().copied().collect())
157            .unwrap_or_default()
158    }
159
160    /// Close a room and return all client node IDs.
161    pub fn close_room(&mut self, code: &str) -> Vec<NodeId> {
162        self.rooms
163            .remove(code)
164            .map(|r| r.clients.keys().copied().collect())
165            .unwrap_or_default()
166    }
167
168    /// Clean up expired rooms.
169    ///
170    /// Returns the number of rooms removed.
171    pub fn cleanup_expired(&mut self) -> usize {
172        let now = Instant::now();
173        let expiration = self.config.expiration;
174        let before = self.rooms.len();
175
176        self.rooms
177            .retain(|_, room| now.duration_since(room.created_at) < expiration);
178
179        before - self.rooms.len()
180    }
181
182    /// Get the total number of active rooms.
183    pub fn room_count(&self) -> usize {
184        self.rooms.len()
185    }
186
187    fn generate_code(&mut self) -> String {
188        let mut code = String::with_capacity(self.config.code_length);
189        for _ in 0..self.config.code_length {
190            let sample = (self.rng.next_noise_sample() + 1.0) / 2.0;
191            let digit = (sample * 10.0).min(9.0) as u8;
192            code.push((b'0' + digit) as char);
193        }
194        code
195    }
196}
197
198impl Default for RoomManager {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_create_room() {
210        let mut manager = RoomManager::new();
211        let code = manager.create_room();
212
213        assert_eq!(code.len(), 6);
214        assert!(manager.room_exists(&code));
215    }
216
217    #[test]
218    fn test_room_code_format() {
219        let mut manager = RoomManager::new();
220        let code = manager.create_room();
221
222        for c in code.chars() {
223            assert!(c.is_ascii_digit());
224        }
225    }
226
227    #[test]
228    fn test_join_room() {
229        let mut manager = RoomManager::new();
230        let code = manager.create_room();
231
232        let node_id = NodeId::from_parts(1, 2);
233        assert!(manager.join_room(&code, node_id, None).is_ok());
234        assert_eq!(manager.client_count(&code), Some(1));
235    }
236
237    #[test]
238    fn test_join_invalid_room() {
239        let mut manager = RoomManager::new();
240        let node_id = NodeId::from_parts(1, 2);
241
242        let result = manager.join_room("000000", node_id, None);
243        assert_eq!(result, Err(NetError::InvalidRoomCode));
244    }
245
246    #[test]
247    fn test_room_capacity() {
248        let config = RoomConfig {
249            max_clients: 2,
250            ..Default::default()
251        };
252        let mut manager = RoomManager::with_config(config);
253        let code = manager.create_room();
254
255        let id1 = NodeId::from_parts(1, 1);
256        let id2 = NodeId::from_parts(2, 2);
257        let id3 = NodeId::from_parts(3, 3);
258
259        assert!(manager.join_room(&code, id1, None).is_ok());
260        assert!(manager.join_room(&code, id2, None).is_ok());
261        assert_eq!(manager.join_room(&code, id3, None), Err(NetError::RoomFull));
262    }
263
264    #[test]
265    fn test_leave_room() {
266        let mut manager = RoomManager::new();
267        let code = manager.create_room();
268
269        let node_id = NodeId::from_parts(5, 6);
270        manager.join_room(&code, node_id, None).unwrap();
271
272        assert!(manager.leave_room(&code, node_id));
273        assert_eq!(manager.client_count(&code), Some(0));
274    }
275
276    #[test]
277    fn test_get_room_clients() {
278        let mut manager = RoomManager::new();
279        let code = manager.create_room();
280
281        let id1 = NodeId::from_parts(1, 1);
282        let id2 = NodeId::from_parts(2, 2);
283
284        manager.join_room(&code, id1, None).unwrap();
285        manager.join_room(&code, id2, None).unwrap();
286
287        let clients = manager.get_room_clients(&code);
288        assert_eq!(clients.len(), 2);
289        assert!(clients.contains(&id1));
290        assert!(clients.contains(&id2));
291    }
292
293    #[test]
294    fn test_close_room() {
295        let mut manager = RoomManager::new();
296        let code = manager.create_room();
297
298        let node_id = NodeId::from_parts(7, 8);
299        manager.join_room(&code, node_id, None).unwrap();
300
301        let clients = manager.close_room(&code);
302        assert_eq!(clients.len(), 1);
303        assert!(!manager.room_exists(&code));
304    }
305
306    #[test]
307    fn test_unique_codes() {
308        let mut manager = RoomManager::new();
309        let mut codes = Vec::new();
310
311        for _ in 0..100 {
312            codes.push(manager.create_room());
313        }
314
315        let unique: std::collections::HashSet<_> = codes.iter().collect();
316        assert_eq!(unique.len(), 100);
317    }
318}