EU-Utility/premium/core/event_bus.py

660 lines
20 KiB
Python

"""
EU-Utility Premium - Event Bus
===============================
High-performance async event system for inter-plugin communication.
Features:
- Async/await support
- Event prioritization
- Event filtering and routing
- Buffered events for high-throughput scenarios
- Type-safe event definitions
Example:
from premium.core.event_bus import EventBus, Event, EventPriority
bus = EventBus()
# Subscribe to events
@bus.on('game.loot')
async def handle_loot(event: Event):
print(f"Got loot: {event.data}")
# Emit events
bus.emit('game.loot', {'item': 'Angel Scales', 'value': 150.50})
"""
from __future__ import annotations
import asyncio
import inspect
import logging
import time
import traceback
import weakref
from abc import ABC, abstractmethod
from collections import defaultdict, deque
from dataclasses import dataclass, field
from enum import Enum, auto
from queue import Queue
from threading import Lock
from typing import (
Any, Callable, Coroutine, Dict, List, Optional, Set, Type, TypeVar,
Union, Generic, Protocol, runtime_checkable
)
# =============================================================================
# EVENT PRIORITY
# =============================================================================
class EventPriority(Enum):
"""Priority levels for event handlers.
Higher priority handlers are called first.
"""
CRITICAL = 100 # System-critical events (error handling, shutdown)
HIGH = 75 # Important game events (loot, globals)
NORMAL = 50 # Standard events
LOW = 25 # Background tasks
BACKGROUND = 10 # Analytics, logging
# =============================================================================
# EVENT CLASS
# =============================================================================
@dataclass
class Event:
"""Represents an event in the system.
Attributes:
type: Event type/category
data: Event payload data
source: Source of the event (plugin_id or system)
timestamp: When the event was created
priority: Event priority
id: Unique event identifier
"""
type: str
data: Dict[str, Any] = field(default_factory=dict)
source: str = "system"
timestamp: float = field(default_factory=time.time)
priority: EventPriority = EventPriority.NORMAL
id: str = field(default_factory=lambda: f"evt_{int(time.time()*1000)}_{id(Event) % 10000}")
def get(self, key: str, default: Any = None) -> Any:
"""Get a value from event data."""
return self.data.get(key, default)
def to_dict(self) -> Dict[str, Any]:
"""Convert event to dictionary."""
return {
'id': self.id,
'type': self.type,
'data': self.data,
'source': self.source,
'timestamp': self.timestamp,
'priority': self.priority.name,
}
@classmethod
def create(
cls,
event_type: str,
data: Dict[str, Any],
source: str = "system",
priority: EventPriority = EventPriority.NORMAL
) -> Event:
"""Create a new event."""
return cls(
type=event_type,
data=data,
source=source,
priority=priority
)
# =============================================================================
# EVENT FILTER
# =============================================================================
@dataclass
class EventFilter:
"""Filter for event subscription.
Allows subscribers to receive only events matching specific criteria.
Example:
filter = EventFilter(
event_types=['game.loot', 'game.skill'],
sources=['game_client'],
min_priority=EventPriority.HIGH
)
"""
event_types: Optional[Set[str]] = None
sources: Optional[Set[str]] = None
min_priority: Optional[EventPriority] = None
data_filter: Optional[Callable[[Dict[str, Any]], bool]] = None
def matches(self, event: Event) -> bool:
"""Check if an event matches this filter."""
if self.event_types and event.type not in self.event_types:
return False
if self.sources and event.source not in self.sources:
return False
if self.min_priority and event.priority.value < self.min_priority.value:
return False
if self.data_filter and not self.data_filter(event.data):
return False
return True
# =============================================================================
# SUBSCRIPTION
# =============================================================================
@dataclass
class Subscription:
"""Represents an event subscription."""
id: str
event_types: Set[str]
callback: Callable[[Event], Any]
filter: Optional[EventFilter]
priority: EventPriority
once: bool
def __hash__(self) -> int:
"""Make subscription hashable (use id only)."""
return hash(self.id)
def __eq__(self, other) -> bool:
"""Compare subscriptions by id."""
if not isinstance(other, Subscription):
return False
return self.id == other.id
def matches(self, event: Event) -> bool:
"""Check if this subscription matches an event."""
if self.event_types and event.type not in self.event_types:
return False
if self.filter and not self.filter.matches(event):
return False
return True
# =============================================================================
# EVENT BUS
# =============================================================================
HandlerType = Callable[[Event], Any]
class EventBus:
"""High-performance async event bus.
Features:
- Async and sync handler support
- Priority-based execution order
- Event filtering
- Buffered event queues
- Wildcard subscriptions
Example:
bus = EventBus()
# Sync handler
@bus.on('game.loot')
def handle_loot(event):
print(f"Loot: {event.data}")
# Async handler
@bus.on('game.global', priority=EventPriority.HIGH)
async def handle_global(event):
await notify_discord(event.data)
# Emit event
bus.emit('game.loot', {'item': 'Uber Item', 'value': 1000})
"""
def __init__(self, max_queue_size: int = 10000):
"""Initialize event bus.
Args:
max_queue_size: Maximum events in queue before dropping
"""
self._subscribers: Dict[str, Set[Subscription]] = defaultdict(set)
self._wildcard_subscribers: Set[Subscription] = set()
self._queue: deque = deque(maxlen=max_queue_size)
self._logger = logging.getLogger("EventBus")
self._sub_counter = 0
self._running = False
self._lock = asyncio.Lock()
self._event_count = 0
self._dropped_count = 0
# Thread-safe queue for cross-thread event emission
self._thread_queue: Queue = Queue()
self._thread_lock = Lock()
self._main_loop: Optional[asyncio.AbstractEventLoop] = None
# ========== Subscription Methods ==========
def on(
self,
event_type: Union[str, List[str]],
priority: EventPriority = EventPriority.NORMAL,
filter: Optional[EventFilter] = None,
once: bool = False
) -> Callable[[HandlerType], HandlerType]:
"""Decorator to subscribe to events.
Args:
event_type: Event type(s) to subscribe to
priority: Handler priority
filter: Optional event filter
once: Remove after first matching event
Example:
@bus.on('game.loot')
def handle_loot(event):
print(event.data)
"""
def decorator(handler: HandlerType) -> HandlerType:
self.subscribe(event_type, handler, priority, filter, once)
return handler
return decorator
def subscribe(
self,
event_type: Union[str, List[str]],
handler: HandlerType,
priority: EventPriority = EventPriority.NORMAL,
filter: Optional[EventFilter] = None,
once: bool = False
) -> str:
"""Subscribe to events.
Args:
event_type: Event type(s) to subscribe to, or '*' for all
handler: Callback function
priority: Handler priority
filter: Optional event filter
once: Remove after first matching event
Returns:
Subscription ID
"""
self._sub_counter += 1
sub_id = f"sub_{self._sub_counter}"
# Handle wildcard subscription
if event_type == '*':
sub = Subscription(
id=sub_id,
event_types=set(),
callback=handler,
filter=filter,
priority=priority,
once=once
)
self._wildcard_subscribers.add(sub)
return sub_id
# Handle single or multiple event types
if isinstance(event_type, str):
event_types = {event_type}
else:
event_types = set(event_type)
sub = Subscription(
id=sub_id,
event_types=event_types,
callback=handler,
filter=filter,
priority=priority,
once=once
)
for et in event_types:
self._subscribers[et].add(sub)
return sub_id
def start(self) -> None:
"""Start the event bus and capture the main event loop.
This should be called from the main thread's async context.
"""
try:
self._main_loop = asyncio.get_running_loop()
self._running = True
self._logger.info("Event bus started")
except RuntimeError:
self._logger.warning("Event bus started without event loop")
def stop(self) -> None:
"""Stop the event bus."""
self._running = False
self._main_loop = None
self._logger.info("Event bus stopped")
def unsubscribe(self, subscription_id: str) -> bool:
"""Unsubscribe from events.
Args:
subscription_id: ID returned by subscribe()
Returns:
True if subscription was found and removed
"""
# Check wildcard subscribers
for sub in self._wildcard_subscribers:
if sub.id == subscription_id:
self._wildcard_subscribers.remove(sub)
return True
# Check typed subscribers
for event_type, subs in self._subscribers.items():
for sub in list(subs):
if sub.id == subscription_id:
subs.remove(sub)
return True
return False
def once(
self,
event_type: str,
handler: HandlerType,
priority: EventPriority = EventPriority.NORMAL,
timeout: Optional[float] = None
) -> asyncio.Future:
"""Subscribe to a single event.
Args:
event_type: Event type to wait for
handler: Optional handler callback
priority: Handler priority
timeout: Optional timeout in seconds
Returns:
Future that resolves with the event
"""
future = asyncio.Future()
def wrapper(event: Event):
if handler:
handler(event)
if not future.done():
future.set_result(event)
self.subscribe(event_type, wrapper, priority, once=True)
if timeout:
async def timeout_handler():
await asyncio.sleep(timeout)
if not future.done():
future.set_exception(TimeoutError(f"Event {event_type} timed out"))
asyncio.create_task(timeout_handler())
return future
# ========== Event Emission ==========
def emit(
self,
event_type: str,
data: Dict[str, Any],
source: str = "system",
priority: EventPriority = EventPriority.NORMAL
) -> Event:
"""Emit an event.
Args:
event_type: Type of event
data: Event data
source: Event source
priority: Event priority
Returns:
The created event
"""
event = Event.create(
event_type=event_type,
data=data,
source=source,
priority=priority
)
self._queue.append(event)
self._event_count += 1
# Try to process in async context
try:
loop = asyncio.get_running_loop()
# We're in an async context - safe to create task
asyncio.create_task(self._process_event(event))
except RuntimeError:
# No event loop in this thread - queue for main thread
with self._thread_lock:
self._thread_queue.put(event)
# Try to notify main loop if available
if self._main_loop and self._main_loop.is_running():
try:
self._main_loop.call_soon_threadsafe(
lambda: asyncio.create_task(self._process_thread_queue())
)
except:
pass
return event
async def _process_thread_queue(self) -> None:
"""Process events queued from other threads."""
while not self._thread_queue.empty():
try:
with self._thread_lock:
if not self._thread_queue.empty():
event = self._thread_queue.get_nowait()
await self._process_event(event)
except:
break
async def emit_async(
self,
event_type: str,
data: Dict[str, Any],
source: str = "system",
priority: EventPriority = EventPriority.NORMAL
) -> Event:
"""Emit an event asynchronously."""
event = Event.create(
event_type=event_type,
data=data,
source=source,
priority=priority
)
await self._process_event(event)
return event
async def _process_event(self, event: Event) -> None:
"""Process a single event."""
# Collect all matching handlers
handlers: List[tuple] = []
# Get handlers for specific event type
for sub in self._subscribers.get(event.type, set()):
if sub.matches(event):
handlers.append((sub.priority.value, sub))
# Get wildcard handlers
for sub in self._wildcard_subscribers:
if sub.filter is None or sub.filter.matches(event):
handlers.append((sub.priority.value, sub))
# Sort by priority (highest first)
handlers.sort(key=lambda x: -x[0])
# Execute handlers
once_subs = []
for _, sub in handlers:
try:
if inspect.iscoroutinefunction(sub.callback):
await sub.callback(event)
else:
sub.callback(event)
if sub.once:
once_subs.append(sub)
except Exception as e:
self._logger.error(f"Error in event handler: {e}")
traceback.print_exc()
# Remove once subscriptions
for sub in once_subs:
self.unsubscribe(sub.id)
# ========== Utility Methods ==========
def clear(self) -> None:
"""Clear all subscriptions and queued events."""
self._subscribers.clear()
self._wildcard_subscribers.clear()
self._queue.clear()
def get_stats(self) -> Dict[str, Any]:
"""Get event bus statistics."""
return {
'total_events': self._event_count,
'dropped_events': self._dropped_count,
'queue_size': len(self._queue),
'subscription_count': sum(
len(subs) for subs in self._subscribers.values()
),
'wildcard_subscriptions': len(self._wildcard_subscribers),
}
def wait_for(
self,
event_type: str,
condition: Optional[Callable[[Event], bool]] = None,
timeout: Optional[float] = None
) -> asyncio.Future:
"""Wait for a specific event.
Args:
event_type: Event type to wait for
condition: Optional condition function
timeout: Optional timeout in seconds
Returns:
Future that resolves with the matching event
"""
future = asyncio.Future()
def handler(event: Event):
if condition and not condition(event):
return
if not future.done():
future.set_result(event)
return True # Unsubscribe
self.subscribe(event_type, handler, once=True)
if timeout:
async def timeout_coro():
await asyncio.sleep(timeout)
if not future.done():
future.set_exception(TimeoutError(f"Timeout waiting for {event_type}"))
asyncio.create_task(timeout_coro())
return future
# =============================================================================
# TYPED EVENTS
# =============================================================================
class TypedEventBus(EventBus):
"""Type-safe event bus with predefined event types."""
# Game events
GAME_CONNECTED = "game.connected"
GAME_DISCONNECTED = "game.disconnected"
GAME_FOCUS_CHANGED = "game.focus_changed"
# Loot events
LOOT_RECEIVED = "game.loot"
GLOBAL_HOF = "game.global"
# Skill events
SKILL_GAIN = "game.skill"
PROFESSION_GAIN = "game.profession"
# Chat events
CHAT_MESSAGE = "game.chat"
# System events
PLUGIN_LOADED = "system.plugin.loaded"
PLUGIN_UNLOADED = "system.plugin.unloaded"
ERROR_OCCURRED = "system.error"
def emit_game_connected(self, character_name: str) -> Event:
"""Emit game connected event."""
return self.emit(self.GAME_CONNECTED, {
'character_name': character_name
})
def emit_loot(self, item: str, value: float, quantity: int = 1) -> Event:
"""Emit loot received event."""
return self.emit(self.LOOT_RECEIVED, {
'item': item,
'value': value,
'quantity': quantity,
'timestamp': time.time(),
})
def emit_skill(self, skill_name: str, value: float, gain: float) -> Event:
"""Emit skill gain event."""
return self.emit(self.SKILL_GAIN, {
'skill': skill_name,
'value': value,
'gain': gain,
})
def emit_global(self, mob_name: str, value: float, player: str) -> Event:
"""Emit global/HoF event."""
return self.emit(self.GLOBAL_HOF, {
'mob': mob_name,
'value': value,
'player': player,
'is_hof': value >= 1000,
})
# =============================================================================
# EXPORTS
# =============================================================================
__all__ = [
'EventPriority',
'Event',
'EventFilter',
'Subscription',
'EventBus',
'TypedEventBus',
]