From fbcebf9337240174fa7d53a51a6c0abf426c3740 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Mon, 14 Oct 2024 18:10:31 -0700 Subject: [PATCH 1/9] Adding in IEventEmitter --- ...event-emitter-type.ts => event-emitter.ts} | 18 +++++++------- src/eventified.ts | 24 +++++++++++++------ 2 files changed, 26 insertions(+), 16 deletions(-) rename src/{event-emitter-type.ts => event-emitter.ts} (93%) 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..e1ae346 100644 --- a/src/event-emitter-type.ts +++ b/src/event-emitter.ts @@ -1,4 +1,4 @@ -type EventEmitterType = { +export type IEventEmitter = { /** * Registers a listener for the specified event. * @@ -11,7 +11,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 +20,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 +34,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 +46,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 +55,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 +90,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 +138,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 +152,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..a08ed84 100644 --- a/src/eventified.ts +++ b/src/eventified.ts @@ -1,6 +1,8 @@ +import { type IEventEmitter } from './event-emitter.js'; + export type EventListener = (...arguments_: any[]) => void; -export class Eventified { +export class Eventified implements IEventEmitter { _eventListeners: Map; _maxListeners: number; @@ -14,11 +16,12 @@ export class Eventified { } // Add an event listener - public addListener(event: string, listener: EventListener): void { + public addListener(event: string, listener: EventListener): IEventEmitter { this.on(event, listener); + return this; } - public on(event: string, listener: EventListener): void { + public on(event: string, listener: EventListener): IEventEmitter { if (!this._eventListeners.has(event)) { this._eventListeners.set(event, []); } @@ -35,11 +38,12 @@ export class Eventified { } // 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 +53,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 +67,8 @@ export class Eventified { listener(...arguments_); } } + + return true; } // Get all listeners for a specific event @@ -69,12 +77,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 From cafca7e8c35645eca6c4b59f2a31a2a28356d4e3 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Tue, 15 Oct 2024 12:06:15 -0700 Subject: [PATCH 2/9] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 24608e06b3ace628d5649ee9768ceef976dd0827 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Tue, 15 Oct 2024 12:16:33 -0700 Subject: [PATCH 3/9] adding in additional iEventEmitter --- src/event-emitter.ts | 1 + src/eventified.ts | 46 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/event-emitter.ts b/src/event-emitter.ts index e1ae346..b19595d 100644 --- a/src/event-emitter.ts +++ b/src/event-emitter.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/naming-convention export type IEventEmitter = { /** * Registers a listener for the specified event. diff --git a/src/eventified.ts b/src/eventified.ts index a08ed84..b526ed3 100644 --- a/src/eventified.ts +++ b/src/eventified.ts @@ -1,27 +1,59 @@ -import { type IEventEmitter } from './event-emitter.js'; +import {type IEventEmitter} from './event-emitter.js'; export type EventListener = (...arguments_: any[]) => void; export class Eventified implements IEventEmitter { - _eventListeners: Map; + _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 { + const listeners = this._eventListeners.get(eventName as string); + return listeners ? listeners.length : 0; + } + + eventNames(): Array { + throw new Error('Method not implemented.'); + } + + rawListeners(eventName: string | symbol): EventListener[] { + throw new Error('Method not implemented.'); + } + + prependListener(eventName: string | symbol, listener: EventListener): IEventEmitter { + throw new Error('Method not implemented.'); + } + + prependOnceListener(eventName: string | symbol, listener: EventListener): IEventEmitter { + throw new Error('Method not implemented.'); + } + public maxListeners(): number { return this._maxListeners; } // Add an event listener - public addListener(event: string, listener: EventListener): IEventEmitter { + public addListener(event: string | symbol, listener: EventListener): IEventEmitter { this.on(event, listener); return this; } - public on(event: string, listener: EventListener): IEventEmitter { + public on(event: string | symbol, listener: EventListener): IEventEmitter { if (!this._eventListeners.has(event)) { this._eventListeners.set(event, []); } @@ -30,11 +62,13 @@ export class Eventified implements IEventEmitter { 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 From d565a83c94096d26402b67c25876193871bc637c Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Tue, 15 Oct 2024 12:18:29 -0700 Subject: [PATCH 4/9] clean up ignore on coverage --- vitest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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', ], }, From 6a222dc4d27dab1ff053c451fd7b55bfd1aad36a Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Tue, 15 Oct 2024 12:20:03 -0700 Subject: [PATCH 5/9] test for once --- test/eventified.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/eventified.test.ts b/test/eventified.test.ts index 3117003..a694711 100644 --- a/test/eventified.test.ts +++ b/test/eventified.test.ts @@ -134,4 +134,18 @@ 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); + }); }); From 6b17b3171ccae0902451466e8a1bce20fd4d0334 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Tue, 15 Oct 2024 12:28:17 -0700 Subject: [PATCH 6/9] adding in listenerCount --- src/eventified.ts | 15 ++++++++++++++- test/eventified.test.ts | 13 +++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/eventified.ts b/src/eventified.ts index b526ed3..853f8ae 100644 --- a/src/eventified.ts +++ b/src/eventified.ts @@ -22,7 +22,11 @@ export class Eventified implements IEventEmitter { return this; } - listenerCount(eventName: string | symbol): number { + listenerCount(eventName?: string | symbol): number { + if (!eventName) { + return this.getAllListeners().length; + } + const listeners = this._eventListeners.get(eventName as string); return listeners ? listeners.length : 0; } @@ -130,4 +134,13 @@ export class Eventified implements IEventEmitter { } } } + + 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 a694711..5b24a82 100644 --- a/test/eventified.test.ts +++ b/test/eventified.test.ts @@ -148,4 +148,17 @@ describe('Eventified', () => { 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); + }); }); From 627f6eb7bc6720a2401b5b545563e88b346c26fe Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Tue, 15 Oct 2024 12:35:55 -0700 Subject: [PATCH 7/9] adding in rawListeners --- src/eventified.ts | 10 +++++++--- test/eventified.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/eventified.ts b/src/eventified.ts index 853f8ae..996d06c 100644 --- a/src/eventified.ts +++ b/src/eventified.ts @@ -32,11 +32,15 @@ export class Eventified implements IEventEmitter { } eventNames(): Array { - throw new Error('Method not implemented.'); + return Array.from(this._eventListeners.keys()); } - rawListeners(eventName: string | symbol): EventListener[] { - throw new Error('Method not implemented.'); + rawListeners(eventName?: string | symbol): EventListener[] { + if (!eventName) { + return this.getAllListeners(); + } + + return this._eventListeners.get(eventName) ?? []; } prependListener(eventName: string | symbol, listener: EventListener): IEventEmitter { diff --git a/test/eventified.test.ts b/test/eventified.test.ts index 5b24a82..270e46c 100644 --- a/test/eventified.test.ts +++ b/test/eventified.test.ts @@ -161,4 +161,29 @@ describe('Eventified', () => { t.expect(emitter.listenerCount()).toBe(4); t.expect(emitter.listenerCount('test-event')).toBe(2); }); + + 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); + }); }); From 6f58fcf6c5adf1734f1c4244c421db2d6b6d9ed2 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Tue, 15 Oct 2024 12:49:58 -0700 Subject: [PATCH 8/9] adding in prepend --- src/eventified.ts | 14 ++++++++++++-- test/eventified.test.ts | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/eventified.ts b/src/eventified.ts index 996d06c..9cf5985 100644 --- a/src/eventified.ts +++ b/src/eventified.ts @@ -44,11 +44,21 @@ export class Eventified implements IEventEmitter { } prependListener(eventName: string | symbol, listener: EventListener): IEventEmitter { - throw new Error('Method not implemented.'); + const listeners = this._eventListeners.get(eventName) ?? []; + listeners.unshift(listener); + this._eventListeners.set(eventName, listeners); + return this; } prependOnceListener(eventName: string | symbol, listener: EventListener): IEventEmitter { - throw new Error('Method not implemented.'); + 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 { diff --git a/test/eventified.test.ts b/test/eventified.test.ts index 270e46c..95b5826 100644 --- a/test/eventified.test.ts +++ b/test/eventified.test.ts @@ -162,6 +162,11 @@ describe('Eventified', () => { 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 = () => {}; @@ -186,4 +191,41 @@ describe('Eventified', () => { 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); + }); }); From bc5556ca5344b72d54cdc1a06483adaab58eea9e Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Tue, 15 Oct 2024 12:58:43 -0700 Subject: [PATCH 9/9] Update README.md --- README.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) 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: