/** * EventBus - Strict event-driven communication system * Enforces type safety and prevents direct module coupling */ class EventBus { constructor() { // Private event storage this._listeners = new Map(); this._moduleRegistry = new Map(); this._eventHistory = []; this._maxHistorySize = 1000; // Seal to prevent external modification Object.seal(this); } /** * Register a module with the event bus * @param {Module} module - Module instance */ registerModule(module) { if (!module || typeof module.name !== 'string') { throw new Error('Invalid module: must have a name property'); } if (this._moduleRegistry.has(module.name)) { throw new Error(`Module ${module.name} is already registered`); } this._moduleRegistry.set(module.name, module); } /** * Unregister a module and clean up its listeners * @param {string} moduleName - Name of module to unregister */ unregisterModule(moduleName) { if (!this._moduleRegistry.has(moduleName)) { throw new Error(`Module ${moduleName} is not registered`); } // Remove all listeners for this module for (const [eventType, listeners] of this._listeners) { const filteredListeners = listeners.filter(listener => listener.module !== moduleName); if (filteredListeners.length === 0) { this._listeners.delete(eventType); } else { this._listeners.set(eventType, filteredListeners); } } this._moduleRegistry.delete(moduleName); } /** * Subscribe to an event type * @param {string} eventType - Type of event to listen for * @param {Function} callback - Function to call when event occurs * @param {string} moduleName - Name of the subscribing module */ on(eventType, callback, moduleName) { this._validateEventType(eventType); this._validateCallback(callback); this._validateModule(moduleName); if (!this._listeners.has(eventType)) { this._listeners.set(eventType, []); } const listener = { callback, module: moduleName, id: this._generateId() }; this._listeners.get(eventType).push(listener); return listener.id; // Return ID for unsubscribing } /** * Unsubscribe from an event * @param {string} eventType - Event type to unsubscribe from * @param {string} listenerId - ID returned from on() method */ off(eventType, listenerId) { this._validateEventType(eventType); if (!this._listeners.has(eventType)) { return false; } const listeners = this._listeners.get(eventType); const index = listeners.findIndex(listener => listener.id === listenerId); if (index === -1) { return false; } listeners.splice(index, 1); if (listeners.length === 0) { this._listeners.delete(eventType); } return true; } /** * Emit an event to all subscribers * @param {string} eventType - Type of event * @param {*} data - Event data * @param {string} sourceModule - Module emitting the event */ emit(eventType, data = null, sourceModule) { this._validateEventType(eventType); this._validateModule(sourceModule); const event = { type: eventType, data, source: sourceModule, timestamp: Date.now(), id: this._generateId() }; // Add to history this._addToHistory(event); // Get listeners for this event type const listeners = this._listeners.get(eventType) || []; // Call all listeners (async to prevent blocking) listeners.forEach(listener => { try { // Prevent modules from listening to their own events (optional) if (listener.module !== sourceModule) { setTimeout(() => listener.callback(event), 0); } } catch (error) { console.error(`Error in event listener for ${eventType}:`, error); } }); return event.id; } /** * Get event history (for debugging) * @param {number} limit - Maximum number of events to return */ getEventHistory(limit = 50) { return this._eventHistory.slice(-limit); } /** * Get registered modules (for debugging) */ getRegisteredModules() { return Array.from(this._moduleRegistry.keys()); } /** * Get active listeners (for debugging) */ getActiveListeners() { const result = {}; for (const [eventType, listeners] of this._listeners) { result[eventType] = listeners.map(l => l.module); } return result; } // Private validation methods _validateEventType(eventType) { if (!eventType || typeof eventType !== 'string') { throw new Error('Event type must be a non-empty string'); } } _validateCallback(callback) { if (typeof callback !== 'function') { throw new Error('Callback must be a function'); } } _validateModule(moduleName) { if (!moduleName || typeof moduleName !== 'string') { throw new Error('Module name must be a non-empty string'); } if (!this._moduleRegistry.has(moduleName)) { throw new Error(`Module ${moduleName} is not registered with EventBus`); } } _generateId() { return Math.random().toString(36).substr(2, 9); } _addToHistory(event) { this._eventHistory.push(event); // Limit history size if (this._eventHistory.length > this._maxHistorySize) { this._eventHistory.shift(); } } } // Freeze to prevent modification Object.freeze(EventBus); Object.freeze(EventBus.prototype); export default EventBus;