"""Server-Sent Events manager (ROADMAP #85, tranche 4). Singleton extrait de :mod:`backend.main` sans changement de comportement : les routers montés par ``main`` partagent la même instance (les clients SSE connectés sur ``/api/events`` reçoivent les broadcasts émis depuis n'importe quel router). """ from __future__ import annotations import asyncio import json as _json import logging logger = logging.getLogger("obsigate") class SSEManager: """Manages SSE client connections and broadcasts events.""" def __init__(self): self._clients: list[asyncio.Queue] = [] async def connect(self) -> asyncio.Queue: """Register a new SSE client and return its message queue.""" queue: asyncio.Queue = asyncio.Queue() self._clients.append(queue) logger.debug(f"SSE client connected (total: {len(self._clients)})") return queue def disconnect(self, queue: asyncio.Queue): """Remove a disconnected SSE client.""" if queue in self._clients: self._clients.remove(queue) logger.debug(f"SSE client disconnected (total: {len(self._clients)})") async def broadcast(self, event_type: str, data: dict): """Send an event to all connected SSE clients.""" message = _json.dumps(data, ensure_ascii=False) dead: list[asyncio.Queue] = [] for q in self._clients: try: q.put_nowait({"event": event_type, "data": message}) except asyncio.QueueFull: dead.append(q) for q in dead: self.disconnect(q) @property def client_count(self) -> int: return len(self._clients) sse_manager = SSEManager()