Files
OpenOPC/opc/core/events.py
T
2026-07-01 17:56:31 +08:00

51 lines
1.7 KiB
Python

"""In-process event bus for OPC system."""
from __future__ import annotations
import asyncio
from collections import defaultdict
from typing import Any, Callable, Coroutine
from opc.core.models import OPCEvent
Listener = Callable[[OPCEvent], Coroutine[Any, Any, None]]
class EventBus:
"""Simple async pub/sub event bus for inter-layer communication."""
def __init__(self) -> None:
self._listeners: dict[str, list[Listener]] = defaultdict(list)
self._global_listeners: list[Listener] = []
self._history: list[OPCEvent] = []
self._lock: asyncio.Lock | None = None
def _get_lock(self) -> asyncio.Lock:
if self._lock is None:
self._lock = asyncio.Lock()
return self._lock
def subscribe(self, event_type: str, listener: Listener) -> None:
self._listeners[event_type].append(listener)
def subscribe_all(self, listener: Listener) -> None:
self._global_listeners.append(listener)
async def publish(self, event: OPCEvent) -> None:
async with self._get_lock():
self._history.append(event)
# Snapshot listener lists under lock to avoid mutation during iteration
typed = list(self._listeners.get(event.event_type, []))
globl = list(self._global_listeners)
# Execute listeners outside lock to avoid holding it during async work
tasks = [fn(event) for fn in typed + globl]
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
def get_history(self, event_type: str | None = None, limit: int = 50) -> list[OPCEvent]:
events = self._history
if event_type:
events = [e for e in events if e.event_type == event_type]
return events[-limit:]