UNPKG

xtutils

Version:

Thuku's assorted general purpose typescript/javascript library.

276 lines 9.56 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EventEmitter = void 0; /** * Default max listener count */ let DEFAULT_MAX_LISTENERS = 10; /** * Private props `Symbol` key name */ const PROPS = Symbol(`__private_props_${Date.now()}__`); /** * @class EventEmitter */ class EventEmitter { /** * EventEmitter global max listeners * - warns if exceeded (helps find memory leaks) */ static get max_listeners() { return DEFAULT_MAX_LISTENERS; } static set max_listeners(value) { DEFAULT_MAX_LISTENERS = !isNaN(value = parseInt(value)) && Number.isInteger(value) && value >= 1 ? value : 10; } /** * Instance "private" props */ [PROPS] = {}; /** * Max listeners count (default: `undefiend` ~ `EventEmitter.max_listeners`) * - Accepts positive integer `number` * - Set to zero for unlimited */ get max_listeners() { return this[PROPS]._max_listeners; } set max_listeners(value) { this[PROPS]._max_listeners = !isNaN(value = parseInt(value)) && Number.isInteger(value) && value >= 0 ? value : EventEmitter.max_listeners; } /** * Create new instance */ constructor() { this[PROPS] = { _events: {}, _max_listeners: undefined, }; } /** * Get valid event type * * @param type - event type/name * @param throwable - enable throwing error when type is invalid * @returns `string` lowercase event name ~ `''` on error */ static type(type, throwable = false) { let _type = ''; if (!('string' === typeof type && (_type = type.trim()))) { const error = 'Invalid event type.'; console.warn(error, { type }); if (throwable) throw new TypeError(error); } return _type; } /** * Get valid event listener callback function. * * @param listener - event listener * @param throwable - enable throwing error when listener is invalid * @returns `(event:IEvent)=>void` event handler | `undefined` on error */ static listener(listener, throwable = false) { let _listener; if ('function' === typeof listener) _listener = listener; else { const error = 'Invalid event listener callback function.'; console.warn(error, { listener }); if (throwable) throw new TypeError(error); } return _listener; } /** * Get event listeners * * @param type - event type/name * @returns `((event:IEvent)=>void)[]` event handlers */ listeners(type) { const props = this[PROPS], listeners = []; if ((type = EventEmitter.type(type)) && props._events.hasOwnProperty(type)) { const listener = props._events[type]; if ('function' === typeof listener) listeners.push(listener); else if (Array.isArray(listener) && listener.length) { for (const val of listener) { if ('function' === typeof val) listeners.push(val); } } } return listeners; } /** * Check if event listener exists * * @param type - event type/name * @param listener - event listener * @returns `((event:IEvent)=>void)[]` event handlers */ hasListener(type, listener) { return this.listeners(type).findIndex(v => v === listener) > -1; } /** * Emit event * * @param type - event type/name * @param data - event data * @returns `boolean` caught */ emit(type, data) { const event = { type, data, time: Date.now() }; const listeners = this.listeners(type = EventEmitter.type(type, true)); if (listeners.length) { for (const fn of listeners) fn.call(this, event); return true; } else if (type === 'error') { if (data instanceof Error) throw data; const error = 'Uncaught, unspecified "error" event.'; console.warn(error, { event }); throw new Error(error); } return false; } /** * Add event listener ~ emits 'newListener' event if added (alias `emitter.addListener`) * * @param type - event type/name * @param listener - event listener callback function * @param once - one time callback * @returns `EventEmitter` - `this` instance */ on(type, listener, once = false) { return this.addListener(type, listener, once); } /** * Add one time event listener ~ emits 'newListener' event if added * * @param type - event type/name * @param listener - event listener callback function * @returns `EventEmitter` - `this` instance */ once(type, listener) { return this.addListener(type, listener, true); } /** * Add event subscriber * * @param type - event type/name * @param listener - event callback handler * @returns `(()=>void)` unsubscribe callback */ subscribe(type, listener) { type = EventEmitter.type(type, true); listener = EventEmitter.listener(listener, true); this.addListener(type, listener); return () => void this.removeListener(type, listener); } /** * Add event listener ~ emits 'newListener' event if added * * @param type - event type/name * @param listener - event listener callback function * @param once - one time callback * @returns `EventEmitter` - `this` instance */ addListener(type, listener, once = false) { type = EventEmitter.type(type, true); let handler = (listener = EventEmitter.listener(listener, true)); //one time listener if (once) { let fired = false; handler = (event) => { if (fired) return; fired = true; listener.call(this, event); this.removeListener(type, handler); }; } //Add event listener if new const listeners = this.listeners(type); if (listeners.findIndex(v => v === handler) < 0) { const props = this[PROPS]; // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (props._events.newListener) this.emit('newListener', { type, handler }); // Optimize the case of one listener. don't need the extra array object. props._events[type] = listeners.length ? [...listeners, handler] : handler; // Max listeners leak warning if (Array.isArray(props._events[type])) { const len = props._events[type].length; const max = 'number' === typeof props._max_listeners ? props._max_listeners : EventEmitter.max_listeners; if ('number' === typeof max && max > 0 && len > max) { console.error(`EventEmitter possible memory leak detected - ${len} "${type}" event listeners added, max count is ${max}.`); } } } else console.warn(`This "${type}" event listener is already added.`); //result return this; } /** * Remove event listener ~ emits 'removeListener' event if removed * * @param type - event type/name * @param listener - event listener callback function * @returns `EventEmitter` - `this` instance */ removeListener(type, listener) { if (!(type = EventEmitter.type(type))) return this; const props = this[PROPS]; if (!(props._events.hasOwnProperty(type))) return this; let removed = false; if (Array.isArray(props._events[type])) { const _listeners = props._events[type]; const index = _listeners.findIndex(v => v === listener); _listeners.splice(index, 1); removed = true; } else if (props._events[type] === listener) { delete props._events[type]; removed = true; } if (removed) { if (!this.listeners(type).length) delete props._events[type]; if (props._events.removeListener) this.emit('removeListener', { type, listener }); } return this; } /** * Remove all listeners ~ emits 'removeListener' event for each removed listener * * @param type - event type/name * @returns `EventEmitter` - `this` instance */ removeAllListeners(type) { if (!(type = EventEmitter.type(type))) return this; const props = this[PROPS]; if (!(props._events.hasOwnProperty(type))) return this; const emit_remove_listener = type !== 'removeListener' && props._events.removeListener; const listeners = emit_remove_listener ? this.listeners(type) : []; delete props._events[type]; if (emit_remove_listener && listeners.length) { for (const listener of listeners) this.emit('removeListener', { type, listener }); } return this; } } exports.EventEmitter = EventEmitter; //# sourceMappingURL=_EventEmitter.js.map