UNPKG

@creejs/commons-events

Version:
882 lines (801 loc) 29.9 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.CommonsLang = {})); })(this, (function (exports) { 'use strict'; var u={isFunction:c,isNil:y};function c(t){return "function"==typeof t}function y(t){return null==t}function m(t){return null!=t&&"number"==typeof t}function N(t){return null!=t&&"string"==typeof t}function $(t){return null!=t&&"symbol"==typeof t}var C={assertNumber:q,assertFunction:W,assertNotNil:V,assertString:M,assertStringOrSymbol:function(t,r){if(!N(t)&&!$(t))throw new Error(`${r?'"'+r+'" ':""}Not String or Symbol: type=${typeof t} value=${Z(t)}`)}};function M(t,r){if(!N(t))throw new Error(`${r?'"'+r+'" ':""}Not String: type=${typeof t} value=${Z(t)}`)}function q(t,r){if(!m(t))throw new Error(`${r?'"'+r+'" ':""}Not Number: type=${typeof t} value=${Z(t)}`)}function W(t,r){if(!c(t))throw new Error(`${r?'"'+r+'" ':""}Not Function: type=${typeof t} value=${Z(t)}`)}function V(t,r){if(y(t))throw new Error((r?'"'+r+'" ':"")+"Should Not Nil")}function Z(t){if(null===t)return "null";if(void 0===t)return "undefined";let r;try{r=JSON.stringify(t);}catch(e){r=t.toString();}return r}new TextDecoder;new TextEncoder; const DefaultOwner = 'DOwner$#$'; // 3rd // internal // module vars const { assertFunction: assertFunction$2, assertNotNil: assertNotNil$1 } = C; /** * Wraps a function to be called when an event is fired. * @typedef {import('./event.js').default} Event * @class Listener */ class Listener { /** * @param {Event} event * @param {function} callback - The function to be called when event is fired * @param {boolean} [isOnce=false] - is a one time listener? */ constructor (event, callback, isOnce = false) { assertNotNil$1(event, 'event'); assertFunction$2(callback, 'callback'); this._event = event; this._callback = callback; this._isOnce = !!isOnce; // is Once Listener? this._owner = undefined; } /** * Sets the owner of this listener. * @param {*} owner - The owner object to be associated with this listener. */ set owner (owner) { this._owner = owner; } get owner () { return this._owner === DefaultOwner ? undefined : this._owner } get event () { return this._event } get isOnce () { return this._isOnce } /** * Checks if the provided function is the same as the listener's wrapped function. * @param {Function} callback - The function to compare against. * @returns {boolean} True if the functions are the same, false otherwise. */ isSameCallback (callback) { return this._callback === callback } get callback () { return this._callback } /** * Invokes the stored function with the provided arguments. * @param {...*} args - Arguments to pass to the function. * @returns {*} The result of the function invocation. */ invoke (...args) { try { return this._callback(...args) } finally { if (this._isOnce) { try { this._event._remove(this); } catch (err) { // do nothing console.warn(err); } } } } /** * Invokes the listener with the provided arguments. * Alias for {@linkcode Listener.invoke} * @param {...*} args - Arguments to be passed to the listener. * @returns {*} The result of the listener invocation. */ listener (...args) { return this.invoke(...args) } } // 3rd // internal // module vars const { isFunction, isNil: isNil$1 } = u; const { assertStringOrSymbol: assertStringOrSymbol$1, assertFunction: assertFunction$1 } = C; /** * An Event definition * 1. listeners are grouped by owner * * if not specified, group it into one Default owner * 2. one listener may belong to multiple owners * 3. one owner may have multiple listeners */ class Event { static get DefaultOwner () { return DefaultOwner } /** * Creates a new Event instance with the specified name. * @param {string|Symbol} eventName - The name of the event. */ constructor (eventName) { assertStringOrSymbol$1(eventName, 'eventName'); this._name = eventName; /** * use Set to store unique listeners * @type {Set<function>} */ this._callbacks = new Set(); /** * @type {Array<Listener>} */ this._listeners = []; // user array to keep order /** * @type {Map<function, Set<Listener>>} */ this._callback2Listeners = new Map(); /** * use listener to index owners. One Listener Instance only belongs to one owner * @type {Map<Listener, *>} */ this._listener2Owner = new Map(); /** * use "owner" to group listeners; one owner may have multiple listeners * @type {Map<*, Set<Listener>>} */ this._owner2Listeners = new Map(); } /** * Name of the event. * @returns {string|Symbol} */ get name () { return this._name } isEmpty () { return this._callbacks.size === 0 } /** * Returns a copy of the raw listeners array. * @returns {Array<Listener>} A shallow copy of the listeners array. */ rawListeners () { return [...this._listeners] } /** * Returns the number of listeners listening for the event. * If callback is provided, it will return how many times the callback is found in the list of the listeners * @param {Function} [callback] - The callback function to count * @returns {number} */ listenerCount (callback) { if (callback == null) { return this._listeners.length } return this._callback2Listeners.get(callback)?.size ?? 0 } /** * Returns a shallow copy of the registered callbacks array. * if one callback function is added multiple times, it will be returned multiple times. * @returns {Array<function>} A new array containing all registered callbacks. */ callbacks () { return [...this.rawListeners().map(listener => listener.callback)] } /** * Emits current event, invoking all registered listeners by order. * @param {...*} args - Arguments to be passed to each listener. * @returns {boolean} True if the event had listeners, false otherwise. */ emit (...args) { if (this._listeners.length === 0) { return false } // Clone _listeners, it may be changed during call listener for (const listener of [...this._listeners]) { listener.invoke(...args); } return true } /** * Emits current event, invoking all registered listeners by order. * @param {string|Symbol} owner * @param {...*} args - Arguments to be passed to each listener. * @returns {boolean} True if the event had listeners, false otherwise. */ emitOwner (owner, ...args) { if (this._listeners.length === 0) { return false } const listeners = this._owner2Listeners.get(owner); if (listeners == null) { return false } // Clone _listeners, it may be changed during call listener for (const listener of [...listeners]) { listener.invoke(...args); } return true } /** * Checks if listener is registered with this event. * @param {function} callback - The listener function to check. * @returns {boolean} True if the listener is registered, false otherwise. */ hasListener (callback) { if (!isFunction(callback)) { return false } return this._callbacks.has(callback) } /** * Checks if owner has any registered listeners. * @param {*} owner - The owner to check for registered listeners. * @returns {boolean} True if the owner has listeners, false otherwise. */ hasOwner (owner) { if (isNil$1(owner)) { return false } return this._owner2Listeners.has(owner) } /** * Adds an event listener * @param {function} callback - The callback function to be invoked when the event occurs. * @param {*} [owner] - use "owner" to group listeners, then can remove all listeners for an owner. * @returns {boolean} true if added, false otherwise. */ addListener (callback, owner) { return this._addListener(callback, owner, false, false) } /** * Prepends a listener callback to the beginning of the listeners array. * No checks are made to see if the listener has already been added. * Multiple calls passing the same combination of eventName and listener will result in the listener being added, * and called, multiple times. * @param {Function} callback - The callback function to be executed when the event is emitted. * @param {Object} owner - The owner object to which the listener belongs. * @returns {boolean} */ prependListener (callback, owner) { return this._addListener(callback, owner, false, true) } /** * Adds a one-time listener for the event. The listener is automatically removed after being invoked once. * @param {Function} callback - The function to call when the event is triggered. * @param {Object} [owner] - The object that owns the callback (used for binding `this` context). * @returns {boolean} */ addOnceListener (callback, owner) { return this._addListener(callback, owner, true, false) } /** * Adds a one-time event listener that will be automatically removed after being triggered once. * The listener will only be triggered if the event is pretended (simulated). * @param {Function} callback - The function to call when the event is triggered. * @param {Object} owner - The object that owns the listener (used for reference tracking). * @returns {boolean} The listener function that was added. */ prependOnceListener (callback, owner) { return this._addListener(callback, owner, true, true) } /** * Adds a listener for the event. * @param {Function} callback - The callback function to be executed when the event occurs * @param {*} [owner] - The owner object that the listener belongs to (defaults to DeaultOwner) * @param {boolean} [isOnce=false] - Whether the listener should be removed after first invocation * @param {boolean} [isPrepend=false] - Whether the listener should be inert at the beginning of the listeners array * @returns {boolean} Returns true if listener was added successfully, false if callback is nil or duplicate * @protected */ _addListener (callback, owner, isOnce, isPrepend) { if (isNil$1(callback)) { return false } assertFunction$1(callback); if (!this._callbacks.has(callback)) { this._callbacks.add(callback); } // a listener must belong to an owner owner = owner ?? DefaultOwner; // use Listener to wrap callback const listener = new Listener(this, callback, isOnce); listener.owner = owner; if (isPrepend) { this._listeners.unshift(listener); } else { this._listeners.push(listener); } // index, rapadly find one listener's owner this._listener2Owner.set(listener, owner); // one callback function may be registered many times let callbackListeners = this._callback2Listeners.get(callback); if (callbackListeners == null) { callbackListeners = new Set(); this._callback2Listeners.set(callback, callbackListeners); } callbackListeners.add(listener); // group by owner let ownerListeners = this._owner2Listeners.get(owner); if (ownerListeners == null) { ownerListeners = new Set(); this._owner2Listeners.set(owner, ownerListeners); } ownerListeners.add(listener); return true } /** * Removes a callback * @param {Function} callback - The callback function to remove. * @returns {boolean} true if removed, false otherwise. */ removeListener (callback) { if (isNil$1(callback)) { return false } if (!this._callbacks.has(callback)) { return false } this._callbacks.delete(callback); const listeners = this._callback2Listeners.get(callback); if (listeners == null) { // should not happen return false } this._callback2Listeners.delete(callback); for (const listener of listeners) { // remove from global index const index = this._listeners.indexOf(listener); index !== -1 && this._listeners.splice(this._listeners.indexOf(listener), 1); // remove from owner index const owner = this._listener2Owner.get(listener); if (owner == null) { continue } this._listener2Owner.delete(listener); const ownerListeners = this._owner2Listeners.get(owner); if (ownerListeners == null) { continue } ownerListeners.delete(listener); if (ownerListeners.size === 0) { this._owner2Listeners.delete(owner); } } return true } /** * Removes a listener from both global and owner indexes. * @param {Listener} listener - The listener function to remove */ _remove (listener) { // remove from global index const index = this._listeners.indexOf(listener); index !== -1 && this._listeners.splice(index, 1); // clean callback index const { callback } = listener; const callbackListeners = this._callback2Listeners.get(callback); if (callbackListeners != null) { callbackListeners.delete(listener); if (callbackListeners.size === 0) { this._callback2Listeners.delete(callback); this._callbacks.delete(callback); } } // remove from owner index const owner = this._listener2Owner.get(listener); if (owner == null) { return } this._listener2Owner.delete(listener); const ownerListeners = this._owner2Listeners.get(owner); if (ownerListeners == null) { return } ownerListeners.delete(listener); if (ownerListeners.size === 0) { this._owner2Listeners.delete(owner); } } /** * Removes all event listeners * @param {*} [owner] - Without owner, all listeners will be removed; otherwise, only listeners of "owner" will be removed. * @returns {this} */ removeAllListeners (owner) { // remove listeners of owner if (isNil$1(owner)) { // remove all Listeners this._callbacks.clear(); this._listeners.length = 0; this._callback2Listeners.clear(); this._listener2Owner.clear(); this._owner2Listeners.clear(); return this } // all listeners of the owner const ownerListeners = this._owner2Listeners.get(owner); // no owner if (ownerListeners == null) { return this } // clear owner index this._owner2Listeners.delete(owner); // clearn listeners of owner one by one for (const listener of ownerListeners) { // remove from global index const index = this._listeners.indexOf(listener); index !== -1 && this._listeners.splice(this._listeners.indexOf(listener), 1); // clean listener-owner index this._listener2Owner.delete(listener); // one callback function may be registered many times, has many Listeners const { callback } = listener; const callbackListeners = this._callback2Listeners.get(callback); if (callbackListeners == null) { continue } callbackListeners.delete(listener); if (callbackListeners.size === 0) { this._callback2Listeners.delete(callback); this._callbacks.delete(callback); } } return this } } // 3rd // internal /** * @typedef {{ * on(eventName:string, listener:function, owner?:any): EventEmitterLike, * addListener(eventName:string, listener:function, owner?:any): EventEmitterLike, * off(eventName:string, listener:function): EventEmitterLike, * removeListener(eventName:string, listener:function): EventEmitterLike, * offAll(eventName:string, owner:any): EventEmitterLike, * offOwner(owner:any): EventEmitterLike, * removeAllListeners(eventName:string): EventEmitterLike, * once(eventName:string, listener:function, owner?:any): EventEmitterLike, * emit(eventName:string, ...args:any[]): EventEmitterLike, * emitOwner(eventName:string, owner:any, ...args:any[]): EventEmitterLike, * }} EventEmitterLike */ // module vars const { isNil } = u; const { assertString, assertFunction, assertNumber, assertStringOrSymbol, assertNotNil } = C; /** * methods allowed to mixin other objects */ const MixinMethods = [ 'on', 'once', 'addListener', 'prependListener', 'prependOnceListener', 'off', 'offAll', 'offOwner', 'removeAllListeners', 'removeListener', 'emit', 'emitOwner', 'setMaxListeners', 'getMaxListeners', 'hasOwner', 'listeners', 'listenerCount', 'eventNames', 'rawListeners' ]; let DefaultMaxListeners = 10; /** * 1. An EventEmitter follows the API of NodeJS EventEmitter. * 2. Enhancement: * * Listeners are grouped by "owner". * * Operation via "owner" * * Duplicate listeners are filtered out. Only unique One kept. */ class EventEmitter { /** * Mixes EventEmitter methods into the given object. * @template T * @param {T} obj - The target object to mix methods into. * @returns {T} The modified object with EventEmitter methods. */ static mixin (obj) { const emitter = new EventEmitter(); // @ts-ignore obj.__emitter = emitter; for (const name of MixinMethods) { // @ts-ignore const method = emitter[name]; // @ts-ignore obj[name] = method.bind(emitter); } return obj } static get defaultMaxListeners () { return DefaultMaxListeners } static set defaultMaxListeners (maxListeners) { assertNumber(maxListeners); DefaultMaxListeners = maxListeners ?? 10; } /** */ constructor () { /** * @type {Map<string|Symbol, Event>} */ this._name2Event = new Map(); this._maxListeners = DefaultMaxListeners; } /** * Alias of {@linkcode EventEmitter.addListener} * @param {string|Symbol} eventName * @param {function} listener * @param {*} [owner] * @returns {this} */ addListener (eventName, listener, owner) { return this.on(eventName, listener, owner) } /** * Adds the listener function to the beginning of the listeners array for the event named eventName. * @param {string|Symbol} eventName * @param {function} listener * @param {*} [owner] * @returns {this} */ prependListener (eventName, listener, owner) { assertString(eventName); assertFunction(listener); this._checkMaxListeners(eventName); const event = this._getOrCreateEvent(eventName); event.prependListener(listener, owner); return this } /** * Adds a one-time listener function for the event named eventName to the beginning of the listeners array. * @param {string|Symbol} eventName * @param {function} listener * @param {*} [owner] * @returns {this} */ prependOnceListener (eventName, listener, owner) { assertString(eventName); assertFunction(listener); this._checkMaxListeners(eventName); const event = this._getOrCreateEvent(eventName); event.prependOnceListener(listener, owner); return this } /** * Synchronously calls each of the listeners registered for the event named eventName, * in the order they were registered, passing the supplied arguments to each. * Returns true if the event had listeners, false otherwise. * @param {string|Symbol} eventName - The name of the event. * @param {...*} args arguments to pass to the listeners. * @returns {boolean} */ emit (eventName, ...args) { const event = this._name2Event.get(eventName); if (event == null || event.isEmpty()) { return false } event.emit(...args); return true } /** * Emits an event To the specified owner. * @param {string} eventName - The name of the event to emit. * @param {any} owner - The owner of listeners * @param {...*} args - Additional arguments to pass with the event. */ emitOwner (eventName, owner, ...args) { if (owner == null) { throw new Error('Missing "owner"') } const event = this._name2Event.get(eventName); if (event == null || event.isEmpty()) { return false } event.emitOwner(owner, ...args); return true } /** * Returns an array listing the events for which the emitter has registered listeners. * @returns {Array<string|Symbol>} An array of event names. */ eventNames () { return [...this._name2Event.keys()] } getMaxListeners () { return this._maxListeners } /** * Returns the number of listeners listening for the event named eventName. * If listener is provided, it will return how many times the listener is found * in the list of the listeners of the event. * @param {string|Symbol} eventName - The name of the event to check * @param {function} [listener] - Optional specific listener to count * @returns {number} The number of listeners for the event */ listenerCount (eventName, listener) { assertStringOrSymbol(eventName, 'eventName'); const event = this._name2Event.get(eventName); if (event == null || event.isEmpty()) { return 0 } return event.listenerCount(listener) } /** * Returns a copy of the array of listeners for the event named eventName. * 1. if a callback function added multiple times, it will be returned multiple times. * @param {string} eventName * @returns {Array<function>} */ listeners (eventName) { assertStringOrSymbol(eventName, 'eventName'); const event = this._name2Event.get(eventName); if (event == null || event.isEmpty()) { return [] } return event.callbacks() } /** * Removes a callback function from a specific event. * 1. If the event doesn't exist or has no listeners, do nothing * 2. if one callback function added multiple times, all of them will be removed. * * !!! This is different from the behavior of Node.js EventEmitter. * * @param {string} eventName - The name of the event to remove callback from * @param {function} callback - The callback function to remove * @returns {EventEmitter} Returns the emitter instance for chaining */ off (eventName, callback) { const event = this._name2Event.get(eventName); if (event == null) { return this } event.removeListener(callback); if (event.isEmpty()) { this._name2Event.delete(eventName); return this } return this } /** * Removes all listeners for the specified event. * if owner is limited, only the listeners of the owner will be removed. * * @param {string|Symbol} eventName - The name of the event to clear listeners for. * @param {*} owner - The owner whose listeners should be removed. * @returns {EventEmitter} The emitter instance for chaining. */ offAll (eventName, owner) { assertStringOrSymbol(eventName, 'eventName'); const event = this._name2Event.get(eventName); if (event == null) { return this } event.removeAllListeners(owner); if (event.isEmpty()) { this._name2Event.delete(eventName); return this } return this } /** * Removes all event listeners belonging to the specified owner. * @param {*} owner - The owner whose listeners should be removed. * @returns {this} */ offOwner (owner) { assertNotNil(owner, 'owner'); const events = [...this._name2Event.values()]; for (const event of events) { event.removeAllListeners(owner); if (event.isEmpty()) { this._name2Event.delete(event.name); } } return this } /** * Adds the listener function to the end of the listeners array for the event named eventName. * @param {string|Symbol} eventName * @param {function} callback * @param {*} [owner] * @returns {this} */ on (eventName, callback, owner) { assertString(eventName); assertFunction(callback); this._checkMaxListeners(eventName); const event = this._getOrCreateEvent(eventName); event.addListener(callback, owner); return this } /** * Checks if the number of listeners for the given event exceeds the maximum allowed. * Emits a warning if the listener count reaches or exceeds the maxListeners threshold. * @private * @param {string|Symbol} eventName - The name of the event to check * @returns {void} */ _checkMaxListeners (eventName) { let listenerCount = 0; if (this._maxListeners !== 0 && this._maxListeners !== Infinity && (listenerCount = this.listenerCount(eventName)) >= this._maxListeners) { console.warn(`maxlistenersexceededwarning: Possible EventEmitter memory leak detected. ${listenerCount} ${eventName} listeners added to [${this}]. Use emitter.setMaxListeners() to increase limit`); } } /** * Adds a listener that will be invoked only once for the event named eventName.SIGINT listeners added to [process]. Use emitter.setMaxListeners() to increase limit`) } } /** * Adds a one-time callback function for the specified event. * The callback is invoked only once and then automatically removed. * * @param {string} eventName - The name of the event to listen for. * @param {Function} callback - The callback function to execute when the event is emitted. * @param {*} [owner] - Optional owner used to group listener callbacks. * @returns {EventEmitter} Returns the emitter instance for chaining. */ once (eventName, callback, owner) { assertString(eventName); assertFunction(callback); const event = this._getOrCreateEvent(eventName); event.addOnceListener(callback, owner); return this } /** * Returns a copy of the array of listeners for the event named eventName, * including any wrappers (such as those created by .once()). * @param {string} eventName - The name of the event. * @returns {Listener[]} An array of raw listener functions, or empty array if none exist. */ rawListeners (eventName) { return this._name2Event.get(eventName)?.rawListeners() || [] } /** * Alias of {@linkcode EventEmitter.offAll} * @param {string|Symbol} eventName - The name of the event to remove listeners for. * @param {*} owner - The owner of the listeners to be removed. * @returns {EventEmitter} The emitter instance for chaining. */ removeAllListeners (eventName, owner) { return this.offAll(eventName, owner) } /** * Alias of {@linkcode EventEmitter.off} * @param {string} eventName - The name of the event. * @param {Function} callback - The callback function to remove. * @returns {EventEmitter} The emitter instance for chaining. */ removeListener (eventName, callback) { return this.off(eventName, callback) } /** * Sets the maximum number of listeners that can be added to this event emitter. * @param {number} max - The maximum number of listeners. * @returns {this} The set maximum number of listeners. */ setMaxListeners (max) { assertNumber(max); if (max < 0) { throw new RangeError('maxListeners must >=0') } this._maxListeners = max; return this } /** * Gets an existing event by name or creates a new one if it doesn't exist. * @private * @param {string|Symbol} eventName - The name of the event to get or create. * @returns {Event} The existing or newly created Event instance. */ _getOrCreateEvent (eventName) { if (this._name2Event.has(eventName)) { // @ts-ignore return this._name2Event.get(eventName) } const event = new Event(eventName); this._name2Event.set(eventName, event); return event } /** * Checks if the specified owner has any registered events. * @param {Object} owner - The owner object to check for registered events. * @returns {boolean} True if the owner has any registered events, false otherwise. */ hasOwner (owner) { if (isNil(owner)) { return false } for (const event of this._name2Event.values()) { if (event.hasOwner(owner)) { return true } } return false } } exports.EventEmitter = EventEmitter; exports.default = EventEmitter; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=index.dev.js.map