"""斗鱼外部单协作台账的进程内实时事件总线。 写接口提交成功后只发布「台账失效」事件,前端收到后按自己的筛选静默重拉, 不在事件里携带整行数据。当前生产单 worker,进程内队列足够;多实例时再换 Redis。 """ from __future__ import annotations import asyncio import itertools import threading from datetime import UTC, datetime from typing import Any class CollabEventHub: def __init__(self) -> None: self._lock = threading.Lock() self._revisions = itertools.count(1) self._current_revision = 0 self._loop: asyncio.AbstractEventLoop | None = None self._subscribers: set[asyncio.Queue[dict[str, Any]]] = set() def subscribe(self) -> asyncio.Queue[dict[str, Any]]: loop = asyncio.get_running_loop() queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(maxsize=100) with self._lock: self._loop = loop self._subscribers.add(queue) return queue def unsubscribe(self, queue: asyncio.Queue[dict[str, Any]]) -> None: with self._lock: self._subscribers.discard(queue) def ready_event(self) -> dict[str, Any]: with self._lock: revision = self._current_revision return {"type": "collab.ready", "revision": revision} def ping_event(self) -> dict[str, Any]: with self._lock: revision = self._current_revision return {"type": "collab.ping", "revision": revision} def publish( self, *, scope: str, action: str, actor_id: int | None = None, ) -> dict[str, Any]: with self._lock: revision = next(self._revisions) self._current_revision = revision loop = self._loop subscribers = list(self._subscribers) event = { "type": "collab.invalidate", "revision": revision, "scope": scope, "action": action, "actor_id": actor_id, "at": datetime.now(UTC).isoformat(), } if loop is None or not subscribers: return event for queue in subscribers: try: loop.call_soon_threadsafe(self._offer, queue, event) except RuntimeError: continue return event @staticmethod def _offer(queue: asyncio.Queue[dict[str, Any]], event: dict[str, Any]) -> None: while queue.full(): try: queue.get_nowait() except asyncio.QueueEmpty: break try: queue.put_nowait(event) except asyncio.QueueFull: pass collab_events = CollabEventHub()