@creejs/commons-events
Version:
Commons EventEmitter
871 lines (793 loc) • 27.9 kB
JavaScript
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
}
}
export { EventEmitter, EventEmitter as default };
//# sourceMappingURL=index-dev.js.map