52 lines
1.7 KiB
Python
52 lines
1.7 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import itertools
|
|
import time
|
|
from collections.abc import AsyncIterator
|
|
|
|
|
|
class EventBroker:
|
|
"""Small in-process fan-out for UI invalidation events."""
|
|
|
|
def __init__(self) -> None:
|
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
self._subscribers: dict[int, asyncio.Queue[dict]] = {}
|
|
self._ids = itertools.count(1)
|
|
|
|
def bind(self) -> None:
|
|
self._loop = asyncio.get_running_loop()
|
|
|
|
def _publish(self, event: dict) -> None:
|
|
for queue in list(self._subscribers.values()):
|
|
if queue.full():
|
|
try:
|
|
queue.get_nowait()
|
|
except asyncio.QueueEmpty:
|
|
pass
|
|
queue.put_nowait(event)
|
|
|
|
def publish(self, topic: str, **payload) -> None:
|
|
loop = self._loop
|
|
if loop is None or loop.is_closed():
|
|
return
|
|
event = {"topic": topic, "at": time.time(), **payload}
|
|
loop.call_soon_threadsafe(self._publish, event)
|
|
|
|
async def subscribe(self) -> AsyncIterator[dict | None]:
|
|
subscriber_id = next(self._ids)
|
|
queue: asyncio.Queue[dict] = asyncio.Queue(maxsize=32)
|
|
self._subscribers[subscriber_id] = queue
|
|
try:
|
|
yield {"topic": "connected", "at": time.time()}
|
|
while True:
|
|
try:
|
|
yield await asyncio.wait_for(queue.get(), timeout=20)
|
|
except TimeoutError:
|
|
yield None
|
|
finally:
|
|
self._subscribers.pop(subscriber_id, None)
|
|
|
|
def status(self) -> dict[str, int | bool]:
|
|
return {"ready": self._loop is not None, "subscribers": len(self._subscribers)}
|