diff --git a/source/event-emitter.ts b/source/event-emitter.ts index caaa66d..9c6b7ed 100644 --- a/source/event-emitter.ts +++ b/source/event-emitter.ts @@ -133,8 +133,8 @@ export class EventEmitter { listeners.delete(entry); }); - // If no listener was provided, remove the listeners set from the listeners map. - if (!listener) { + // If no listener was provided and no listeners are left for the event type, remove the listeners set from the listeners map. + if (!listener || this.#listeners.get(type)?.size === 0) { this.#listeners.delete(type); } } @@ -169,7 +169,11 @@ export class EventEmitter { * Remove all listeners. */ removeAllListeners(): void { - this.#listeners.clear(); + for (const [ type, listeners ] of this.#listeners.entries()) { + listeners.forEach((entry) => { + this.off(type, entry.listener); + }); + } } /** diff --git a/test/event-emitter.test.ts b/test/event-emitter.test.ts index 415ba3f..dd32b33 100644 --- a/test/event-emitter.test.ts +++ b/test/event-emitter.test.ts @@ -305,6 +305,31 @@ const testEventEmitterRemoveAllListeners = (): void => { expect(countListener).not.toHaveBeenCalled(); }; +/** + * Tests that removeAllListeners() cancels a pending debounced callback. + */ +const testEventEmitterRemoveAllListenersCancelsPendingDebouncedCallback = async (): Promise => { + vi.useFakeTimers(); + + try { + const emitter = new EventEmitter(); + const listener = vi.fn(); + + // Arm a debounce timer, then clear every listener before it expires. + emitter.on('message', listener, 100); + emitter.emit('message', 'should not arrive'); + emitter.removeAllListeners(); + + await vi.advanceTimersByTimeAsync(100); + + // Cleared listeners must not receive delayed debounced delivery. + expect(listener).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + vi.restoreAllMocks(); + } +}; + /** * Tests that waitFor() resolves when a matching event is emitted. */ @@ -658,6 +683,10 @@ const runTests = async (): Promise => { testEventEmitterOffCancelsPendingDebouncedCallback, ); test('EventEmitter: removes all listeners when removeAllListeners is called', testEventEmitterRemoveAllListeners); + test( + 'EventEmitter: cancels a pending debounced callback when removeAllListeners is called', + testEventEmitterRemoveAllListenersCancelsPendingDebouncedCallback, + ); test('EventEmitter: resolves waitFor when a matching event is emitted', testEventEmitterWaitForResolvesOnMatch); test('EventEmitter: ignores non-matching events while waiting with waitFor', testEventEmitterWaitForIgnoresNonMatchingEvents); test('EventEmitter: rejects waitFor when the timeout is reached', testEventEmitterWaitForRejectsOnTimeout);