/** * @file EventBus.js * @description Event communication system for decoupled components * @author Implementation Team */ /** * @class EventBus * @description Simple pub/sub event bus for component communication * * @example * import EventBus from './EventBus.js'; * * // Subscribe to event * EventBus.on('move-made', (data) => { * console.log('Move:', data.move); * }); * * // Emit event * EventBus.emit('move-made', { move: moveObject }); */ class EventBus { constructor() { /** * @private * @property {Object>} events - Event listeners map */ this.events = {}; } /** * Subscribe to an event * * @param {string} eventName - Name of the event * @param {Function} callback - Callback function * @returns {Function} Unsubscribe function * * @example * const unsubscribe = EventBus.on('piece-moved', handleMove); * // Later... * unsubscribe(); // Remove listener */ on(eventName, callback) { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push(callback); // Return unsubscribe function return () => this.off(eventName, callback); } /** * Subscribe to an event once (auto-unsubscribe after first call) * * @param {string} eventName - Name of the event * @param {Function} callback - Callback function * @returns {Function} Unsubscribe function */ once(eventName, callback) { const onceWrapper = (...args) => { callback(...args); this.off(eventName, onceWrapper); }; return this.on(eventName, onceWrapper); } /** * Unsubscribe from an event * * @param {string} eventName - Name of the event * @param {Function} callback - Callback function to remove */ off(eventName, callback) { if (!this.events[eventName]) { return; } this.events[eventName] = this.events[eventName].filter( cb => cb !== callback ); // Clean up empty event arrays if (this.events[eventName].length === 0) { delete this.events[eventName]; } } /** * Emit an event to all subscribers * * @param {string} eventName - Name of the event * @param {*} data - Data to pass to listeners * * @example * EventBus.emit('game-over', { winner: 'white', reason: 'checkmate' }); */ emit(eventName, data) { if (!this.events[eventName]) { return; } this.events[eventName].forEach(callback => { try { callback(data); } catch (error) { console.error(`Error in event listener for '${eventName}':`, error); } }); } /** * Remove all listeners for an event, or all events if no name specified * * @param {string} [eventName] - Optional event name to clear */ clear(eventName) { if (eventName) { delete this.events[eventName]; } else { this.events = {}; } } /** * Get all event names that have listeners * * @returns {string[]} Array of event names */ getEventNames() { return Object.keys(this.events); } /** * Get listener count for an event * * @param {string} eventName - Name of the event * @returns {number} Number of listeners */ listenerCount(eventName) { return this.events[eventName] ? this.events[eventName].length : 0; } } // Export singleton instance export default new EventBus();