UNPKG

@infectedbyjs/emitts

Version:

A type-safe event emitter for TypeScript with priority-based listeners, sequential/parallel execution strategies, and memory leak detection

212 lines (211 loc) 7.64 kB
import { log, insertSorted, removeFromArray } from "./helpers"; /** * A type-safe event emitter with priority-based event handling. * * Features: * - Priority-based listeners (higher numbers execute first) * - Sequential or parallel execution strategies * - Debugging capabilities with custom loggers * - Memory leak detection with maxListeners warning * * @template Events A map of event names to their payload types */ export class EmitTS { /** * Creates a new event emitter instance. * * @param options Configuration options * @param options.debug Whether to enable debug logging (defaults to false) * @param options.logger Custom debug logger (defaults to internal logger) * @param options.maxListeners Maximum listeners per event before warning (defaults to 10) */ constructor(options = {}) { this.logger = options.debug ? options.logger || log : () => { }; this.subscriptions = new Map(); this.maxListeners = options.maxListeners ?? 10; } /** * Checks whether the emitter has listeners for the given event. * @param event Event name * @returns `true` if any listeners are registered */ has(event) { return this.subscriptions.has(event); } /** * Returns the number of listeners for the given event. * @param event Event name * @returns The number of listeners */ listenersCount(event) { return this.subscriptions.get(event)?.subscriberCount ?? 0; } /** * Returns a list of all currently registered event names. */ get eventNames() { return Array.from(this.subscriptions.keys()); } /** * Checks whether the emitter has no listeners at all. */ isEmpty() { return this.subscriptions.size === 0; } /** * Removes all listeners for all events. */ clear() { this.logger("clear", { message: `CLEARING ${this.subscriptions.size} EVENT TYPES` }); this.subscriptions.forEach((subscription) => subscription.clear()); this.subscriptions.clear(); } /** * Subscribes a callback function to an event with optional priority. * Higher priority listeners are called first. */ on(event, callback, priority = 0) { if (!this.subscriptions.has(event)) { this.subscriptions.set(event, new EventSubscription(String(event), this.maxListeners)); } const subscription = this.subscriptions.get(event); const off = subscription.addSubscriber(callback, priority); this.logger("subscribe", { event, message: `PRIORITY ${priority}` }); return () => { this.logger("unsubscribe", { event }); off(); if (subscription.isEmpty) this.subscriptions.delete(event); }; } /** * Subscribes a one-time callback function that automatically unsubscribes after execution. */ once(event, callback, priority = 0) { this.logger("subscribe:once", { event }); let off = this.on(event, async (data) => { this.logger("unsubscribe:once", { event }); await callback(data); off?.(); off = null; }, priority); } /** * Removes listeners from an event. If no callback is provided, removes all listeners. * If no event is provided, clears all listeners from all events. */ off(event, callback) { if (!event || !this.subscriptions.has(event)) { this.clear(); return; } const subscription = this.subscriptions.get(event); if (callback) { this.logger("unsubscribe", { event }); subscription.removeSubscriber(callback); } else { this.logger("unsubscribe_all", { event, message: `REMOVING ${subscription.subscriberCount} SUBSCRIBERS`, }); subscription.clear(); } if (subscription.isEmpty) this.subscriptions.delete(event); } /** * Triggers an event with payload data and executes all listeners. * Supports both parallel (default) and sequential execution strategies. * Handles errors to prevent one listener from breaking others. */ async emit(event, data, options = { strategy: "parallel" }) { const subscription = this.subscriptions.get(event); if (!subscription) { this.logger("emit", { event, message: `!!! NO EVENTS "${String(event)}" FOUND !!!` }); return; } this.logger("emit", { event, data, message: `STRATEGY: ${options.strategy}` }); if (options.strategy === "sequential") { for (const { callback } of subscription.subscribers) { try { await callback(data); } catch (err) { console.error(`[EmitTS] Error in listener for event "${String(event)}":`, err); } } } else { const results = []; for (const { callback } of subscription.subscribers) { try { const result = callback(data); if (result instanceof Promise) results.push(result); } catch (err) { console.error(`[EmitTS] Error in listener for event "${String(event)}":`, err); } } try { await Promise.all(results); } catch (err) { console.error(`[EmitTS] Error in listener for event "${String(event)}":`, err); } } } /** * Returns a promise that resolves when the given event is emitted. * * @param event Event name * @returns Promise that resolves with the event payload */ toPromise(event) { return new Promise((resolve) => this.once(event, resolve)); } } /** * Internal class that manages subscribers for a single event type. * Handles priority sorting, duplicate detection, and memory leak warnings. * * @template T The event payload type */ class EventSubscription { constructor(type, maxListeners) { this.type = type; this.maxListeners = maxListeners; this.subscribers = []; } get isEmpty() { return this.subscribers.length === 0; } get subscriberCount() { return this.subscribers.length; } /** * Adds a new subscriber with specified priority. * Handles duplicate detection and maxListeners warnings. * Orders subscribers by priority (highest first). */ addSubscriber(callback, priority = 0) { if (this.subscribers.some((s) => s.callback === callback)) { console.warn(`[EmitTS] Duplicate listener detected for "${this.type}". Used the already existing one.`); return () => this.removeSubscriber(callback); } if (this.subscribers.length >= this.maxListeners) { console.warn(`[EmitTS] MaxListenersExceededWarning: Possible memory leak detected. ` + `More than ${this.maxListeners} listeners for event "${this.type}".`); } const subscriber = { callback, priority }; insertSorted(this.subscribers, subscriber, (s) => s.priority); return () => this.removeSubscriber(callback); } removeSubscriber(callback) { removeFromArray(this.subscribers, (s) => s.callback === callback); } clear() { this.subscribers = []; } }