UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

389 lines (346 loc) 12.7 kB
'use strict'; /** * A generic event listener callback function. * * @callback handler * @param {...any} payload - The data payload passed when the event is triggered. * @returns {void} */ /** * TinyEvents provides a minimalistic event emitter system similar to Node.js's EventEmitter, * enabling components to subscribe to, emit, and manage events and their listeners. * * Features include: * - Adding/removing event listeners (`on`, `off`, `offAll`, `offAllTypes`) * - One-time listeners (`once`) * - Emitting events (`emit`) * - Listener inspection and limits (`listenerCount`, `listeners`, `eventNames`) * - Maximum listener control (`setMaxListeners`, `getMaxListeners`) * * This class is useful for lightweight, dependency-free publish/subscribe event handling * within modular JavaScript applications. * * @class */ class TinyEvents { /** @type {Map<string, { handler: handler; config: { once: boolean } }[]>} */ #listeners = new Map(); /** @type {number} */ #maxListeners = 10; /** @type {boolean} */ #throwMaxListeners = false; /** * Normalizes the event parameter into an array of strings. * * @param {string|string[]} event * @param {string} method * @returns {string[]} */ #normalizeEvents(event, method) { if (typeof event === 'string') return [event]; if (Array.isArray(event) && event.every((e) => typeof e === 'string')) return event; throw new TypeError(`${method}(event): event must be a string or string[]`); } /** * Enables or disables throwing an error when the maximum number of listeners is exceeded. * * @param {boolean} shouldThrow - If true, an error will be thrown when the max is exceeded. */ setThrowOnMaxListeners(shouldThrow) { if (typeof shouldThrow !== 'boolean') throw new TypeError('setThrowOnMaxListeners(value): value must be a boolean'); this.#throwMaxListeners = shouldThrow; } /** * Checks whether an error will be thrown when the max listener limit is exceeded. * * @returns {boolean} True if an error will be thrown, false if only a warning is shown. */ getThrowOnMaxListeners() { return this.#throwMaxListeners; } /////////////////////////////////////////////////// /** * Internal method to prepend a listener with options. * * @param {string} event - Event name. * @param {handler} handler - The callback function. * @param {Object} [settings={}] - Optional settings. * @param {boolean} [settings.once=false] - If the listener should be executed once. */ #prepend(event, handler, { once = false } = {}) { let eventData = this.#listeners.get(event); if (!Array.isArray(eventData)) { eventData = []; this.#listeners.set(event, eventData); } eventData.unshift({ handler, config: { once } }); const max = this.#maxListeners; if (max > 0 && eventData.length > max) { const warnMessage = `Possible memory leak detected. ${eventData.length} "${event}" listeners added. ` + `Use setMaxListeners() to increase limit.`; if (!this.#throwMaxListeners) console.warn(warnMessage); else throw new Error(warnMessage); } } /** * Adds a listener to the beginning of the listeners array for the specified event. * * @param {string|string[]} event - Event name. * @param {handler} handler - The callback function. */ prependListener(event, handler) { const events = this.#normalizeEvents(event, 'prependListener'); if (typeof handler !== 'function') throw new TypeError('prependListener(event, handler): handler must be a function'); for (const ev of events) this.#prepend(ev, handler); } /** * Adds a one-time listener to the beginning of the listeners array for the specified event. * * @param {string|string[]} event - Event name. * @param {handler} handler - The callback function. * @returns {handler[]} - The wrapped handler used internally. */ prependListenerOnce(event, handler) { const events = this.#normalizeEvents(event, 'prependListenerOnce'); if (typeof handler !== 'function') throw new TypeError('prependListenerOnce(event, handler): handler must be a function'); const wrappedHandlers = []; for (const ev of events) { /** @type {handler} */ const wrapped = (...args) => { this.off(ev, wrapped); handler(...args); }; this.#prepend(ev, wrapped, { once: true }); wrappedHandlers.push(wrapped); } return wrappedHandlers; } //////////////////////////////////////////////////////////// /** * Adds a event listener. * * @param {string|string[]} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'. * @param {handler} handler - Callback function to be called when event fires. * @param {Object} [settings={}] - Optional settings. * @param {boolean} [settings.once=false] - This is a once event. */ #on(event, handler, { once = false } = {}) { const events = this.#normalizeEvents(event, 'on'); for (const ev of events) { let eventData = this.#listeners.get(ev); if (!Array.isArray(eventData)) { eventData = []; this.#listeners.set(ev, eventData); } eventData.push({ handler, config: { once } }); // Warn if listener count exceeds the max allowed const max = this.#maxListeners; if (max > 0 && eventData.length > max) { const warnMessage = `Possible memory leak detected. ${eventData.length} "${ev}" listeners added. ` + `Use setMaxListeners() to increase limit.`; if (!this.#throwMaxListeners) console.warn(warnMessage); else throw new Error(warnMessage); } } } /** * Adds a event listener. * * @param {string|string[]} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'. * @param {handler} handler - Callback function to be called when event fires. */ on(event, handler) { if (typeof handler !== 'function') throw new TypeError('on(event, handler): handler must be a function'); return this.#on(event, handler); } /** * Registers an event listener that runs only once, then is removed. * * @param {string|string[]} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'. * @param {handler} handler - The callback function to run on event. * @returns {handler[]} - The wrapped version of the handler. */ once(event, handler) { const events = this.#normalizeEvents(event, 'once'); if (typeof handler !== 'function') throw new TypeError('once(event, handler): handler must be a function'); const wrappedHandlers = []; for (const ev of events) { /** @type {handler} */ const wrapped = (...args) => { this.off(ev, wrapped); handler(...args); }; this.#on(ev, wrapped, { once: true }); wrappedHandlers.push(wrapped); } return wrappedHandlers; } /** * Adds a event listener. * * @param {string|string[]} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'. * @param {handler} handler - Callback function to be called when event fires. */ appendListener(event, handler) { return this.on(event, handler); } /** * Registers an event listener that runs only once, then is removed. * * @param {string|string[]} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'. * @param {handler} handler - The callback function to run on event. * @returns {handler[]} - The wrapped version of the handler. */ appendListenerOnce(event, handler) { return this.once(event, handler); } /////////////////////////////////////////////// /** * Removes a previously registered event listener. * * @param {string|string[]} event - The name of the event to remove the handler from. * @param {handler} handler - The specific callback function to remove. */ off(event, handler) { const events = this.#normalizeEvents(event, 'off'); if (typeof handler !== 'function') throw new TypeError('off(event, handler): handler must be a function'); for (const ev of events) { const listeners = this.#listeners.get(ev); if (!Array.isArray(listeners)) continue; const index = listeners.findIndex((listener) => listener.handler === handler); if (index !== -1) listeners.splice(index, 1); // Optionally clean up empty arrays (optional) if (listeners.length === 0) this.#listeners.delete(ev); } } /** * Removes all event listeners of a specific type from the element. * * @param {string|string[]} event - The event type to remove (e.g. 'onScrollBoundary'). */ offAll(event) { const events = this.#normalizeEvents(event, 'offAll'); for (const ev of events) this.#listeners.delete(ev); } /** * Removes all event listeners of all types from the element. */ offAllTypes() { this.#listeners.clear(); } ///////////////////////////////////////////// /** * Returns the number of listeners for a given event. * * @param {string} event - The name of the event. * @returns {number} Number of listeners for the event. */ listenerCount(event) { if (typeof event !== 'string') throw new TypeError('listenerCount(event): event name must be a string'); const listeners = this.#listeners.get(event); return Array.isArray(listeners) ? listeners.length : 0; } /** * Returns a copy of the array of listeners for the specified event. * * @param {string} event - The name of the event. * @returns {handler[]} Array of listener functions. */ listeners(event) { if (typeof event !== 'string') throw new TypeError('listeners(event): event name must be a string'); const listeners = this.#listeners.get(event); return Array.isArray(listeners) ? [...listeners] .filter((listener) => !listener.config.once) .map((listener) => listener.handler) : []; } /** * Returns a copy of the array of listeners for the specified event. * * @param {string} event - The name of the event. * @returns {handler[]} Array of listener functions. */ onceListeners(event) { if (typeof event !== 'string') throw new TypeError('onceListeners(event): event name must be a string'); const listeners = this.#listeners.get(event); return Array.isArray(listeners) ? [...listeners] .filter((listener) => listener.config.once) .map((listener) => listener.handler) : []; } /** * Returns a copy of the internal listeners array for the specified event, * including wrapper functions like those used by `.once()`. * @param {string | symbol} event - The event name. * @returns {handler[]} An array of raw listener functions. */ allListeners(event) { if (typeof event !== 'string') throw new TypeError('allListeners(event): event name must be a string'); const listeners = this.#listeners.get(event); return Array.isArray(listeners) ? [...listeners].map((listener) => listener.handler) : []; } /** * Returns an array of event names for which there are registered listeners. * * @returns {string[]} Array of registered event names. */ eventNames() { return [...this.#listeners.keys()]; } /** * Emits an event, triggering all registered handlers for that event. * * @param {string|string[]} event - The event name to emit. * @param {...any} payload - Optional data to pass to each handler. * @returns {boolean[]} True if any listeners were called, false otherwise. */ emit(event, ...payload) { const events = this.#normalizeEvents(event, 'emit'); const called = []; for (const ev of events) { const listeners = this.#listeners.get(ev); if (!Array.isArray(listeners) || listeners.length === 0) { called.push(false); continue; } // Call all listeners with the provided data listeners.forEach((listener) => listener.handler(...payload)); called.push(true); } return called; } /////////////////////////////////// /** * Sets the maximum number of listeners per event before a warning is shown. * * @param {number} n - The maximum number of listeners. */ setMaxListeners(n) { if (!Number.isInteger(n) || n < 0) throw new TypeError('setMaxListeners(n): n must be a non-negative integer'); this.#maxListeners = n; } /** * Gets the maximum number of listeners allowed per event. * * @returns {number} The maximum number of listeners. */ getMaxListeners() { return this.#maxListeners; } } module.exports = TinyEvents;