from __future__ import annotations import asyncio import json from queue import Empty, Full, Queue from threading import Lock from typing import Any class EventBus: """Small bounded fan-out bus used by the SSE endpoint. Persistence is handled by SQLite; this bus only wakes currently connected clients. A queue per client keeps one slow browser from blocking probes or other subscribers. """ def __init__(self, max_queue_size: int = 100) -> None: self.max_queue_size = max_queue_size self._subscribers: set[Queue[dict[str, Any]]] = set() self._lock = Lock() def subscribe(self) -> Queue[dict[str, Any]]: queue: Queue[dict[str, Any]] = Queue(maxsize=self.max_queue_size) with self._lock: self._subscribers.add(queue) return queue def unsubscribe(self, queue: Queue[dict[str, Any]]) -> None: with self._lock: self._subscribers.discard(queue) def publish(self, payload: dict[str, Any]) -> None: with self._lock: subscribers = tuple(self._subscribers) for queue in subscribers: try: queue.put_nowait(payload) except Full: # Drop the oldest event for a lagging client, then retain the # newest state notification. The durable event journal remains # the source of truth if a client reconnects. try: queue.get_nowait() except Empty: pass try: queue.put_nowait(payload) except Full: pass event_bus = EventBus() def sse_payload(payload: dict[str, Any]) -> str: event_type = str(payload.get("event_type", "update")) return f"event: {event_type}\ndata: {json.dumps(payload, default=str)}\n\n" async def next_event(queue: Queue[dict[str, Any]], timeout: float = 15.0) -> dict[str, Any] | None: try: return await asyncio.to_thread(queue.get, True, timeout) except Empty: return None