File size: 4,517 Bytes
9bd422a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
 * 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;