92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
"""Lightweight in-process event bus for inter-module communication.
|
|
|
|
Usage:
|
|
from core.event_bus import bus
|
|
|
|
# Subscribe
|
|
bus.on("voice.user_said", my_handler) # sync or async callable
|
|
bus.on("motion.action_done", other_handler)
|
|
|
|
# Publish
|
|
await bus.emit("voice.user_said", text="hello")
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import threading
|
|
from collections import defaultdict
|
|
from typing import Any, Callable
|
|
|
|
from Project.Sanad.core.logger import get_logger
|
|
|
|
log = get_logger("event_bus", to_console=False)
|
|
|
|
|
|
class EventBus:
|
|
def __init__(self):
|
|
self._lock = threading.Lock()
|
|
self._listeners: dict[str, list[Callable]] = defaultdict(list)
|
|
|
|
def on(self, event: str, callback: Callable):
|
|
with self._lock:
|
|
self._listeners[event].append(callback)
|
|
log.debug("Subscribed %s → %s", event, callback.__qualname__)
|
|
|
|
def off(self, event: str, callback: Callable):
|
|
with self._lock:
|
|
try:
|
|
self._listeners[event].remove(callback)
|
|
except ValueError:
|
|
pass
|
|
|
|
async def emit(self, event: str, **kwargs: Any):
|
|
with self._lock:
|
|
handlers = list(self._listeners.get(event, []))
|
|
for handler in handlers:
|
|
try:
|
|
result = handler(**kwargs)
|
|
if asyncio.iscoroutine(result):
|
|
await result
|
|
except Exception:
|
|
log.exception("Handler %s for event '%s' failed", handler.__qualname__, event)
|
|
|
|
def emit_sync(self, event: str, **kwargs: Any):
|
|
"""Fire-and-forget from a sync context.
|
|
|
|
Async handlers are scheduled on the running event loop if one exists.
|
|
Otherwise they are dropped with a warning (the original silent-no-op
|
|
bug — at least now it's logged).
|
|
"""
|
|
with self._lock:
|
|
handlers = list(self._listeners.get(event, []))
|
|
for handler in handlers:
|
|
try:
|
|
if asyncio.iscoroutinefunction(handler):
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
loop.create_task(handler(**kwargs))
|
|
except RuntimeError:
|
|
log.warning(
|
|
"Async handler %s for '%s' dropped — no running loop",
|
|
handler.__qualname__, event,
|
|
)
|
|
continue
|
|
result = handler(**kwargs)
|
|
if asyncio.iscoroutine(result):
|
|
# Sync handler returned a coroutine — schedule it
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
loop.create_task(result)
|
|
except RuntimeError:
|
|
result.close()
|
|
log.warning(
|
|
"Coroutine result from %s for '%s' dropped — no running loop",
|
|
handler.__qualname__, event,
|
|
)
|
|
except Exception:
|
|
log.exception("Handler %s for event '%s' failed", handler.__qualname__, event)
|
|
|
|
|
|
bus = EventBus()
|