Replace event-emitter with eventEmitter3
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitauth/libauth": "^3.1.0-next.8",
|
"@bitauth/libauth": "^3.1.0-next.8",
|
||||||
"@xo-cash/types": "0.0.3",
|
"@xo-cash/types": "0.0.3",
|
||||||
|
"eventemitter3": "^5.0.4",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -5690,6 +5691,12 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/execa": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitauth/libauth": "^3.1.0-next.8",
|
"@bitauth/libauth": "^3.1.0-next.8",
|
||||||
"@xo-cash/types": "0.0.3",
|
"@xo-cash/types": "0.0.3",
|
||||||
|
"eventemitter3": "^5.0.4",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
export type EventMap = Record<string, unknown>;
|
|
||||||
|
|
||||||
type Listener<T> = (detail: T) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A listener entry.
|
|
||||||
* @template T - The event type.
|
|
||||||
*/
|
|
||||||
interface ListenerEntry<T> {
|
|
||||||
listener: Listener<T>;
|
|
||||||
wrappedListener: Listener<T>;
|
|
||||||
debounceTime?: number;
|
|
||||||
once?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OffCallback = () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple event emitter implementation.
|
|
||||||
* @template T - The event map type.
|
|
||||||
*/
|
|
||||||
export class EventEmitter<T extends EventMap> {
|
|
||||||
/**
|
|
||||||
* The listeners map.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private listeners: Map<keyof T, Set<ListenerEntry<T[keyof T]>>> = 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<K extends keyof T>(type: K, listener: Listener<T[K]>, 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<T[K]> = {
|
|
||||||
listener,
|
|
||||||
wrappedListener,
|
|
||||||
...(debounceMilliseconds !== undefined ? { debounceTime: debounceMilliseconds } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add the listener entry to the listeners map.
|
|
||||||
this.listeners.get(type)?.add(listenerEntry as ListenerEntry<T[keyof T]>);
|
|
||||||
|
|
||||||
// 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<K extends keyof T>(type: K, listener: Listener<T[K]>, debounceMilliseconds?: number): OffCallback {
|
|
||||||
const wrappedListener: Listener<T[K]> = (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<T[K]> = {
|
|
||||||
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<T[keyof T]>);
|
|
||||||
|
|
||||||
// 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<K extends keyof T>(type: K, listener: Listener<T[K]>): 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<K extends keyof T>(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<K extends keyof T>(type: K, predicate: (payload: T[K]) => boolean, timeoutMs?: number): Promise<T[K]> {
|
|
||||||
// Create a promise to wait for the event to be emitted.
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | 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<K extends keyof T>(func: Listener<T[K]>, wait: number): Listener<T[K]> {
|
|
||||||
// Create a timeout variable.
|
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './event-emitter.ts';
|
|
||||||
export * from './exponential-backoff.ts';
|
export * from './exponential-backoff.ts';
|
||||||
export * from './extended-json.ts';
|
export * from './extended-json.ts';
|
||||||
export * from './misc.ts';
|
export * from './misc.ts';
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { SSESessionOptions, SSESessionEventMap, SSEvent } from './types.ts';
|
import type { SSESessionOptions, SSESessionEventMap, SSEvent } from './types.ts';
|
||||||
|
|
||||||
|
import { EventEmitter } from 'eventemitter3'
|
||||||
|
|
||||||
import { HTTPError, ResponseBodyNullError } from '../errors.ts';
|
import { HTTPError, ResponseBodyNullError } from '../errors.ts';
|
||||||
import { tryAsync } from '../misc.ts';
|
import { tryAsync } from '../misc.ts';
|
||||||
import { EventEmitter } from '../event-emitter.ts';
|
|
||||||
import { ExponentialBackoff } from '../exponential-backoff.ts';
|
import { ExponentialBackoff } from '../exponential-backoff.ts';
|
||||||
|
|
||||||
import { SSEEventParser } from './sse-event-parser.ts';
|
import { SSEEventParser } from './sse-event-parser.ts';
|
||||||
|
|||||||
@@ -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<TestEvents>();
|
|
||||||
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<TestEvents>();
|
|
||||||
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<TestEvents>();
|
|
||||||
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<TestEvents>();
|
|
||||||
|
|
||||||
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<TestEvents>();
|
|
||||||
|
|
||||||
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<TestEvents>();
|
|
||||||
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<TestEvents>();
|
|
||||||
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<TestEvents>();
|
|
||||||
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<TestEvents>();
|
|
||||||
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<TestEvents>();
|
|
||||||
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<TestEvents>();
|
|
||||||
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<TestEvents>();
|
|
||||||
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<TestEvents>();
|
|
||||||
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<void> => {
|
|
||||||
const emitter = new EventEmitter<TestEvents>();
|
|
||||||
|
|
||||||
// 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<void> => {
|
|
||||||
const emitter = new EventEmitter<TestEvents>();
|
|
||||||
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<void> => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const emitter = new EventEmitter<TestEvents>();
|
|
||||||
|
|
||||||
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<void> => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const emitter = new EventEmitter<TestEvents>();
|
|
||||||
|
|
||||||
// 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<void> => {
|
|
||||||
const emitter = new EventEmitter<TestEvents>();
|
|
||||||
|
|
||||||
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<TestEvents>();
|
|
||||||
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<void> => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const emitter = new EventEmitter<TestEvents>();
|
|
||||||
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<void> => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const emitter = new EventEmitter<TestEvents>();
|
|
||||||
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<void> => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const emitter = new EventEmitter<TestEvents>();
|
|
||||||
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<void> => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const emitter = new EventEmitter<TestEvents>();
|
|
||||||
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<void> => {
|
|
||||||
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();
|
|
||||||
Reference in New Issue
Block a user