/** * EventBus.js * Simple publish/subscribe event system for decoupled communication * between game systems. */ class EventBus { constructor() { /** @type {Map>} */ this._listeners = new Map(); } /** * Subscribe to an event. * @param {string} event * @param {Function} fn * @returns {Function} Unsubscribe function. */ on(event, fn) { if (!this._listeners.has(event)) { this._listeners.set(event, []); } const entry = { fn, once: false }; this._listeners.get(event).push(entry); return () => this.off(event, fn); } /** * Subscribe to an event, but only fire once. * @param {string} event * @param {Function} fn */ once(event, fn) { if (!this._listeners.has(event)) { this._listeners.set(event, []); } this._listeners.get(event).push({ fn, once: true }); } /** * Unsubscribe from an event. * @param {string} event * @param {Function} fn */ off(event, fn) { const list = this._listeners.get(event); if (!list) return; const idx = list.findIndex((entry) => entry.fn === fn); if (idx !== -1) list.splice(idx, 1); } /** * Emit an event with optional data. * @param {string} event * @param {*} [data] */ emit(event, data) { const list = this._listeners.get(event); if (!list || list.length === 0) return; // Iterate in reverse so we can safely remove "once" entries for (let i = list.length - 1; i >= 0; i--) { const entry = list[i]; entry.fn(data); if (entry.once) { list.splice(i, 1); } } } /** * Remove all listeners for a specific event, or all events. * @param {string} [event] */ clear(event) { if (event) { this._listeners.delete(event); } else { this._listeners.clear(); } } } module.exports = EventBus;