teecup/app/realtime.py

65 lines
2.5 KiB
Python
Raw Permalink Normal View History

"""
Delt in-memory WebSocket-kringkasting for sanntidsoppdateringer -- den
offentlige "Følg live"-siden (ADR-027, oppfølging av ADR-025/026) OG
frittstående runder (2026-07-26, oppfølging av ADR-033/036 sin
medspiller-utvidelse).
Egen, RUTEFRI modul -- importerer INGEN router. Grunnen: både
`scoring.py`/`tournaments.py` (som endrer matchstatus) og `messaging.py`
(som eier selve WS-ruten, `/ws/public/tournaments/{id}/live`) trenger denne,
og `registration.py` importerer allerede fra scoring.py/tournaments.py/
matches.py mens `messaging.py` importerer fra `registration.py` -- en
kringkastingsfunksjon plassert i en av routerne ville skapt en sirkulær
import. Denne modulen ligger BAK alle routere i importgrafen i stedet.
`rounds.py` er ikke del av den kjeden, men gjenbruker samme modul for
konsistens (kun ett sted i kodebasen som eier WebSocket-kringkasting).
In-memory PER PROSESS -- samme kjente begrensning som meldinger sin egen
kringkasting (`app/routers/messaging.py`): trygt med dagens ene
`teecup_api`-container, deles IKKE tvers av flere prosesser/containere
(se ARCHITECTURE_DECISIONS.md sine "Åpne spørsmål").
Bevisst "noe endret seg, hent på nytt"-signal, IKKE selve dataene --
i motsetning til meldinger (der selve meldingen kringkastes direkte),
ville det å bygge/sende hele leaderboard+matchliste+scorekort-formen her
duplisert betydelig beregningslogikk. Klienten reagerer ved å kalle de
samme REST-endepunktene nytt, akkurat som ved førstegangslasting.
"""
from collections import defaultdict
from fastapi import WebSocket
_tournament_live_sockets: dict[str, set[WebSocket]] = defaultdict(set)
_round_live_sockets: dict[str, set[WebSocket]] = defaultdict(set)
def live_sockets_for(tournament_id: str) -> set[WebSocket]:
return _tournament_live_sockets[tournament_id]
async def broadcast_live_update(tournament_id: str) -> None:
await _broadcast(_tournament_live_sockets, tournament_id)
def live_sockets_for_round(round_id: str) -> set[WebSocket]:
return _round_live_sockets[round_id]
async def broadcast_round_update(round_id: str) -> None:
await _broadcast(_round_live_sockets, round_id)
async def _broadcast(registry: dict[str, set[WebSocket]], key: str) -> None:
sockets = registry.get(key)
if not sockets:
return
dead = []
for ws in list(sockets):
try:
await ws.send_json({"type": "update"})
except Exception:
dead.append(ws)
for ws in dead:
sockets.discard(ws)