"""Real-time pushes for lobbies over WebSockets. The sockets only carry short "something changed" notes ({"t": "messages"} or {"t": "lobby"}). The browser then loads the data through the normal API, so all visibility rules (whispers, hidden rolls, who sees which character) stay in one place. Without a working socket the client keeps polling, so nothing breaks when a proxy blocks WebSockets.""" import asyncio import threading MAX_PER_USER = 6 # open sockets per user (several tabs) MAX_PER_LOBBY = 80 class Hub: def __init__(self): self.rooms: dict = {} # lobby id -> {websocket: user id} self.loop = None self.lock = threading.Lock() def attach(self, loop): self.loop = loop def add(self, lid: str, ws, uid: int) -> bool: with self.lock: room = self.rooms.setdefault(lid, {}) if len(room) >= MAX_PER_LOBBY or sum(1 for u in room.values() if u == uid) >= MAX_PER_USER: return False room[ws] = uid return True def remove(self, lid: str, ws): with self.lock: room = self.rooms.get(lid) if room: room.pop(ws, None) if not room: del self.rooms[lid] def online(self, lid: str) -> set: with self.lock: return set(self.rooms.get(lid, {}).values()) async def _send(self, lid: str, note: dict, users): with self.lock: targets = [ws for ws, uid in self.rooms.get(lid, {}).items() if users is None or uid in users] for ws in targets: try: await ws.send_json(note) except Exception: self.remove(lid, ws) def notify(self, lid: str, kind: str, users=None, delay: float = 0.08, **extra): """Callable from normal (threadpool) endpoints. The short delay lets the database transaction finish first.""" if not self.loop or lid not in self.rooms: return async def go(): await asyncio.sleep(delay) await self._send(lid, {"t": kind, **extra}, users) asyncio.run_coroutine_threadsafe(go(), self.loop) def close_user(self, lid: str, uid: int): """Drops the sockets of someone who left or was removed.""" if not self.loop: return with self.lock: targets = [ws for ws, u in self.rooms.get(lid, {}).items() if u == uid] for ws in targets: asyncio.run_coroutine_threadsafe(ws.close(code=4403), self.loop) def close_lobby(self, lid: str): if not self.loop: return with self.lock: targets = list(self.rooms.get(lid, {})) for ws in targets: asyncio.run_coroutine_threadsafe(ws.close(code=4404), self.loop) hub = Hub()