""" Event Bus — Async pub/sub for real-time gateway events. WebSocket connections, MCP server, and internal services all subscribe to events here. Pure asyncio — no external deps. """ import asyncio import logging from typing import Optional from models.events import EventType, GatewayEvent logger = logging.getLogger(__name__) class EventBus: """ Async pub/sub event bus using asyncio.Queue per subscriber. Features: - Non-blocking publish (put_nowait) - Automatic dead-subscriber cleanup (full queues are removed) - Event history (last N events for late joiners) - Typed event filtering on subscriptions - Async iteration via EventSubscription """ def __init__(self, max_history: int = 1000): self._subscribers: list[tuple[asyncio.Queue[GatewayEvent], Optional[set[EventType]]]] = [] self._history: list[GatewayEvent] = [] self._max_history = max_history async def publish(self, event: GatewayEvent) -> None: """Publish an event to all subscribers.""" self._history.append(event) if len(self._history) > self._max_history: self._history = self._history[-self._max_history :] logger.info(f"📡 Event: {event.type.value} | {event.message or ''}") dead_queues = [] for queue, type_filter in self._subscribers: # Skip if subscriber has a type filter and this event doesn't match if type_filter and event.type not in type_filter: continue try: queue.put_nowait(event) except asyncio.QueueFull: dead_queues.append((queue, type_filter)) for entry in dead_queues: self._subscribers.remove(entry) def subscribe( self, max_size: int = 100, event_types: Optional[set[EventType]] = None, ) -> "EventSubscription": """ Create a new subscription. Args: max_size: Queue depth before subscriber is considered dead. event_types: Optional filter — only receive these event types. None means receive everything. Returns: An async iterator of GatewayEvents. """ queue: asyncio.Queue[GatewayEvent] = asyncio.Queue(maxsize=max_size) entry = (queue, event_types) self._subscribers.append(entry) return EventSubscription(queue, self, entry) def unsubscribe(self, entry: tuple) -> None: """Remove a subscriber.""" if entry in self._subscribers: self._subscribers.remove(entry) @property def recent_events(self) -> list[GatewayEvent]: """Get recent event history.""" return list(self._history) @property def subscriber_count(self) -> int: return len(self._subscribers) class EventSubscription: """An async iterator that yields events from the bus.""" def __init__( self, queue: asyncio.Queue[GatewayEvent], bus: EventBus, entry: tuple, ): self._queue = queue self._bus = bus self._entry = entry def __aiter__(self): return self async def __anext__(self) -> GatewayEvent: try: return await self._queue.get() except asyncio.CancelledError: self._bus.unsubscribe(self._entry) raise async def get(self, timeout: Optional[float] = None) -> GatewayEvent: """Get next event with optional timeout.""" return await asyncio.wait_for(self._queue.get(), timeout=timeout) def close(self): """Unsubscribe from the event bus.""" self._bus.unsubscribe(self._entry)