/** * EventBus - Hệ Thống Sự Kiện Trung Tâm * Cung cấp giao tiếp dựa trên sự kiện giữa các thành phần * Requirements: 2.2, 11.4 */ const EventBus = (function () { // ─── Private State ──────────────────────────────────────────────────────── /** @type {Map>} */ const _listeners = new Map(); // ─── Private Helpers ────────────────────────────────────────────────────── /** * Validate event name. * @param {string} eventName */ function _validateEvent(eventName) { if (!eventName || typeof eventName !== 'string') { throw new TypeError('[EventBus] Event name must be a non-empty string'); } } // ─── Public API ─────────────────────────────────────────────────────────── return { /** * Subscribe to an event. * @param {string} eventName - Event name to listen for * @param {Function} callback - Handler called with event data * @returns {Function} Unsubscribe function */ on(eventName, callback) { _validateEvent(eventName); if (typeof callback !== 'function') { throw new TypeError('[EventBus] Callback must be a function'); } if (!_listeners.has(eventName)) { _listeners.set(eventName, new Set()); } _listeners.get(eventName).add(callback); // Return unsubscribe function return function off() { const set = _listeners.get(eventName); if (set) { set.delete(callback); if (set.size === 0) { _listeners.delete(eventName); } } }; }, /** * Unsubscribe a specific callback from an event. * @param {string} eventName * @param {Function} callback */ off(eventName, callback) { _validateEvent(eventName); const set = _listeners.get(eventName); if (set) { set.delete(callback); if (set.size === 0) { _listeners.delete(eventName); } } }, /** * Emit an event with optional data. * @param {string} eventName * @param {*} [data] */ emit(eventName, data) { _validateEvent(eventName); const set = _listeners.get(eventName); if (!set || set.size === 0) return; // Iterate over a copy to allow handlers to call off() safely const handlers = Array.from(set); for (const handler of handlers) { try { handler(data); } catch (err) { console.error(`[EventBus] Error in handler for "${eventName}":`, err); } } }, /** * Subscribe to an event for a single invocation, then auto-unsubscribe. * @param {string} eventName * @param {Function} callback * @returns {Function} Unsubscribe function */ once(eventName, callback) { const unsubscribe = this.on(eventName, (data) => { unsubscribe(); callback(data); }); return unsubscribe; }, /** * Remove all listeners for a specific event, or all events if no name given. * @param {string} [eventName] */ clear(eventName) { if (eventName) { _listeners.delete(eventName); } else { _listeners.clear(); } }, /** * Get the number of listeners for an event (useful for debugging/testing). * @param {string} eventName * @returns {number} */ listenerCount(eventName) { const set = _listeners.get(eventName); return set ? set.size : 0; } }; })(); // Export as global for browser usage window.EventBus = EventBus;