UNPKG

@irrelon/emitter

Version:

Simple JavaScript event emitter with some powerful features.

574 lines (573 loc) 23.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Emitter = exports.ListenerReturnFlag = void 0; exports.makeEmitter = makeEmitter; var ListenerReturnFlag; (function (ListenerReturnFlag) { ListenerReturnFlag["none"] = "!+_NONE"; ListenerReturnFlag["cancel"] = "!+_CANCEL"; })(ListenerReturnFlag || (exports.ListenerReturnFlag = ListenerReturnFlag = {})); /** * Creates a new class with the capability to emit events. */ class Emitter { constructor() { this._eventsEmitting = false; this._eventRemovalQueue = []; this._eventListeners = {}; this._eventStaticEmitters = {}; this._eventsAllowDefer = false; this._eventsDeferTimeouts = {}; } /** * Attach an event listener to the passed event only if the passed * id matches the id for the event being fired. * @param eventName The name of the event to listen for. * @param id The id to match against. * @param listener The method to call when the event is fired. * @returns The emitter instance. */ _on(eventName, id, listener) { const generateTimeout = (emitter) => { setTimeout(() => { listener(...emitter.args); }, 1); }; this._eventListeners = this._eventListeners || {}; const nonNullableEventListeners = (this._eventListeners[eventName] = this._eventListeners[eventName] || {}); nonNullableEventListeners[id] = nonNullableEventListeners[id] || []; nonNullableEventListeners[id].push(listener); // Check for any static emitters, and fire the event if any exist if (!this._eventStaticEmitters || !this._eventStaticEmitters[eventName] || !this._eventStaticEmitters[eventName].length) return this; // Emit events for each emitter for (let i = 0; i < this._eventStaticEmitters[eventName].length; i++) { const emitter = this._eventStaticEmitters[eventName][i]; if (id === "*" || emitter.id === id) { // Call the listener out of process so that any code that expects a listener // to be called at some point in the future rather than immediately on registration // will not fail generateTimeout(emitter); } } return this; } /** * Attach an event listener to the passed event only if the passed * id matches the document id for the event being fired. * @param eventName The name of the event to listen for. * @param id The id to match against. * @param listener The method to call when the event is fired. * @returns The emitter instance. */ _once(eventName, id, listener) { let fired = false; let originalReturnData; const internalCallback = (...args) => { if (fired) return originalReturnData; fired = true; this.off(eventName, id, internalCallback); originalReturnData = listener(...args); return originalReturnData; }; return this.on(eventName, id, internalCallback); } /** * Cancels an event listener based on an event name, id and listener function. * @param eventName The event to cancel listener for. * @param id The ID of the event to cancel listening for. * @param listener The event listener function used in the on() * or once() call to cancel. * @returns The emitter instance. */ _off(eventName, id, listener) { // If the event name doesn't have any listeners, exit early if (!this._eventListeners || !this._eventListeners[eventName] || !this._eventListeners[eventName][id]) return this; // If we are emitting events at the moment, don't remove this listener // until the process has completed, so we queue for removal instead if (this._eventsEmitting) { this._eventRemovalQueue = this._eventRemovalQueue || []; this._eventRemovalQueue.push(() => { this.off(eventName, id, listener); }); return this; } // Check if we have no specific listener... in this case // we want to remove all listeners for an id if (!listener) { if (id === "*") { // The id is "all" and no listener was provided so delete all // event listeners for this event name delete this._eventListeners[eventName]; return this; } // No listener provided, delete all listeners for this id delete this._eventListeners[eventName][id]; return this; } const arr = this._eventListeners[eventName][id] || []; const index = arr.indexOf(listener); if (index > -1) { arr.splice(index, 1); } return this; } /** * Creates an event listener that listens for any and all events * emitted. */ onAny(listener) { this.on("*", listener); } on(eventName, idOrListener, listener) { if (typeof idOrListener === "string") { return this._on(eventName, idOrListener, listener); } return this._on(eventName, "*", idOrListener); } once(eventName, ...rest) { const restTypes = rest.map((arg) => typeof arg); if (restTypes[0] === "function") { return this._once(eventName, "*", rest[0]); } return this._once(eventName, rest[0], rest[1]); } overwrite(eventName, ...rest) { const restTypes = rest.map((arg) => typeof arg); if (restTypes[0] === "function") { this.off(eventName); return this._on(eventName, "*", rest[0]); } this.off(eventName, rest[0]); return this._on(eventName, rest[0], rest[1]); } /** * Removes an event listener that listens for any and all events * emitted. */ offAny(listener) { this.off("*", listener); } off(eventName, ...rest) { if (rest.length === 0) { // Only event was provided, use * as the id to mean "any without" // a specific id return this._off(eventName, "*"); } const restTypes = rest.map((arg) => typeof arg); if (restTypes[0] === "function") { // The first arg after the event name was a function (listener) // so remove listening for events for this specific listener return this._off(eventName, "*", rest[0]); } // Both id and listener were provided, remove for the specific id return this._off(eventName, rest[0], rest[1]); } _emitToSingleListener(listener, data) { return listener(...data); } _emitToArrayOfListeners(arr, data) { const arrCount = arr.length; const resultArr = []; for (let arrIndex = 0; arrIndex < arrCount; arrIndex++) { // Check we have a function to execute const tmpFunc = arr[arrIndex]; if (typeof tmpFunc !== "function") { continue; } const eventListenerResponse = tmpFunc(...data); resultArr.push(eventListenerResponse); if (eventListenerResponse === ListenerReturnFlag.cancel) { // Cancel further event processing break; } } return resultArr; } /** * Call an event listener by name. The `rpc()` function differs from `emit()` in * that it will only call and return data from the first event listener for the * event rather than all event listeners. If you have more than one event listener * assigned to the event, all others except the first will be ignored and will * not receive the event. * @param eventName The name of the event to call the listener for. * @param {...any} data The arguments to send to the listening method. * If you are sending multiple arguments, separate them with a comma so * that they are received by the function as separate arguments. * @return {number} * @example #Call the Event Listener * // Emit the event named "hello" * myEntity.emit('hello'); * @example #Call the Event Listener With Data Object * // Emit the event named "hello" * myEntity.emit('hello', {moo: true}); * @example #Call the Event Listener With Multiple Data Values * // Emit the event named "hello" * myEntity.emit('hello', {moo: true}, 'someString'); * @example #Register the Listener for Event Data * // Set a listener to listen for the data (multiple values emitted * // from an event are passed as function arguments) * myEntity.on('hello', function (arg1, arg2) { * console.log("Arguments:", arg1, arg2); * return "foo"; * } * * // Emit the event named "hello" * const result = myEntity.emit('hello', 'data1', 'data2'); * console.log("Result:", result); * * // The console output is: * // Arguments: data1 data2 * // Result: "foo" */ rpc(eventName, ...data) { return this.rpcId(eventName, "*", ...data); } rpcId(eventName, id, ...data) { if (!this._eventListeners || !this._eventListeners[eventName]) { throw new Error("Cannot make an rpc call without at least one event listener and none were found"); } let emitterResult; this._eventsEmitting = true; if (this._eventListeners[eventName][id]) { // Handle id emit const arr = this._eventListeners[eventName][id]; emitterResult = this._emitToSingleListener(arr[0], data); } else if (id !== "*" && this._eventListeners[eventName]["*"]) { // Handle wildcard emit if the id passed wasn't already a wildcard (*) id const arr = this._eventListeners[eventName]["*"]; emitterResult = this._emitToSingleListener(arr[0], data); } else { if (id === "*") { throw new Error(`Cannot make an rpc call to event "${eventName}" without at least one event listener and none were found, register a listener via on("${eventName}", () => { ...some listener code })`); } throw new Error(`Cannot make an rpc call to event "${eventName}" and id "${id}" without at least one event listener and none were found, register a listener via on("${eventName}", "${id}", () => { ...some listener code })`); } this._eventsEmitting = false; this._processRemovalQueue(); return emitterResult; } /** * Emit an event by name. * @param eventName The name of the event to emit. * @param {...any} data The arguments to send to any listening methods. * If you are sending multiple arguments, separate them with a comma so * that they are received by the function as separate arguments. * @return {number} * @example #Emit an Event * // Emit the event named "hello" * myEntity.emit('hello'); * @example #Emit an Event With Data Object * // Emit the event named "hello" * myEntity.emit('hello', {moo: true}); * @example #Emit an Event With Multiple Data Values * // Emit the event named "hello" * myEntity.emit('hello', {moo: true}, 'someString'); * @example #Listen for Event Data * // Set a listener to listen for the data (multiple values emitted * // from an event are passed as function arguments) * myEntity.on('hello', function (arg1, arg2) { * console.log("Arguments:", arg1, arg2); * return "foo"; * } * * // Emit the event named "hello" * // The result is returned as an array of all return values * // from all the listeners for the event * const result = myEntity.emit('hello', 'data1', 'data2'); * console.log("Result:", result); * * // The console output is: * // Arguments: data1 data2 * // Result: ["foo"] */ emit(eventName, ...data) { return this.emitId(eventName, "*", ...data); } emitId(eventName, id, ...data) { if (!this._eventListeners || (!this._eventListeners[eventName] && !this._eventListeners["*"])) { return []; } let emitterResult = []; this._eventsEmitting = true; if (this._eventListeners[eventName]) { // Handle id emit if (this._eventListeners[eventName][id]) { const arr = this._eventListeners[eventName][id]; emitterResult = emitterResult.concat(this._emitToArrayOfListeners(arr, data)); } // Handle id wildcard emit if the id passed wasn't already a wildcard (*) id if (id !== "*" && this._eventListeners[eventName]["*"]) { const arr = this._eventListeners[eventName]["*"]; emitterResult = emitterResult.concat(this._emitToArrayOfListeners(arr, data)); } } if (this._eventListeners["*"]) { const arr = this._eventListeners["*"]["*"]; emitterResult = emitterResult.concat(this._emitToArrayOfListeners(arr, [eventName, ...data])); } this._eventsEmitting = false; this._processRemovalQueue(); return emitterResult; } /** * Creates a persistent emitter record that will fire a listener if * one is added for this event after the emitStatic() call has been * made. * @param eventName The name of the event to emit. * @param {...any} data Optional arguments to emit with the event. * @returns The emitter instance. * @private */ emitStatic(eventName, ...data) { const id = "*"; this._eventListeners = this._eventListeners || {}; this._eventsEmitting = true; if (this._eventListeners[eventName] && this._eventListeners[eventName][id]) { // Handle id wildcard emit const arr = this._eventListeners[eventName][id]; const arrCount = arr.length; for (let arrIndex = 0; arrIndex < arrCount; arrIndex++) { // Check we have a function to execute const tmpFunc = arr[arrIndex]; if (typeof tmpFunc === "function") { tmpFunc.call(this, ...data); } } } this._eventsEmitting = false; this._eventStaticEmitters = this._eventStaticEmitters || {}; this._eventStaticEmitters[eventName] = this._eventStaticEmitters[eventName] || []; this._eventStaticEmitters[eventName].push({ id: "*", args: data }); this._processRemovalQueue(); return this; } /** * Creates a persistent emitter record that will fire a listener if * one is added for this event after the emitStatic() call has been * made. * @param eventName The name of the event to emit. * @param id The id of the event to emit. * @param {...any} data Optional arguments to emit with the event. * @returns The emitter instance. * @private */ emitStaticId(eventName, id, ...data) { if (!id) throw new Error("Missing id from emitId call!"); this._eventListeners = this._eventListeners || {}; this._eventsEmitting = true; if (this._eventListeners[eventName]) { // Handle id emit if (this._eventListeners[eventName][id]) { const arr = this._eventListeners[eventName][id]; const arrCount = arr.length; for (let arrIndex = 0; arrIndex < arrCount; arrIndex++) { // Check we have a function to execute const tmpFunc = arr[arrIndex]; if (typeof tmpFunc === "function") { tmpFunc.call(this, ...data); } } } // Handle wildcard emit if (this._eventListeners[eventName]["*"]) { const arr = this._eventListeners[eventName]["*"]; const arrCount = arr.length; for (let arrIndex = 0; arrIndex < arrCount; arrIndex++) { // Check we have a function to execute const tmpFunc = arr[arrIndex]; if (typeof tmpFunc === "function") { tmpFunc.call(this, ...data); } } } } this._eventsEmitting = false; this._eventStaticEmitters = this._eventStaticEmitters || {}; this._eventStaticEmitters[eventName] = this._eventStaticEmitters[eventName] || []; this._eventStaticEmitters[eventName].push({ id, args: data }); this._processRemovalQueue(); return this; } /** * Handles removing emitters, is an internal method not called directly. * @param eventName The event to remove static emitter for. * @returns The emitter instance. * @private */ cancelStatic(eventName) { this._eventStaticEmitters = this._eventStaticEmitters || {}; this._eventStaticEmitters[eventName] = []; return this; } /** * Checks if an event has any event listeners or not. * @param eventName The name of the event to check for. * @returns True if one or more event listeners are registered for * the event. False if none are found. */ willEmit(eventName) { const id = "*"; if (!this._eventListeners || !this._eventListeners[eventName]) { return false; } const arr = this._eventListeners[eventName][id]; const arrCount = arr.length; for (let arrIndex = 0; arrIndex < arrCount; arrIndex++) { // Check we have a function to execute const tmpFunc = arr[arrIndex]; if (typeof tmpFunc === "function") { return true; } } return false; } /** * Checks if an event has any event listeners or not based on the passed id. * @param eventName The name of the event to check for. * @param id The event ID to check for. * @returns True if one or more event listeners are registered for * the event. False if none are found. */ willEmitId(eventName, id) { if (!this._eventListeners || !this._eventListeners[eventName]) { return false; } // Handle id emit if (this._eventListeners[eventName][id]) { const arr = this._eventListeners[eventName][id]; const arrCount = arr.length; for (let arrIndex = 0; arrIndex < arrCount; arrIndex++) { // Check we have a function to execute const tmpFunc = arr[arrIndex]; if (typeof tmpFunc === "function") { return true; } } } // Handle wildcard emit if (this._eventListeners[eventName]["*"]) { const arr = this._eventListeners[eventName]["*"]; const arrCount = arr.length; for (let arrIndex = 0; arrIndex < arrCount; arrIndex++) { // Check we have a function to execute const tmpFunc = arr[arrIndex]; if (typeof tmpFunc === "function") { return true; } } } return false; } /** * Queues an event to be fired. This has automatic de-bouncing so that any * events of the same type that occur within 100 milliseconds of a previous * one will all be wrapped into a single emit rather than emitting tons of * events for lots of chained inserts etc. Only the data from the last * de-bounced event will be emitted. * @param eventName The name of the event to emit. * @param {...any} data Optional arguments to emit with the event. * @returns The emitter instance. */ deferEmit(eventName, ...data) { if (!this._eventsAllowDefer) { // Check for an existing timeout this._eventsDeferTimeouts = this._eventsDeferTimeouts || {}; if (this._eventsDeferTimeouts[eventName]) { clearTimeout(this._eventsDeferTimeouts[eventName]); } // Set a timeout this._eventsDeferTimeouts[eventName] = setTimeout(() => { void this.emit.call(this, eventName, ...data); }, 1); } else { void this.emit.call(this, eventName, ...data); } return this; } /** * Returns true if the result from an emit() call passed as the first * argument contains a cancellation flag. * @param resultArr */ didCancel(resultArr) { return resultArr.findIndex((result) => result === ListenerReturnFlag.cancel) > -1; } /** * If events are cleared with the off() method while the event emitter is * actively processing any events then the off() calls get added to a * queue to be executed after the event emitter is finished. This stops * errors that might occur by potentially mutating the event listener array * while the emitter is running through them. This method is called after * the event emitter is finished processing. * @private */ _processRemovalQueue() { if (!this._eventRemovalQueue || !this._eventRemovalQueue.length) { return; } // Execute each removal call for (let i = 0; i < this._eventRemovalQueue.length; i++) { this._eventRemovalQueue[i](); } // Clear the removal queue this._eventRemovalQueue = []; } } exports.Emitter = Emitter; function makeEmitter(obj, prototypeMode) { let operateOnObject; if (obj === undefined && prototypeMode === undefined) { obj = {}; prototypeMode = false; } if (typeof obj === "boolean" && prototypeMode === undefined) { prototypeMode = obj; obj = {}; } if (prototypeMode === undefined) { prototypeMode = true; } if (typeof obj !== "object" && typeof obj !== "function") { throw new Error("Cannot operate on a non-object / non-function passed as first argument!"); } if (prototypeMode) { if (obj.prototype === undefined) { throw new Error("Cannot modify prototype of passed object, it has no prototype property! Was it instantiated with the new operator correctly?"); } operateOnObject = obj.prototype; } else { operateOnObject = obj; } // Convert the object prototype to have eventing capability operateOnObject.on = Emitter.prototype.on; operateOnObject.onAny = Emitter.prototype.onAny; operateOnObject.off = Emitter.prototype.off; operateOnObject.offAny = Emitter.prototype.offAny; operateOnObject.once = Emitter.prototype.once; operateOnObject.emit = Emitter.prototype.emit; operateOnObject.emitId = Emitter.prototype.emitId; operateOnObject.emitStatic = Emitter.prototype.emitStatic; operateOnObject.emitStaticId = Emitter.prototype.emitStaticId; operateOnObject.cancelStatic = Emitter.prototype.cancelStatic; operateOnObject.deferEmit = Emitter.prototype.deferEmit; operateOnObject.willEmit = Emitter.prototype.willEmit; operateOnObject.willEmitId = Emitter.prototype.willEmitId; operateOnObject._processRemovalQueue = Emitter.prototype._processRemovalQueue; return obj; }