diff --git a/package-lock.json b/package-lock.json index 813ede6..045cdec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@bitauth/libauth": "^3.1.0-next.8", "@xo-cash/types": "0.0.3", + "eventemitter3": "^5.0.4", "zod": "^4.3.6" }, "devDependencies": { @@ -5690,6 +5691,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/execa": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", diff --git a/package.json b/package.json index 17576df..b952cd8 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dependencies": { "@bitauth/libauth": "^3.1.0-next.8", "@xo-cash/types": "0.0.3", + "eventemitter3": "^5.0.4", "zod": "^4.3.6" }, "overrides": { diff --git a/source/event-emitter.ts b/source/event-emitter.ts deleted file mode 100644 index e455a08..0000000 --- a/source/event-emitter.ts +++ /dev/null @@ -1,201 +0,0 @@ -export type EventMap = Record; - -type Listener = (detail: T) => void; - -/** - * A listener entry. - * @template T - The event type. - */ -interface ListenerEntry { - listener: Listener; - wrappedListener: Listener; - debounceTime?: number; - once?: boolean; -} - -export type OffCallback = () => void; - -/** - * A simple event emitter implementation. - * @template T - The event map type. - */ -export class EventEmitter { - /** - * The listeners map. - * @private - */ - private listeners: Map>> = new Map(); - - /** - * Add a listener for an event. - * @param type - The event type. - * @param listener - The listener function. - * @param debounceMilliseconds - The debounce time in milliseconds. - * @returns An off callback that can be called to stop listening for events. - */ - on(type: K, listener: Listener, debounceMilliseconds?: number): OffCallback { - // Create a wrapped listener so that the debounce can be applied. - const wrappedListener = debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(listener, debounceMilliseconds) : listener; - - // If the listeners map does not have the event type, create a new set. - if (!this.listeners.has(type)) { - this.listeners.set(type, new Set()); - } - - // Create a listener entry. - const listenerEntry: ListenerEntry = { - listener, - wrappedListener, - ...(debounceMilliseconds !== undefined ? { debounceTime: debounceMilliseconds } : {}), - }; - - // Add the listener entry to the listeners map. - this.listeners.get(type)?.add(listenerEntry as ListenerEntry); - - // Return an "off" callback that can be called to stop listening for events. - return () => this.off(type, listener); - } - - /** - * Add a one-time listener for an event. - * @param type - The event type. - * @param listener - The listener function. - * @param debounceMilliseconds - The debounce time in milliseconds. - * @returns An off callback that can be called to stop listening for events. - */ - once(type: K, listener: Listener, debounceMilliseconds?: number): OffCallback { - const wrappedListener: Listener = (detail: T[K]) => { - this.off(type, listener); - listener(detail); - }; - - // Create a debounced listener. - const debouncedListener = - debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(wrappedListener, debounceMilliseconds) : wrappedListener; - - // If the listeners map does not have the event type, create a new set. - if (!this.listeners.has(type)) { - this.listeners.set(type, new Set()); - } - - // Create a listener entry. - const listenerEntry: ListenerEntry = { - listener, - wrappedListener: debouncedListener, - once: true, - ...(debounceMilliseconds !== undefined ? { debounceTime: debounceMilliseconds } : {}), - }; - - // Add the listener entry to the listeners map. - this.listeners.get(type)?.add(listenerEntry as ListenerEntry); - - // Return an "off" callback that can be called to stop listening for events. - return () => this.off(type, listener); - } - - /** - * Remove a listener for an event. - * @param type - The event type. - * @param listener - The listener function. - */ - off(type: K, listener: Listener): void { - // Get the listeners for the event type. - const listeners = this.listeners.get(type); - if (!listeners) return; - - // Find the listener entry. - const listenerEntry = Array.from(listeners).find((entry) => entry.listener === listener || entry.wrappedListener === listener); - - // If the listener entry is found, remove it from the listeners map. - if (listenerEntry) { - listeners.delete(listenerEntry); - } - } - - /** - * Emit an event. - * @param type - The event type. - * @param payload - The event payload. - * @returns True if there are listeners for the event, false otherwise. - */ - emit(type: K, payload: T[K]): boolean { - // Get the listeners for the event type. - const listeners = this.listeners.get(type); - if (!listeners) return false; - - // Emit the event to all listeners. - listeners.forEach((entry) => { - entry.wrappedListener(payload); - }); - - // Return true if there are listeners for the event, false otherwise. - return listeners.size > 0; - } - - /** - * Remove all listeners. - */ - removeAllListeners(): void { - this.listeners.clear(); - } - - /** - * Wait for an event to be emitted. - * @param type - The event type. - * @param predicate - The predicate function. - * @param timeoutMs - The timeout in milliseconds. - * @returns The event payload. - */ - async waitFor(type: K, predicate: (payload: T[K]) => boolean, timeoutMs?: number): Promise { - // Create a promise to wait for the event to be emitted. - return new Promise((resolve, reject) => { - let timeoutId: ReturnType | undefined; - - // Create a listener function. - const listener = (payload: T[K]): void => { - if (predicate(payload)) { - // Clean up - this.off(type, listener); - if (timeoutId !== undefined) { - clearTimeout(timeoutId); - } - - resolve(payload); - } - }; - - // Set up timeout if specified - if (timeoutMs !== undefined) { - timeoutId = setTimeout(() => { - this.off(type, listener); - reject(new Error(`Timeout waiting for event "${String(type)}"`)); - }, timeoutMs); - } - - // Add the listener to the listeners map. - this.on(type, listener); - }); - } - - /** - * Debounce a function. - * @param func - The function to debounce. - * @param wait - The wait time in milliseconds. - * @returns The debounced function. - */ - private debounce(func: Listener, wait: number): Listener { - // Create a timeout variable. - let timeout: ReturnType; - - return (detail: T[K]) => { - // If a debounce timer is already pending, clear it before scheduling the next one. - if (timeout !== undefined) { - clearTimeout(timeout); - } - - timeout = setTimeout(() => { - func(detail); - }, wait); - }; - } -} diff --git a/source/index.ts b/source/index.ts index de07278..f7d1ed5 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,4 +1,3 @@ -export * from './event-emitter.ts'; export * from './exponential-backoff.ts'; export * from './extended-json.ts'; export * from './misc.ts'; diff --git a/source/sse-session/sse-session.ts b/source/sse-session/sse-session.ts index c1dd20c..386a1e4 100644 --- a/source/sse-session/sse-session.ts +++ b/source/sse-session/sse-session.ts @@ -1,8 +1,9 @@ import type { SSESessionOptions, SSESessionEventMap, SSEvent } from './types.ts'; +import { EventEmitter } from 'eventemitter3' + import { HTTPError, ResponseBodyNullError } from '../errors.ts'; import { tryAsync } from '../misc.ts'; -import { EventEmitter } from '../event-emitter.ts'; import { ExponentialBackoff } from '../exponential-backoff.ts'; import { SSEEventParser } from './sse-event-parser.ts'; diff --git a/test/event-emitter.test.ts b/test/event-emitter.test.ts deleted file mode 100644 index b0b0e81..0000000 --- a/test/event-emitter.test.ts +++ /dev/null @@ -1,505 +0,0 @@ -import { expect, test, vi } from 'vitest'; -import { EventEmitter } from '../source/event-emitter.ts'; - -/** Simple event map used across these tests. */ -type TestEvents = { - message: string; - count: number; -}; - -/** - * Tests that EventEmitter invokes listeners when an event is emitted. - */ -const testEventEmitterCallsListeners = (): void => { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - // Register the listener and emit an event. - emitter.on('message', listener); - emitter.emit('message', 'hello'); - - // Expect the listener to have been called with the emitted payload. - expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith('hello'); -}; - -/** - * Tests that EventEmitter invokes all listeners registered for the same event. - */ -const testEventEmitterCallsMultipleListeners = (): void => { - const emitter = new EventEmitter(); - const firstListener = vi.fn(); - const secondListener = vi.fn(); - - // Register two listeners for the same event type. - emitter.on('count', firstListener); - emitter.on('count', secondListener); - emitter.emit('count', 42); - - // Expect both listeners to receive the same payload. - expect(firstListener).toHaveBeenCalledOnce(); - expect(firstListener).toHaveBeenCalledWith(42); - expect(secondListener).toHaveBeenCalledOnce(); - expect(secondListener).toHaveBeenCalledWith(42); -}; - -/** - * Tests that EventEmitter only invokes listeners registered for the emitted event type. - */ -const testEventEmitterCallsOnlyMatchingListeners = (): void => { - const emitter = new EventEmitter(); - const messageListener = vi.fn(); - const countListener = vi.fn(); - - // Register listeners on different event types. - emitter.on('message', messageListener); - emitter.on('count', countListener); - - // Emit only the message event. - emitter.emit('message', 'hello'); - - // Expect only the matching listener to have been called. - expect(messageListener).toHaveBeenCalledOnce(); - expect(countListener).not.toHaveBeenCalled(); -}; - -/** - * Tests that EventEmitter.emit returns false when no listeners are registered. - */ -const testEventEmitterEmitReturnsFalseWithNoListeners = (): void => { - const emitter = new EventEmitter(); - - const hasListeners = emitter.emit('message', 'hello'); - - // Expect emit to report that nobody was listening. - expect(hasListeners).toBe(false); -}; - -/** - * Tests that EventEmitter.emit returns true when listeners are registered. - */ -const testEventEmitterEmitReturnsTrueWithListeners = (): void => { - const emitter = new EventEmitter(); - - emitter.on('message', vi.fn()); - - const hasListeners = emitter.emit('message', 'hello'); - - // Expect emit to report that at least one listener was invoked. - expect(hasListeners).toBe(true); -}; - -/** - * Tests that the off callback returned by on() removes the listener. - */ -const testEventEmitterOffCallbackRemovesListener = (): void => { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - // on() returns an off callback that removes the listener. - const off = emitter.on('message', listener); - emitter.emit('message', 'first'); - - // Unsubscribe before emitting again. - off(); - emitter.emit('message', 'second'); - - // Expect the listener to have only received the first event. - expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith('first'); -}; - -/** - * Tests that off() removes a listener when given the same function reference. - */ -const testEventEmitterOffRemovesListenerByReference = (): void => { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - emitter.on('message', listener); - emitter.off('message', listener); - emitter.emit('message', 'hello'); - - // Expect the listener to have been removed before the emit. - expect(listener).not.toHaveBeenCalled(); -}; - -/** - * Tests that off() does nothing when given an unknown listener reference. - */ -const testEventEmitterOffIgnoresUnknownListener = (): void => { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - emitter.on('message', listener); - - // Try to remove a different function reference. - emitter.off('message', vi.fn()); - emitter.emit('message', 'hello'); - - // Expect the original listener to still receive the event. - expect(listener).toHaveBeenCalledOnce(); -}; - -/** - * Tests that off() does nothing when called for an event type with no listeners. - */ -const testEventEmitterOffIgnoresUnregisteredEventType = (): void => { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - // Call off without ever registering this listener. - emitter.off('message', listener); - - expect(listener).not.toHaveBeenCalled(); -}; - -/** - * Tests that once() listeners are invoked only one time. - */ -const testEventEmitterOnceListenerFiresOnce = (): void => { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - emitter.once('message', listener); - emitter.emit('message', 'first'); - emitter.emit('message', 'second'); - - // Expect the listener to auto-unsubscribe after the first emit. - expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith('first'); -}; - -/** - * Tests that once() can be added when regular listeners already exist for the event type. - */ -const testEventEmitterOnceWorksWithExistingListeners = (): void => { - const emitter = new EventEmitter(); - const existingListener = vi.fn(); - const onceListener = vi.fn(); - - // Register a regular listener first so the event type already exists in the map. - emitter.on('message', existingListener); - emitter.once('message', onceListener); - emitter.emit('message', 'hello'); - - expect(existingListener).toHaveBeenCalledOnce(); - expect(onceListener).toHaveBeenCalledOnce(); -}; - -/** - * Tests that the off callback returned by once() removes the listener before it fires. - */ -const testEventEmitterOnceOffCallbackRemovesListener = (): void => { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - const off = emitter.once('message', listener); - - // Unsubscribe before the event is ever emitted. - off(); - emitter.emit('message', 'hello'); - - expect(listener).not.toHaveBeenCalled(); -}; - -/** - * Tests that removeAllListeners() clears every registered listener. - */ -const testEventEmitterRemoveAllListeners = (): void => { - const emitter = new EventEmitter(); - const messageListener = vi.fn(); - const countListener = vi.fn(); - - emitter.on('message', messageListener); - emitter.on('count', countListener); - emitter.removeAllListeners(); - - // Emit on both event types after clearing all listeners. - emitter.emit('message', 'hello'); - emitter.emit('count', 1); - - expect(messageListener).not.toHaveBeenCalled(); - expect(countListener).not.toHaveBeenCalled(); -}; - -/** - * Tests that waitFor() resolves when a matching event is emitted. - */ -const testEventEmitterWaitForResolvesOnMatch = async (): Promise => { - const emitter = new EventEmitter(); - - // Wait until an event matches the predicate. - const waitPromise = emitter.waitFor('count', (payload) => payload === 42); - - // Emit a non-matching event first, then the matching one. - emitter.emit('count', 41); - emitter.emit('count', 42); - - await expect(waitPromise).resolves.toBe(42); -}; - -/** - * Tests that waitFor() ignores non-matching events while other listeners still receive them. - */ -const testEventEmitterWaitForIgnoresNonMatchingEvents = async (): Promise => { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - const waitPromise = emitter.waitFor('message', (payload) => payload === 'done'); - - // A regular listener should still receive every emit while waitFor filters. - emitter.on('message', listener); - emitter.emit('message', 'pending'); - emitter.emit('message', 'done'); - - await expect(waitPromise).resolves.toBe('done'); - expect(listener).toHaveBeenCalledTimes(2); -}; - -/** - * Tests that waitFor() rejects when the timeout expires. - */ -const testEventEmitterWaitForRejectsOnTimeout = async (): Promise => { - vi.useFakeTimers(); - - try { - const emitter = new EventEmitter(); - - const waitPromise = emitter.waitFor('message', () => true, 100); - - // Attach the rejection handler before advancing timers so the rejection is handled. - const assertion = expect(waitPromise).rejects.toThrow('Timeout waiting for event "message"'); - - await vi.advanceTimersByTimeAsync(100); - - await assertion; - } finally { - vi.useRealTimers(); - vi.restoreAllMocks(); - } -}; - -/** - * Tests that waitFor() clears its timeout when it resolves before expiry. - */ -const testEventEmitterWaitForClearsTimeoutOnResolve = async (): Promise => { - vi.useFakeTimers(); - - try { - const emitter = new EventEmitter(); - - // Register waitFor with a timeout, then resolve it before the timer fires. - const waitPromise = emitter.waitFor('message', (payload) => payload === 'done', 100); - - emitter.emit('message', 'done'); - - await expect(waitPromise).resolves.toBe('done'); - - // If clearTimeout was not called, advancing past the timeout would reject the promise. - await vi.advanceTimersByTimeAsync(100); - } finally { - vi.useRealTimers(); - vi.restoreAllMocks(); - } -}; - -/** - * Tests that waitFor() removes its listener after resolving. - */ -const testEventEmitterWaitForRemovesListenerAfterResolve = async (): Promise => { - const emitter = new EventEmitter(); - - const waitPromise = emitter.waitFor('message', () => true); - - emitter.emit('message', 'first'); - await expect(waitPromise).resolves.toBe('first'); - - // Register a second waitFor so we can verify the first listener was cleaned up. - const secondWaitPromise = emitter.waitFor('message', (payload) => payload === 'second'); - - // Emit a payload that only the second waitFor should accept. - emitter.emit('message', 'ignored'); - - // Track whether the second waitFor resolves too early. - let resolvedEarly = false; - /* eslint-disable-next-line */ - secondWaitPromise.then(() => { - resolvedEarly = true; - }); - - // Yield so any premature resolution would have a chance to run. - await Promise.resolve(); - expect(resolvedEarly).toBe(false); - - emitter.emit('message', 'second'); - await expect(secondWaitPromise).resolves.toBe('second'); -}; - -/** - * Tests that the first debounced emit does not call clearTimeout. - */ -const testEventEmitterDebouncedFirstEmitDoesNotClearTimeout = (): void => { - vi.useFakeTimers(); - - try { - const emitter = new EventEmitter(); - const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout'); - const listener = vi.fn(); - - emitter.on('message', listener, 100); - emitter.emit('message', 'first'); - - // The first emit starts the debounce timer; there is nothing to clear yet. - expect(clearTimeoutSpy).not.toHaveBeenCalled(); - expect(listener).not.toHaveBeenCalled(); - } finally { - vi.useRealTimers(); - vi.restoreAllMocks(); - } -}; - -/** - * Tests that debounced on() listeners receive only the last payload after the debounce window. - */ -const testEventEmitterDebouncedOnListener = async (): Promise => { - vi.useFakeTimers(); - - try { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - emitter.on('message', listener, 100); - - // Emit several events in quick succession. - emitter.emit('message', 'first'); - emitter.emit('message', 'second'); - emitter.emit('message', 'third'); - - // Expect the listener to not have fired yet. - expect(listener).not.toHaveBeenCalled(); - - // Advance past the debounce window. - await vi.advanceTimersByTimeAsync(100); - - // Expect only the last payload to have been delivered. - expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith('third'); - } finally { - vi.useRealTimers(); - vi.restoreAllMocks(); - } -}; - -/** - * Tests that repeated debounced emits reset the debounce timer. - */ -const testEventEmitterDebouncedTimerResetsOnRepeatedEmits = async (): Promise => { - vi.useFakeTimers(); - - try { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - emitter.on('count', listener, 100); - emitter.emit('count', 1); - - // Advance halfway through the debounce window and emit again. - await vi.advanceTimersByTimeAsync(50); - emitter.emit('count', 2); - await vi.advanceTimersByTimeAsync(50); - - // The timer was reset, so the listener should not have fired yet. - expect(listener).not.toHaveBeenCalled(); - - // Advance the remaining time for the reset timer to expire. - await vi.advanceTimersByTimeAsync(50); - - expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith(2); - } finally { - vi.useRealTimers(); - vi.restoreAllMocks(); - } -}; - -/** - * Tests that a debounce time of zero behaves like a normal listener. - */ -const testEventEmitterZeroDebounceDoesNotDebounce = async (): Promise => { - vi.useFakeTimers(); - - try { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - // A debounce time of zero should behave like a normal listener. - emitter.on('message', listener, 0); - emitter.emit('message', 'first'); - emitter.emit('message', 'second'); - - expect(listener).toHaveBeenCalledTimes(2); - } finally { - vi.useRealTimers(); - vi.restoreAllMocks(); - } -}; - -/** - * Tests that debounced once() listeners fire once with the last payload. - */ -const testEventEmitterDebouncedOnceListener = async (): Promise => { - vi.useFakeTimers(); - - try { - const emitter = new EventEmitter(); - const listener = vi.fn(); - - emitter.once('message', listener, 100); - emitter.emit('message', 'first'); - emitter.emit('message', 'second'); - - await vi.advanceTimersByTimeAsync(100); - - // Expect the debounced once listener to fire once with the last payload. - expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith('second'); - - // Emit again after the debounce window; the once listener should stay removed. - emitter.emit('message', 'third'); - await vi.advanceTimersByTimeAsync(100); - - expect(listener).toHaveBeenCalledOnce(); - } finally { - vi.useRealTimers(); - vi.restoreAllMocks(); - } -}; - -const runTests = async (): Promise => { - test('EventEmitter: calls listeners when an event is emitted', testEventEmitterCallsListeners); - test('EventEmitter: calls multiple listeners for the same event', testEventEmitterCallsMultipleListeners); - test('EventEmitter: only calls listeners for the emitted event type', testEventEmitterCallsOnlyMatchingListeners); - test('EventEmitter: returns false when emitting with no listeners', testEventEmitterEmitReturnsFalseWithNoListeners); - test('EventEmitter: returns true when emitting with listeners', testEventEmitterEmitReturnsTrueWithListeners); - test('EventEmitter: stops calling a listener after its off callback is invoked', testEventEmitterOffCallbackRemovesListener); - test('EventEmitter: removes a listener when off is called with the same reference', testEventEmitterOffRemovesListenerByReference); - test('EventEmitter: ignores off when the listener reference is unknown', testEventEmitterOffIgnoresUnknownListener); - test('EventEmitter: ignores off for an event type with no listeners', testEventEmitterOffIgnoresUnregisteredEventType); - test('EventEmitter: calls a once listener only one time', testEventEmitterOnceListenerFiresOnce); - test('EventEmitter: registers once when listeners already exist', testEventEmitterOnceWorksWithExistingListeners); - test('EventEmitter: stops a once listener after its off callback is invoked', testEventEmitterOnceOffCallbackRemovesListener); - test('EventEmitter: removes all listeners when removeAllListeners is called', testEventEmitterRemoveAllListeners); - 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); - test('EventEmitter: clears the timeout when waitFor resolves before expiry', testEventEmitterWaitForClearsTimeoutOnResolve); - test('EventEmitter: removes the waitFor listener after it resolves', testEventEmitterWaitForRemovesListenerAfterResolve); - test('EventEmitter: does not clear a timeout on the first debounced emit', testEventEmitterDebouncedFirstEmitDoesNotClearTimeout); - test('EventEmitter: debounces on listeners', testEventEmitterDebouncedOnListener); - test('EventEmitter: resets the debounce timer on repeated emits', testEventEmitterDebouncedTimerResetsOnRepeatedEmits); - test('EventEmitter: does not debounce when debounceMilliseconds is zero', testEventEmitterZeroDebounceDoesNotDebounce); - test('EventEmitter: debounces once listeners and invokes them only once', testEventEmitterDebouncedOnceListener); -}; - -await runTests();