forked from Rockachopa/Timmy-time-dashboard
feat: SensoryEvent model + SensoryBus dispatcher (#318)
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
This commit is contained in:
79
src/timmy/event_bus.py
Normal file
79
src/timmy/event_bus.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user