Lobby: WebSockets for real-time sync
Server pushes small change notes (messages / lobby / sheet) per lobby; clients fetch data through the normal API so visibility rules stay in one place. Polling stays as slow fallback. nginx WebSocket headers documented. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user