Files
ServiceMap-Public/backend/app/events.py
T

64 lines
2.0 KiB
Python

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