Clean up listener and timeout after waitfor reject

This commit is contained in:
2026-08-10 02:00:12 +00:00
parent 91649558a8
commit da412e4ad8
2 changed files with 79 additions and 7 deletions
+14 -7
View File
@@ -190,22 +190,29 @@ export class EventEmitter<T extends EventMap> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let timeoutId: ReturnType<typeof setTimeout> | undefined; let timeoutId: ReturnType<typeof setTimeout> | undefined;
// Create a cleanup function to remove the listener and clear the timeout if it is still pending.
const cleanup = (listener: Listener<T[K]>): void => {
// Remove the listener from the listeners map.
this.off(type, listener);
// Clear the timeout if it is still pending.
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
};
// Create a listener function. // Create a listener function.
const listener = (payload: Readonly<T[K]>): void => { const listener = (payload: DeeplyReadonly<T[K]>): void => {
try { try {
// If the event payload does not match the predicate condition, return. // If the event payload does not match the predicate condition, return.
if (!predicate(payload)) { if (!predicate(payload)) {
return; return;
} }
// Clean up cleanup(listener);
this.off(type, listener);
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
resolve(payload); resolve(payload);
} catch (error) { } catch (error) {
cleanup(listener);
reject(error); reject(error);
} }
}; };
+65
View File
@@ -551,6 +551,66 @@ const testEventEmitterWaitForRejectsOnPredicateError = async (): Promise<void> =
expect(listener).not.toHaveBeenCalled(); expect(listener).not.toHaveBeenCalled();
}; };
/**
* Tests that waitFor() removes its listener after the predicate function throws an error.
*/
const testEventEmitterWaitForRemovesListenerAfterPredicateError = async (): Promise<void> => {
const emitter = new EventEmitter<TestEvents>();
// Create a predicate function that throws an error.
const predicate = vi.fn().mockImplementation(() => {
throw new Error('predicate error');
});
// Wait for the predicate function to throw an error.
const waitPromise = emitter.waitFor('message', predicate);
// Emit an event, expecting the predicate function to throw an error.
emitter.emit('message', 'hello');
await expect(waitPromise).rejects.toThrow('predicate error');
// Expect the predicate function to have been called once.
expect(predicate).toHaveBeenCalledTimes(1);
// A later emit must not invoke the failed waitFor predicate again.
emitter.emit('message', 'again');
// Expect the predicate function to still have been called once.
expect(predicate).toHaveBeenCalledTimes(1);
};
/**
* Tests that waitFor() clears its timeout when the predicate function throws an error.
*/
const testEventEmitterWaitForClearsTimeoutAfterPredicateError = async (): Promise<void> => {
vi.useFakeTimers();
try {
const emitter = new EventEmitter<TestEvents>();
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout');
const waitPromise = emitter.waitFor(
'message',
(): boolean => {
throw new Error('predicate error');
},
100,
);
emitter.emit('message', 'hello');
await expect(waitPromise).rejects.toThrow('predicate error');
// The timeout scheduled for waitFor must be cleared on predicate failure.
expect(clearTimeoutSpy).toHaveBeenCalled();
// Advancing past the original timeout must not produce a second rejection path.
await vi.advanceTimersByTimeAsync(100);
} finally {
vi.useRealTimers();
vi.restoreAllMocks();
}
};
const runTests = async (): Promise<void> => { const runTests = async (): Promise<void> => {
test('EventEmitter: calls listeners when an event is emitted', testEventEmitterCallsListeners); test('EventEmitter: calls listeners when an event is emitted', testEventEmitterCallsListeners);
test('EventEmitter: calls multiple listeners for the same event', testEventEmitterCallsMultipleListeners); test('EventEmitter: calls multiple listeners for the same event', testEventEmitterCallsMultipleListeners);
@@ -581,6 +641,11 @@ const runTests = async (): Promise<void> => {
test('EventEmitter: does not debounce when debounceMilliseconds is zero', testEventEmitterZeroDebounceDoesNotDebounce); test('EventEmitter: does not debounce when debounceMilliseconds is zero', testEventEmitterZeroDebounceDoesNotDebounce);
test('EventEmitter: debounces once listeners and invokes them only once', testEventEmitterDebouncedOnceListener); test('EventEmitter: debounces once listeners and invokes them only once', testEventEmitterDebouncedOnceListener);
test('EventEmitter: rejects waitFor when the predicate function throws', testEventEmitterWaitForRejectsOnPredicateError); test('EventEmitter: rejects waitFor when the predicate function throws', testEventEmitterWaitForRejectsOnPredicateError);
test(
'EventEmitter: removes the waitFor listener after it rejects due to predicate error',
testEventEmitterWaitForRemovesListenerAfterPredicateError,
);
test('EventEmitter: clears the timeout when waitFor rejects due to predicate error', testEventEmitterWaitForClearsTimeoutAfterPredicateError);
}; };
await runTests(); await runTests();