Dont emit to debounced listener if off was called
This commit is contained in:
+25
-19
@@ -22,8 +22,7 @@ type StoredListener<T> = {
|
|||||||
interface ListenerEntry<T> {
|
interface ListenerEntry<T> {
|
||||||
listener: StoredListener<T>;
|
listener: StoredListener<T>;
|
||||||
wrappedListener: StoredListener<T>;
|
wrappedListener: StoredListener<T>;
|
||||||
debounceTime?: number;
|
cancel: () => void;
|
||||||
once?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,9 +48,11 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
* @param debounceMilliseconds - The debounce time in milliseconds.
|
* @param debounceMilliseconds - The debounce time in milliseconds.
|
||||||
* @returns An off callback that can be called to stop listening for events.
|
* @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 {
|
on<K extends keyof T>(type: K, listener: Listener<T[K]>, debounceMilliseconds: number = 0): OffCallback {
|
||||||
|
const { cancel, listener: cancellableListener } = this.cancellable(listener);
|
||||||
|
|
||||||
// Create a wrapped listener so that the debounce can be applied.
|
// Create a wrapped listener so that the debounce can be applied.
|
||||||
const wrappedListener = debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(listener, debounceMilliseconds) : listener;
|
const wrappedListener = debounceMilliseconds > 0 ? this.debounce(cancellableListener, debounceMilliseconds) : cancellableListener;
|
||||||
|
|
||||||
// If the listeners map does not have the event type, create a new set.
|
// If the listeners map does not have the event type, create a new set.
|
||||||
if (!this.#listeners.has(type)) {
|
if (!this.#listeners.has(type)) {
|
||||||
@@ -62,13 +63,9 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
const listenerEntry: ListenerEntry<T[K]> = {
|
const listenerEntry: ListenerEntry<T[K]> = {
|
||||||
listener,
|
listener,
|
||||||
wrappedListener,
|
wrappedListener,
|
||||||
|
cancel,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set the debounce time if specified.
|
|
||||||
if (debounceMilliseconds && debounceMilliseconds > 0) {
|
|
||||||
listenerEntry.debounceTime = debounceMilliseconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the listener entry to the listeners map.
|
// Add the listener entry to the listeners map.
|
||||||
this.#listeners.get(type)?.add(listenerEntry);
|
this.#listeners.get(type)?.add(listenerEntry);
|
||||||
|
|
||||||
@@ -83,15 +80,17 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
* @param debounceMilliseconds - The debounce time in milliseconds.
|
* @param debounceMilliseconds - The debounce time in milliseconds.
|
||||||
* @returns An off callback that can be called to stop listening for events.
|
* @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 {
|
once<K extends keyof T>(type: K, listener: Listener<T[K]>, debounceMilliseconds: number = 0): OffCallback {
|
||||||
const wrappedListener: Listener<T[K]> = (detail: DeeplyReadonly<T[K]>) => {
|
const wrappedListener: Listener<T[K]> = (detail: DeeplyReadonly<T[K]>) => {
|
||||||
this.off(type, listener);
|
this.off(type, listener);
|
||||||
listener(detail);
|
listener(detail);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create a cancellable listener.
|
||||||
|
const { cancel, listener: cancellableListener } = this.cancellable(wrappedListener);
|
||||||
|
|
||||||
// Create a debounced listener.
|
// Create a debounced listener.
|
||||||
const debouncedListener =
|
const debouncedListener = debounceMilliseconds > 0 ? this.debounce(cancellableListener, debounceMilliseconds) : cancellableListener;
|
||||||
debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(wrappedListener, debounceMilliseconds) : wrappedListener;
|
|
||||||
|
|
||||||
// If the listeners map does not have the event type, create a new set.
|
// If the listeners map does not have the event type, create a new set.
|
||||||
if (!this.#listeners.has(type)) {
|
if (!this.#listeners.has(type)) {
|
||||||
@@ -102,14 +101,9 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
const listenerEntry: ListenerEntry<T[K]> = {
|
const listenerEntry: ListenerEntry<T[K]> = {
|
||||||
listener,
|
listener,
|
||||||
wrappedListener: debouncedListener,
|
wrappedListener: debouncedListener,
|
||||||
once: true,
|
cancel,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set the debounce time if specified.
|
|
||||||
if (debounceMilliseconds && debounceMilliseconds > 0) {
|
|
||||||
listenerEntry.debounceTime = debounceMilliseconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the listener entry to the listeners map.
|
// Add the listener entry to the listeners map.
|
||||||
this.#listeners.get(type)?.add(listenerEntry);
|
this.#listeners.get(type)?.add(listenerEntry);
|
||||||
|
|
||||||
@@ -133,7 +127,7 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
// Remove the listener entries from the listeners set.
|
// Remove the listener entries from the listeners set.
|
||||||
listenerEntries.forEach((entry) => {
|
listenerEntries.forEach((entry) => {
|
||||||
// Set the wrapped listener to a no-op function to prevent it from being called by debounced events after it's been removed.
|
// Set the wrapped listener to a no-op function to prevent it from being called by debounced events after it's been removed.
|
||||||
entry.wrappedListener = (): void => {};
|
entry.cancel();
|
||||||
|
|
||||||
// Remove the listener entry from the listeners set.
|
// Remove the listener entry from the listeners set.
|
||||||
listeners.delete(entry);
|
listeners.delete(entry);
|
||||||
@@ -251,4 +245,16 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
}, wait);
|
}, wait);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cancellable<K extends keyof T>(func: Listener<T[K]>): { cancel: () => void; listener: Listener<T[K]> } {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
cancel: (): boolean => (cancelled = true),
|
||||||
|
listener: (detail: DeeplyReadonly<T[K]>): void => {
|
||||||
|
if (cancelled) return;
|
||||||
|
func(detail);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,6 +261,30 @@ const testEventEmitterOnceOffCallbackRemovesListener = (): void => {
|
|||||||
expect(listener).not.toHaveBeenCalled();
|
expect(listener).not.toHaveBeenCalled();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that debounced listeners do not receive the debounced event if the listener is removed.
|
||||||
|
*/
|
||||||
|
const testEventEmitterOffCancelsPendingDebouncedCallback = async (): Promise<void> => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const emitter = new EventEmitter<TestEvents>();
|
||||||
|
const listener = vi.fn();
|
||||||
|
|
||||||
|
const off = emitter.on('message', listener, 100);
|
||||||
|
emitter.emit('message', 'first');
|
||||||
|
|
||||||
|
off();
|
||||||
|
emitter.emit('message', 'second');
|
||||||
|
await vi.advanceTimersByTimeAsync(100);
|
||||||
|
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests that removeAllListeners() clears every registered listener.
|
* Tests that removeAllListeners() clears every registered listener.
|
||||||
*/
|
*/
|
||||||
@@ -629,6 +653,10 @@ const runTests = async (): Promise<void> => {
|
|||||||
test('EventEmitter: calls a once listener only one time', testEventEmitterOnceListenerFiresOnce);
|
test('EventEmitter: calls a once listener only one time', testEventEmitterOnceListenerFiresOnce);
|
||||||
test('EventEmitter: registers once when listeners already exist', testEventEmitterOnceWorksWithExistingListeners);
|
test('EventEmitter: registers once when listeners already exist', testEventEmitterOnceWorksWithExistingListeners);
|
||||||
test('EventEmitter: stops a once listener after its off callback is invoked', testEventEmitterOnceOffCallbackRemovesListener);
|
test('EventEmitter: stops a once listener after its off callback is invoked', testEventEmitterOnceOffCallbackRemovesListener);
|
||||||
|
test(
|
||||||
|
'EventEmitter: debounced listeners do not receive the debounced event if the listener is removed',
|
||||||
|
testEventEmitterOffCancelsPendingDebouncedCallback,
|
||||||
|
);
|
||||||
test('EventEmitter: removes all listeners when removeAllListeners is called', testEventEmitterRemoveAllListeners);
|
test('EventEmitter: removes all listeners when removeAllListeners is called', testEventEmitterRemoveAllListeners);
|
||||||
test('EventEmitter: resolves waitFor when a matching event is emitted', testEventEmitterWaitForResolvesOnMatch);
|
test('EventEmitter: resolves waitFor when a matching event is emitted', testEventEmitterWaitForResolvesOnMatch);
|
||||||
test('EventEmitter: ignores non-matching events while waiting with waitFor', testEventEmitterWaitForIgnoresNonMatchingEvents);
|
test('EventEmitter: ignores non-matching events while waiting with waitFor', testEventEmitterWaitForIgnoresNonMatchingEvents);
|
||||||
|
|||||||
Reference in New Issue
Block a user