""" EventBus for Information Spaces. This module provides an in-process publish/subscribe system for space events. It supports both synchronous and asynchronous event handlers. """ import asyncio import logging from collections import defaultdict from typing import Callable, Dict, List, Optional, Set, Union, Any from dataclasses import dataclass, field import weakref from .models import SpaceEvent, SpaceEventType # Type aliases for handlers SyncHandler = Callable[[SpaceEvent], None] AsyncHandler = Callable[[SpaceEvent], Any] # Coroutine Handler = Union[SyncHandler, AsyncHandler] logger = logging.getLogger(__name__) @dataclass class HandlerRegistration: """ Registration info for an event handler. Attributes: handler: The handler function is_async: Whether the handler is async priority: Handler priority (lower = earlier execution) handler_id: Unique ID for this registration """ handler: Handler is_async: bool = False priority: int = 100 handler_id: str = field(default_factory=lambda: str(id(None))) def __post_init__(self): self.handler_id = str(id(self.handler)) class EventBus: """ In-process event bus for space events. Provides publish/subscribe functionality for space operations. Handlers can be registered for specific event types or for all events. Usage: bus = EventBus() # Register a handler def on_space_created(event: SpaceEvent): print(f"Space created: {event.payload['name']}") bus.subscribe(SpaceEventType.SPACE_CREATED, on_space_created) # Emit an event bus.emit(SpaceEvent( event_type=SpaceEventType.SPACE_CREATED, space_id="space-123", payload={"name": "my-docs"} )) # Unsubscribe bus.unsubscribe(SpaceEventType.SPACE_CREATED, on_space_created) """ def __init__(self): """Initialize the event bus.""" # Handlers indexed by event type self._handlers: Dict[SpaceEventType, List[HandlerRegistration]] = defaultdict( list ) # Handlers that receive all events self._global_handlers: List[HandlerRegistration] = [] # Track all handler IDs for quick lookup self._handler_ids: Set[str] = set() # Event history (optional, for debugging) self._history: List[SpaceEvent] = [] self._history_enabled: bool = False self._max_history: int = 1000 def subscribe( self, event_type: Optional[SpaceEventType], handler: Handler, priority: int = 100, ) -> str: """ Subscribe a handler to events. Args: event_type: The event type to subscribe to, or None for all events handler: The handler function (sync or async) priority: Handler priority (lower = earlier execution) Returns: Handler ID that can be used to unsubscribe """ is_async = asyncio.iscoroutinefunction(handler) registration = HandlerRegistration( handler=handler, is_async=is_async, priority=priority, ) if event_type is None: self._global_handlers.append(registration) self._global_handlers.sort(key=lambda r: r.priority) else: self._handlers[event_type].append(registration) self._handlers[event_type].sort(key=lambda r: r.priority) self._handler_ids.add(registration.handler_id) return registration.handler_id def subscribe_all(self, handler: Handler, priority: int = 100) -> str: """ Subscribe a handler to all events. Args: handler: The handler function priority: Handler priority Returns: Handler ID """ return self.subscribe(None, handler, priority) def unsubscribe( self, event_type: Optional[SpaceEventType], handler: Handler, ) -> bool: """ Unsubscribe a handler from events. Args: event_type: The event type, or None for global handlers handler: The handler to remove Returns: True if handler was found and removed """ handler_id = str(id(handler)) if event_type is None: for i, reg in enumerate(self._global_handlers): if reg.handler_id == handler_id: self._global_handlers.pop(i) self._handler_ids.discard(handler_id) return True else: handlers = self._handlers.get(event_type, []) for i, reg in enumerate(handlers): if reg.handler_id == handler_id: handlers.pop(i) self._handler_ids.discard(handler_id) return True return False def unsubscribe_by_id(self, handler_id: str) -> bool: """ Unsubscribe a handler by its ID. Args: handler_id: The handler ID returned by subscribe Returns: True if handler was found and removed """ # Check global handlers for i, reg in enumerate(self._global_handlers): if reg.handler_id == handler_id: self._global_handlers.pop(i) self._handler_ids.discard(handler_id) return True # Check type-specific handlers for handlers in self._handlers.values(): for i, reg in enumerate(handlers): if reg.handler_id == handler_id: handlers.pop(i) self._handler_ids.discard(handler_id) return True return False def emit(self, event: SpaceEvent) -> List[Exception]: """ Emit an event synchronously. Calls all registered handlers for the event type. Async handlers are run in a new event loop if needed. Args: event: The event to emit Returns: List of exceptions raised by handlers (empty if all succeeded) """ if self._history_enabled: self._record_history(event) exceptions: List[Exception] = [] # Collect all handlers to call handlers = list(self._global_handlers) handlers.extend(self._handlers.get(event.event_type, [])) handlers.sort(key=lambda r: r.priority) for registration in handlers: try: if registration.is_async: # Run async handler try: loop = asyncio.get_running_loop() except RuntimeError: loop = None if loop and loop.is_running(): # Create a task but don't await it here asyncio.create_task(registration.handler(event)) else: asyncio.run(registration.handler(event)) else: registration.handler(event) except Exception as e: logger.exception( f"Handler {registration.handler_id} raised exception for event {event.event_type}" ) exceptions.append(e) return exceptions async def emit_async(self, event: SpaceEvent) -> List[Exception]: """ Emit an event asynchronously. Args: event: The event to emit Returns: List of exceptions raised by handlers """ if self._history_enabled: self._record_history(event) exceptions: List[Exception] = [] # Collect all handlers to call handlers = list(self._global_handlers) handlers.extend(self._handlers.get(event.event_type, [])) handlers.sort(key=lambda r: r.priority) for registration in handlers: try: if registration.is_async: await registration.handler(event) else: registration.handler(event) except Exception as e: logger.exception( f"Handler {registration.handler_id} raised exception for event {event.event_type}" ) exceptions.append(e) return exceptions def has_handlers(self, event_type: SpaceEventType) -> bool: """ Check if there are any handlers for an event type. Args: event_type: The event type to check Returns: True if there are handlers (including global handlers) """ return bool(self._global_handlers) or bool( self._handlers.get(event_type, []) ) def handler_count(self, event_type: Optional[SpaceEventType] = None) -> int: """ Get the number of handlers registered. Args: event_type: Specific event type, or None for total count Returns: Number of handlers """ if event_type is None: count = len(self._global_handlers) for handlers in self._handlers.values(): count += len(handlers) return count else: return len(self._handlers.get(event_type, [])) + len( self._global_handlers ) def clear(self, event_type: Optional[SpaceEventType] = None) -> int: """ Clear handlers. Args: event_type: Specific event type, or None to clear all Returns: Number of handlers removed """ if event_type is None: count = self.handler_count() self._handlers.clear() self._global_handlers.clear() self._handler_ids.clear() return count else: handlers = self._handlers.pop(event_type, []) for reg in handlers: self._handler_ids.discard(reg.handler_id) return len(handlers) # History management def enable_history(self, max_events: int = 1000) -> None: """Enable event history recording.""" self._history_enabled = True self._max_history = max_events def disable_history(self) -> None: """Disable event history recording.""" self._history_enabled = False def clear_history(self) -> None: """Clear the event history.""" self._history.clear() def get_history( self, event_type: Optional[SpaceEventType] = None, space_id: Optional[str] = None, limit: Optional[int] = None, ) -> List[SpaceEvent]: """ Get event history. Args: event_type: Filter by event type space_id: Filter by space ID limit: Maximum number of events to return Returns: List of events (newest first) """ events = list(reversed(self._history)) if event_type is not None: events = [e for e in events if e.event_type == event_type] if space_id is not None: events = [e for e in events if e.space_id == space_id] if limit is not None: events = events[:limit] return events def _record_history(self, event: SpaceEvent) -> None: """Record an event in history.""" self._history.append(event) if len(self._history) > self._max_history: self._history = self._history[-self._max_history :] # Global event bus instance (optional singleton pattern) _default_bus: Optional[EventBus] = None def get_event_bus() -> EventBus: """Get the default global event bus instance.""" global _default_bus if _default_bus is None: _default_bus = EventBus() return _default_bus def reset_event_bus() -> None: """Reset the default global event bus (useful for testing).""" global _default_bus _default_bus = None