model-explorer / js /core /eventBus.js
mr4's picture
Upload 71 files
9bd422a verified
/**
* 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;