Clean up listener and timeout after waitfor reject
This commit is contained in:
+14
-7
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user