/** * Event Bus Module * * Provides centralized event system for cross-module communication. * Features: * - Pub/sub pattern * - Namespaced events * - Event filtering * - Event history * - Integration with LiveComponents * - Integration with SSE */ import { Logger } from '../../core/logger.js'; /** * EventBus - Centralized event system */ export class EventBus { constructor(config = {}) { this.config = { enableHistory: config.enableHistory ?? false, maxHistorySize: config.maxHistorySize || 100, enableWildcards: config.enableWildcards ?? true, ...config }; this.subscribers = new Map(); // Map> this.history = []; this.middleware = []; Logger.info('[EventBus] Initialized', { enableHistory: this.config.enableHistory, enableWildcards: this.config.enableWildcards }); } /** * Create a new EventBus instance */ static create(config = {}) { return new EventBus(config); } /** * Subscribe to an event */ on(eventName, callback, options = {}) { if (typeof callback !== 'function') { throw new Error('Callback must be a function'); } if (!this.subscribers.has(eventName)) { this.subscribers.set(eventName, new Set()); } const subscriber = { callback, once: options.once ?? false, priority: options.priority || 0, filter: options.filter || null }; this.subscribers.get(eventName).add(subscriber); // Sort by priority const subscribers = Array.from(this.subscribers.get(eventName)); subscribers.sort((a, b) => b.priority - a.priority); this.subscribers.set(eventName, new Set(subscribers)); // Return unsubscribe function return () => { const callbacks = this.subscribers.get(eventName); if (callbacks) { callbacks.delete(subscriber); if (callbacks.size === 0) { this.subscribers.delete(eventName); } } }; } /** * Subscribe to an event (once) */ once(eventName, callback, options = {}) { return this.on(eventName, callback, { ...options, once: true }); } /** * Unsubscribe from an event */ off(eventName, callback) { if (!this.subscribers.has(eventName)) { return; } const callbacks = this.subscribers.get(eventName); for (const subscriber of callbacks) { if (subscriber.callback === callback) { callbacks.delete(subscriber); break; } } if (callbacks.size === 0) { this.subscribers.delete(eventName); } } /** * Emit an event */ emit(eventName, data = null, options = {}) { // Apply middleware let processedData = data; for (const middleware of this.middleware) { const result = middleware(eventName, processedData, options); if (result === null || result === false) { return; // Middleware blocked the event } if (result !== undefined) { processedData = result; } } // Add to history if (this.config.enableHistory) { this.addToHistory(eventName, processedData, options); } // Get subscribers for exact event name const subscribers = this.subscribers.get(eventName); if (subscribers) { this.notifySubscribers(subscribers, eventName, processedData, options); } // Handle wildcards if (this.config.enableWildcards) { this.handleWildcards(eventName, processedData, options); } // Handle namespaced events (e.g., 'user:created' triggers 'user:*') this.handleNamespacedEvents(eventName, processedData, options); Logger.debug('[EventBus] Event emitted', { eventName, data: processedData }); } /** * Notify subscribers */ notifySubscribers(subscribers, eventName, data, options) { const subscribersArray = Array.from(subscribers); for (const subscriber of subscribersArray) { // Apply filter if (subscriber.filter && !subscriber.filter(data, options)) { continue; } try { subscriber.callback(data, eventName, options); } catch (error) { Logger.error('[EventBus] Subscriber error', error); } // Remove if once if (subscriber.once) { subscribers.delete(subscriber); } } } /** * Handle wildcard subscriptions */ handleWildcards(eventName, data, options) { // Check for '*' subscribers const wildcardSubscribers = this.subscribers.get('*'); if (wildcardSubscribers) { this.notifySubscribers(wildcardSubscribers, eventName, data, options); } // Check for pattern matches (e.g., 'user:*' matches 'user:created') for (const [pattern, subscribers] of this.subscribers.entries()) { if (pattern.includes('*') && this.matchesPattern(eventName, pattern)) { this.notifySubscribers(subscribers, eventName, data, options); } } } /** * Handle namespaced events */ handleNamespacedEvents(eventName, data, options) { const parts = eventName.split(':'); if (parts.length > 1) { // Emit namespace wildcard (e.g., 'user:created' triggers 'user:*') const namespacePattern = parts[0] + ':*'; const namespaceSubscribers = this.subscribers.get(namespacePattern); if (namespaceSubscribers) { this.notifySubscribers(namespaceSubscribers, eventName, data, options); } } } /** * Check if event name matches pattern */ matchesPattern(eventName, pattern) { const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$'); return regex.test(eventName); } /** * Add middleware */ use(middleware) { if (typeof middleware !== 'function') { throw new Error('Middleware must be a function'); } this.middleware.push(middleware); } /** * Add event to history */ addToHistory(eventName, data, options) { this.history.push({ eventName, data, options, timestamp: Date.now() }); // Limit history size if (this.history.length > this.config.maxHistorySize) { this.history.shift(); } } /** * Get event history */ getHistory(filter = null) { if (!filter) { return [...this.history]; } if (typeof filter === 'string') { // Filter by event name return this.history.filter(event => event.eventName === filter); } if (typeof filter === 'function') { // Custom filter function return this.history.filter(filter); } return []; } /** * Clear event history */ clearHistory() { this.history = []; } /** * Get all event names */ getEventNames() { return Array.from(this.subscribers.keys()); } /** * Get subscriber count for an event */ getSubscriberCount(eventName) { const subscribers = this.subscribers.get(eventName); return subscribers ? subscribers.size : 0; } /** * Remove all subscribers for an event */ removeAllListeners(eventName = null) { if (eventName) { this.subscribers.delete(eventName); } else { this.subscribers.clear(); } } /** * Destroy event bus */ destroy() { this.subscribers.clear(); this.history = []; this.middleware = []; Logger.info('[EventBus] Destroyed'); } } /** * Create a global event bus instance */ let globalEventBus = null; /** * Get or create global event bus */ export function getGlobalEventBus(config = {}) { if (!globalEventBus) { globalEventBus = EventBus.create(config); } return globalEventBus; }