Spaces:
Running
Running
| /** | |
| * 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<string, Set<Function>>} */ | |
| 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; | |