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
80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
"""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()
|