""" Heicode SSE 事件总线(§7.8.3 配套) 进程内 pub/sub:每个用户的活跃 SSE 订阅者用一个 asyncio.Queue 排队接收事件; 其他业务路径(approvals/tasks/...)通过 emit() 发布事件。 ⚠️ 单进程范围:mcp-server 多副本部署下,副本 A 上的订阅者**收不到**副本 B 上 emit 的事件。MVP 阶段可接受(cc-haha 单连接);生产化时换成 Redis pub/sub 或 NATS(mcp-server 已部署 NATS)即可,emit/subscribe 接口不变。 """ from __future__ import annotations import asyncio from typing import Any, Dict, List, Optional, Set class _EventBus: """每用户独立队列的内存 pub/sub。""" def __init__(self): # user_id (str) → set of asyncio.Queue self._subscribers: Dict[str, Set[asyncio.Queue]] = {} self._lock = asyncio.Lock() async def subscribe(self, user_id: str) -> asyncio.Queue: """订阅指定用户的事件流。返回一个新的 Queue。""" queue: asyncio.Queue = asyncio.Queue(maxsize=256) async with self._lock: self._subscribers.setdefault(user_id, set()).add(queue) return queue async def unsubscribe(self, user_id: str, queue: asyncio.Queue) -> None: async with self._lock: subs = self._subscribers.get(user_id) if subs: subs.discard(queue) if not subs: self._subscribers.pop(user_id, None) async def emit(self, user_id: str, event_type: str, data: Dict[str, Any]) -> int: """向指定用户的所有订阅者推送事件。返回送达的订阅者数。 队列已满时 drop(不阻塞 emit;客户端断重连后会重拉一次状态)。 """ payload = {"event": event_type, "data": data} delivered = 0 async with self._lock: subs = list(self._subscribers.get(user_id, [])) for q in subs: try: q.put_nowait(payload) delivered += 1 except asyncio.QueueFull: pass return delivered def subscriber_count(self, user_id: Optional[str] = None) -> int: if user_id is not None: return len(self._subscribers.get(user_id, set())) return sum(len(s) for s in self._subscribers.values()) # 单例 _default_bus: Optional[_EventBus] = None def get_event_bus() -> _EventBus: global _default_bus if _default_bus is None: _default_bus = _EventBus() return _default_bus