bbx_net/websocket/
room.rs1use 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
15pub struct RoomConfig {
17 pub code_length: usize,
19 pub expiration: Duration,
21 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#[derive(Debug, Clone)]
37pub struct ClientInfo {
38 pub name: Option<String>,
39 pub connected_at: Instant,
40 pub last_activity: Instant,
41}
42
43pub 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 pub fn client_count(&self) -> usize {
61 self.clients.len()
62 }
63}
64
65pub struct RoomManager {
67 rooms: HashMap<String, Room>,
68 config: RoomConfig,
69 rng: XorShiftRng,
70}
71
72impl RoomManager {
73 pub fn new() -> Self {
75 Self::with_config(RoomConfig::default())
76 }
77
78 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 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 pub fn room_exists(&self, code: &str) -> bool {
105 self.rooms.contains_key(code)
106 }
107
108 pub fn client_count(&self, code: &str) -> Option<usize> {
110 self.rooms.get(code).map(|r| r.client_count())
111 }
112
113 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 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 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 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 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 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 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}