"""Sensory EventBus — simple pub/sub for SensoryEvents. Thin facade over the infrastructure EventBus that speaks in SensoryEvent objects instead of raw infrastructure Events. """ import asyncio import logging from collections.abc import Awaitable, Callable from timmy.events import SensoryEvent logger = logging.getLogger(__name__) # Handler: sync or async callable that receives a SensoryEvent SensoryHandler = Callable[[SensoryEvent], None | Awaitable[None]] class SensoryBus: """Pub/sub dispatcher for SensoryEvents.""" def __init__(self, max_history: int = 500) -> None: self._subscribers: dict[str, list[SensoryHandler]] = {} self._history: list[SensoryEvent] = [] self._max_history = max_history # ── Public API ──────────────────────────────────────────────────────── async def emit(self, event: SensoryEvent) -> int: """Push *event* to all subscribers whose event_type filter matches. Returns the number of handlers invoked. """ self._history.append(event) if len(self._history) > self._max_history: self._history = self._history[-self._max_history :] handlers = self._matching_handlers(event.event_type) for h in handlers: try: result = h(event) if asyncio.iscoroutine(result): await result except Exception as exc: logger.error("SensoryBus handler error for '%s': %s", event.event_type, exc) return len(handlers) def subscribe(self, event_type: str, callback: SensoryHandler) -> None: """Register *callback* for events matching *event_type*. Use ``"*"`` to subscribe to all event types. """ self._subscribers.setdefault(event_type, []).append(callback) def recent(self, n: int = 10) -> list[SensoryEvent]: """Return the last *n* events (most recent last).""" return self._history[-n:] # ── Internals ───────────────────────────────────────────────────────── def _matching_handlers(self, event_type: str) -> list[SensoryHandler]: handlers: list[SensoryHandler] = [] for pattern, cbs in self._subscribers.items(): if pattern == "*" or pattern == event_type: handlers.extend(cbs) return handlers # ── Module-level singleton ──────────────────────────────────────────────────── _bus: SensoryBus | None = None def get_sensory_bus() -> SensoryBus: """Return the module-level SensoryBus singleton.""" global _bus if _bus is None: _bus = SensoryBus() return _bus