diff --git a/.gitignore b/.gitignore index 24bb9f0..a4238eb 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,4 @@ site/.DS_Store # npm package-lock.json +.DS_Store diff --git a/README.md b/README.md index a4b6c75..655f7d1 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ if you are not using ESM modules, you can use the following: ``` -# API +# API - Hooks ## .onHook(eventName, handler) @@ -143,6 +143,8 @@ Get all hooks for an event. ## .clearHooks(eventName) +# API - Events + ## .on(eventName, handler) Subscribe to an event. @@ -167,6 +169,30 @@ Remove all listeners for an event. Set the maximum number of listeners and will truncate if there are already too many. +## .once(eventName, handler) + +Subscribe to an event once. + +## .prependListener(eventName, handler) + +Prepend a listener to an event. + +## .prependOnceListener(eventName, handler) + +Prepend a listener to an event once. + +## .eventNames() + +Get all event names. + +## .listenerCount(eventName?) + +Get the count of listeners for an event or all events if evenName not provided. + +## .rawListeners(eventName?) + +Get all listeners for an event or all events if evenName not provided. + # Development and Testing Hookified is written in TypeScript and tests are written in `vitest`. To run the tests, use the following command: diff --git a/src/event-emitter-type.ts b/src/event-emitter.ts similarity index 93% rename from src/event-emitter-type.ts rename to src/event-emitter.ts index 1fd3a94..b19595d 100644 --- a/src/event-emitter-type.ts +++ b/src/event-emitter.ts @@ -1,4 +1,5 @@ -type EventEmitterType = { +// eslint-disable-next-line @typescript-eslint/naming-convention +export type IEventEmitter = { /** * Registers a listener for the specified event. * @@ -11,7 +12,7 @@ type EventEmitterType = { * console.log(message); * }); */ - on(eventName: string | symbol, listener: (...arguments_: any[]) => void): EventEmitterType; + on(eventName: string | symbol, listener: (...arguments_: any[]) => void): IEventEmitter; /** * Alias for `on`. Registers a listener for the specified event. @@ -20,7 +21,7 @@ type EventEmitterType = { * @param listener - A callback function that will be invoked when the event is emitted. * @returns The current instance of EventEmitter for method chaining. */ - addListener(eventName: string | symbol, listener: (...arguments_: any[]) => void): EventEmitterType; + addListener(eventName: string | symbol, listener: (...arguments_: any[]) => void): IEventEmitter; /** * Registers a one-time listener for the specified event. The listener is removed after it is called once. @@ -34,7 +35,7 @@ type EventEmitterType = { * console.log('The connection was closed.'); * }); */ - once(eventName: string | symbol, listener: (...arguments_: any[]) => void): EventEmitterType; + once(eventName: string | symbol, listener: (...arguments_: any[]) => void): IEventEmitter; /** * Removes a previously registered listener for the specified event. @@ -46,7 +47,7 @@ type EventEmitterType = { * @example * emitter.off('data', myListener); */ - off(eventName: string | symbol, listener: (...arguments_: any[]) => void): EventEmitterType; + off(eventName: string | symbol, listener: (...arguments_: any[]) => void): IEventEmitter; /** * Alias for `off`. Removes a previously registered listener for the specified event. @@ -55,7 +56,7 @@ type EventEmitterType = { * @param listener - The specific callback function to remove. * @returns The current instance of EventEmitter for method chaining. */ - removeListener(eventName: string | symbol, listener: (...arguments_: any[]) => void): EventEmitterType; + removeListener(eventName: string | symbol, listener: (...arguments_: any[]) => void): IEventEmitter; /** * Emits the specified event, invoking all registered listeners with the provided arguments. @@ -90,7 +91,7 @@ type EventEmitterType = { * @example * emitter.removeAllListeners('data'); */ - removeAllListeners(eventName?: string | symbol): EventEmitterType; + removeAllListeners(eventName?: string | symbol): IEventEmitter; /** * Returns an array of event names for which listeners have been registered. @@ -138,7 +139,7 @@ type EventEmitterType = { * console.log('This will run first.'); * }); */ - prependListener(eventName: string | symbol, listener: (...arguments_: any[]) => void): EventEmitterType; + prependListener(eventName: string | symbol, listener: (...arguments_: any[]) => void): IEventEmitter; /** * Adds a one-time listener to the beginning of the listeners array for the specified event. @@ -152,5 +153,5 @@ type EventEmitterType = { * console.log('This will run first and only once.'); * }); */ - prependOnceListener(eventName: string | symbol, listener: (...arguments_: any[]) => void): EventEmitterType; + prependOnceListener(eventName: string | symbol, listener: (...arguments_: any[]) => void): IEventEmitter; }; diff --git a/src/eventified.ts b/src/eventified.ts index 08d7d8f..9cf5985 100644 --- a/src/eventified.ts +++ b/src/eventified.ts @@ -1,24 +1,77 @@ +import {type IEventEmitter} from './event-emitter.js'; + export type EventListener = (...arguments_: any[]) => void; -export class Eventified { - _eventListeners: Map; +export class Eventified implements IEventEmitter { + _eventListeners: Map; _maxListeners: number; constructor() { - this._eventListeners = new Map(); + this._eventListeners = new Map(); this._maxListeners = 100; // Default maximum number of listeners } + once(eventName: string | symbol, listener: EventListener): IEventEmitter { + const onceListener: EventListener = (...arguments_: any[]) => { + this.off(eventName as string, onceListener); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + listener(...arguments_); + }; + + this.on(eventName as string, onceListener); + return this; + } + + listenerCount(eventName?: string | symbol): number { + if (!eventName) { + return this.getAllListeners().length; + } + + const listeners = this._eventListeners.get(eventName as string); + return listeners ? listeners.length : 0; + } + + eventNames(): Array { + return Array.from(this._eventListeners.keys()); + } + + rawListeners(eventName?: string | symbol): EventListener[] { + if (!eventName) { + return this.getAllListeners(); + } + + return this._eventListeners.get(eventName) ?? []; + } + + prependListener(eventName: string | symbol, listener: EventListener): IEventEmitter { + const listeners = this._eventListeners.get(eventName) ?? []; + listeners.unshift(listener); + this._eventListeners.set(eventName, listeners); + return this; + } + + prependOnceListener(eventName: string | symbol, listener: EventListener): IEventEmitter { + const onceListener: EventListener = (...arguments_: any[]) => { + this.off(eventName as string, onceListener); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + listener(...arguments_); + }; + + this.prependListener(eventName as string, onceListener); + return this; + } + public maxListeners(): number { return this._maxListeners; } // Add an event listener - public addListener(event: string, listener: EventListener): void { + public addListener(event: string | symbol, listener: EventListener): IEventEmitter { this.on(event, listener); + return this; } - public on(event: string, listener: EventListener): void { + public on(event: string | symbol, listener: EventListener): IEventEmitter { if (!this._eventListeners.has(event)) { this._eventListeners.set(event, []); } @@ -27,19 +80,22 @@ export class Eventified { if (listeners) { if (listeners.length >= this._maxListeners) { - console.warn(`MaxListenersExceededWarning: Possible event memory leak detected. ${listeners.length + 1} ${event} listeners added. Use setMaxListeners() to increase limit.`); + console.warn(`MaxListenersExceededWarning: Possible event memory leak detected. ${listeners.length + 1} ${event as string} listeners added. Use setMaxListeners() to increase limit.`); } listeners.push(listener); } + + return this; } // Remove an event listener - public removeListener(event: string, listener: EventListener): void { + public removeListener(event: string, listener: EventListener): IEventEmitter { this.off(event, listener); + return this; } - public off(event: string, listener: EventListener): void { + public off(event: string, listener: EventListener): IEventEmitter { const listeners = this._eventListeners.get(event) ?? []; const index = listeners.indexOf(listener); if (index > -1) { @@ -49,10 +105,12 @@ export class Eventified { if (listeners.length === 0) { this._eventListeners.delete(event); } + + return this; } // Emit an event - public emit(event: string, ...arguments_: any[]): void { + public emit(event: string, ...arguments_: any[]): boolean { const listeners = this._eventListeners.get(event); if (listeners && listeners.length > 0) { @@ -61,6 +119,8 @@ export class Eventified { listener(...arguments_); } } + + return true; } // Get all listeners for a specific event @@ -69,12 +129,14 @@ export class Eventified { } // Remove all listeners for a specific event - public removeAllListeners(event?: string): void { + public removeAllListeners(event?: string): IEventEmitter { if (event) { this._eventListeners.delete(event); } else { this._eventListeners.clear(); } + + return this; } // Set the maximum number of listeners for a single event @@ -86,4 +148,13 @@ export class Eventified { } } } + + public getAllListeners(): EventListener[] { + let result = new Array(); + for (const listeners of this._eventListeners.values()) { + result = result.concat(listeners); + } + + return result; + } } diff --git a/test/eventified.test.ts b/test/eventified.test.ts index 3117003..95b5826 100644 --- a/test/eventified.test.ts +++ b/test/eventified.test.ts @@ -134,4 +134,98 @@ describe('Eventified', () => { t.expect(emitter.listeners('test-event')).toEqual([listener]); }); + + test('emit event only once with once method', t => { + const emitter = new Eventified(); + let dataReceived = 0; + + emitter.once('test-event', () => { + dataReceived++; + }); + + emitter.emit('test-event'); + emitter.emit('test-event'); + + t.expect(dataReceived).toBe(1); + }); + + test('get listener count', t => { + const emitter = new Eventified(); + const listener = () => {}; + + emitter.on('test-event', listener); + emitter.on('test-event', listener); + emitter.on('test-event1', listener); + emitter.on('test-event2', listener); + + t.expect(emitter.listenerCount()).toBe(4); + t.expect(emitter.listenerCount('test-event')).toBe(2); + }); + + test('no listener count', t => { + const emitter = new Eventified(); + t.expect(emitter.listenerCount('test-event')).toBe(0); + }); + + test('get event names', t => { + const emitter = new Eventified(); + const listener = () => {}; + + emitter.on('test-event', listener); + emitter.on('test-event1', listener); + emitter.on('test-event2', listener); + + t.expect(emitter.eventNames()).toEqual(['test-event', 'test-event1', 'test-event2']); + }); + + test('get raw listeners', t => { + const emitter = new Eventified(); + const listener = () => {}; + + emitter.on('test-event', listener); + emitter.on('test-event', listener); + emitter.on('test-event1', listener); + emitter.on('test-event2', listener); + + t.expect(emitter.rawListeners('test-event')).toEqual([listener, listener]); + t.expect(emitter.rawListeners('test-event1')).toEqual([listener]); + t.expect(emitter.rawListeners().length).toEqual(4); + }); + + test('get raw listeners when no listeners', t => { + const emitter = new Eventified(); + t.expect(emitter.rawListeners('test-event')).toEqual([]); + }); + + test('prepend listener', t => { + const emitter = new Eventified(); + const listener = () => {}; + + emitter.on('test-event', listener); + emitter.prependListener('test-event', () => {}); + + t.expect(emitter.rawListeners('test-event').length).toBe(2); + t.expect(emitter.rawListeners('test-event')[0]).not.toBe(listener); + }); + + test('prepend with no listenters', t => { + const emitter = new Eventified(); + emitter.prependListener('test-event', () => {}); + t.expect(emitter.rawListeners('test-event').length).toBe(1); + }); + + test('prepend once listener', t => { + const emitter = new Eventified(); + const listener = () => {}; + + emitter.on('test-event', listener); + emitter.prependOnceListener('test-event', () => {}); + + t.expect(emitter.rawListeners('test-event').length).toBe(2); + t.expect(emitter.rawListeners('test-event')[0]).not.toBe(listener); + + emitter.emit('test-event'); + + t.expect(emitter.rawListeners('test-event').length).toBe(1); + }); }); diff --git a/vitest.config.ts b/vitest.config.ts index b2012b8..c59406c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ 'vitest.config.ts', 'dist/**', 'test/**', - 'src/event-emitter-type.ts', + 'src/event-emitter.ts', 'tsup.config.ts', ], },