From 78311487a485050e1b67c08c68e2f80dceb63321 Mon Sep 17 00:00:00 2001 From: Harvmaster Date: Mon, 10 Aug 2026 01:58:04 +0000 Subject: [PATCH 1/8] Deeply freeze object --- source/event-emitter.ts | 21 ++++++++++++------ source/misc.ts | 25 ++++++++++++++++++++++ source/types.ts | 11 ++++++++++ test/event-emitter.test.ts | 44 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 source/misc.ts create mode 100644 source/types.ts diff --git a/source/event-emitter.ts b/source/event-emitter.ts index 0b753c0..6e4fc26 100644 --- a/source/event-emitter.ts +++ b/source/event-emitter.ts @@ -1,13 +1,17 @@ +import type { DeeplyReadonly } from './types.ts'; + +import { deepFreeze } from './misc.ts'; + export type EventMap = Record; -type Listener = (detail: Readonly) => void; +type Listener = (detail: DeeplyReadonly) => void; /** * Internally permits listeners for individual event payloads to be stored * in a collection typed with the union of all event payloads. */ type StoredListener = { - bivarianceHack(detail: Readonly): void; + bivarianceHack(detail: DeeplyReadonly): void; }['bivarianceHack']; /** @@ -79,7 +83,7 @@ export class EventEmitter { * @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: Readonly) => { + const wrappedListener: Listener = (detail: DeeplyReadonly) => { this.off(type, listener); listener(detail); }; @@ -151,8 +155,11 @@ export class EventEmitter { const listeners = this.#listeners.get(type); if (!listeners) return false; - // Freeze the payload to make it readonly. - const readonlyPayload = Object.freeze(payload); + // Clone the payload to avoid freezing the original object. + const payloadClone = structuredClone(payload); + + // Freeze the cloned payload to make it readonly. + const readonlyPayload = deepFreeze(payloadClone); // Emit the event to all listeners. listeners.forEach((entry) => { @@ -177,7 +184,7 @@ export class EventEmitter { * @param timeoutMs - The timeout in milliseconds. * @returns The event payload. */ - async waitFor(type: K, predicate: (payload: Readonly) => boolean, timeoutMs?: number): Promise> { + async waitFor(type: K, predicate: (payload: DeeplyReadonly) => boolean, timeoutMs?: number): Promise> { // Create a promise to wait for the event to be emitted. return new Promise((resolve, reject) => { let timeoutId: ReturnType | undefined; @@ -225,7 +232,7 @@ export class EventEmitter { // Create a timeout variable. let timeout: ReturnType; - return (detail: Readonly) => { + return (detail: DeeplyReadonly) => { // If a debounce timer is already pending, clear it before scheduling the next one. if (timeout !== undefined) { clearTimeout(timeout); diff --git a/source/misc.ts b/source/misc.ts new file mode 100644 index 0000000..157ef72 --- /dev/null +++ b/source/misc.ts @@ -0,0 +1,25 @@ +import { DeeplyReadonly } from "./types"; + +/** + * Recursively freezes an object by iterating over all properties and freezing them. + * @param obj - The object to freeze. + * @returns The frozen object. + */ +export const deepFreeze = (value: T): DeeplyReadonly => { + if ( + value !== null && + (typeof value === 'object' || typeof value === 'function') + ) { + for (const key of Reflect.ownKeys(value)) { + const descriptor = Reflect.getOwnPropertyDescriptor(value, key); + + if (descriptor && 'value' in descriptor) { + deepFreeze(descriptor.value); + } + } + + Object.freeze(value); + } + + return value; +} diff --git a/source/types.ts b/source/types.ts new file mode 100644 index 0000000..b643cde --- /dev/null +++ b/source/types.ts @@ -0,0 +1,11 @@ +/** + * A deeply readonly type. + * @template T - The type to make deeply readonly. + * @returns The deeply readonly type. + */ +export type DeeplyReadonly = { + readonly [K in keyof T]: + T[K] extends (...args: never[]) => unknown + ? T[K] + : DeeplyReadonly; +}; \ No newline at end of file diff --git a/test/event-emitter.test.ts b/test/event-emitter.test.ts index 7b1eb71..affab82 100644 --- a/test/event-emitter.test.ts +++ b/test/event-emitter.test.ts @@ -89,6 +89,49 @@ const testEventEmitterEmitReturnsTrueWithListeners = (): void => { expect(hasListeners).toBe(true); }; +/** + * Tests that emitted events cannot be mutated. + */ +const testEventEmitterEmittedEventsCannotBeMutated = async (): Promise => { + vi.useFakeTimers(); + + try { + const emitter = new EventEmitter(); + const listener = vi.fn(); + const payload = { nested: { value: 1 } }; + + emitter.on('nested', listener, 100); + + // Arm the debounce timer with the current payload. + emitter.emit('nested', payload); + + // Mutate the original object while the timer is still pending. + payload.nested.value = 999; + + await vi.advanceTimersByTimeAsync(100); + + // The listener must receive a snapshot from emit time, not the mutated value. + expect(listener).toHaveBeenCalledOnce(); + + // Expect the listener to have received the original payload object without any mutations + expect(listener).toHaveBeenCalledWith({ + nested: { + value: 1, + }, + }); + + // Expect the payload object to have been mutated + expect(payload).toStrictEqual({ + nested: { + value: 999, + }, + }); + } finally { + vi.useRealTimers(); + vi.restoreAllMocks(); + } +}; + /** * Tests that the off callback returned by on() removes the listener. */ @@ -514,6 +557,7 @@ const runTests = async (): Promise => { 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: emitted events cannot be mutated', testEventEmitterEmittedEventsCannotBeMutated); 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( From 296ab4aadf0cd24d2623f1fb4b237b6424ea8803 Mon Sep 17 00:00:00 2001 From: Harvmaster Date: Mon, 10 Aug 2026 01:58:37 +0000 Subject: [PATCH 2/8] Custom waitFor timeout error --- source/errors.ts | 9 +++++++++ source/event-emitter.ts | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 source/errors.ts diff --git a/source/errors.ts b/source/errors.ts new file mode 100644 index 0000000..35f2a33 --- /dev/null +++ b/source/errors.ts @@ -0,0 +1,9 @@ +/** + * Error thrown when a waitFor timeout is reached + */ +export class WaitForTimeoutError extends Error { + constructor(type: string) { + super(`Timeout waiting for event "${type}"`); + this.name = 'WaitForTimeoutError'; + } +} diff --git a/source/event-emitter.ts b/source/event-emitter.ts index 6e4fc26..ccc0dd5 100644 --- a/source/event-emitter.ts +++ b/source/event-emitter.ts @@ -1,5 +1,6 @@ import type { DeeplyReadonly } from './types.ts'; +import { WaitForTimeoutError } from './errors.ts'; import { deepFreeze } from './misc.ts'; export type EventMap = Record; @@ -213,7 +214,7 @@ export class EventEmitter { if (timeoutMs !== undefined) { timeoutId = setTimeout(() => { this.off(type, listener); - reject(new Error(`Timeout waiting for event "${String(type)}"`)); + reject(new WaitForTimeoutError(String(type))); }, timeoutMs); } From bc28a7e96a5e38a2e550c17b453604c4193f7ab9 Mon Sep 17 00:00:00 2001 From: Harvmaster Date: Mon, 10 Aug 2026 02:00:12 +0000 Subject: [PATCH 3/8] Clean up listener and timeout after waitfor reject --- source/event-emitter.ts | 21 ++++++++---- test/event-emitter.test.ts | 65 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/source/event-emitter.ts b/source/event-emitter.ts index ccc0dd5..f99bee4 100644 --- a/source/event-emitter.ts +++ b/source/event-emitter.ts @@ -190,22 +190,29 @@ export class EventEmitter { return new Promise((resolve, reject) => { let timeoutId: ReturnType | undefined; + // Create a cleanup function to remove the listener and clear the timeout if it is still pending. + const cleanup = (listener: Listener): 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. - const listener = (payload: Readonly): void => { + const listener = (payload: DeeplyReadonly): void => { try { // If the event payload does not match the predicate condition, return. if (!predicate(payload)) { return; } - // Clean up - this.off(type, listener); - if (timeoutId !== undefined) { - clearTimeout(timeoutId); - } - + cleanup(listener); resolve(payload); } catch (error) { + cleanup(listener); reject(error); } }; diff --git a/test/event-emitter.test.ts b/test/event-emitter.test.ts index affab82..d14f6fc 100644 --- a/test/event-emitter.test.ts +++ b/test/event-emitter.test.ts @@ -551,6 +551,66 @@ const testEventEmitterWaitForRejectsOnPredicateError = async (): Promise = expect(listener).not.toHaveBeenCalled(); }; +/** + * Tests that waitFor() removes its listener after the predicate function throws an error. + */ +const testEventEmitterWaitForRemovesListenerAfterPredicateError = async (): Promise => { + const emitter = new EventEmitter(); + + // 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 => { + vi.useFakeTimers(); + + try { + const emitter = new EventEmitter(); + 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 => { test('EventEmitter: calls listeners when an event is emitted', testEventEmitterCallsListeners); test('EventEmitter: calls multiple listeners for the same event', testEventEmitterCallsMultipleListeners); @@ -581,6 +641,11 @@ const runTests = async (): Promise => { test('EventEmitter: does not debounce when debounceMilliseconds is zero', testEventEmitterZeroDebounceDoesNotDebounce); test('EventEmitter: debounces once listeners and invokes them only once', testEventEmitterDebouncedOnceListener); 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(); From 57e809157b06c5e9a54b6b3f839e51d4233c4c82 Mon Sep 17 00:00:00 2001 From: Harvmaster Date: Mon, 10 Aug 2026 02:02:44 +0000 Subject: [PATCH 4/8] Dont emit to debounced listener if off was called --- source/event-emitter.ts | 44 ++++++++++++++++++++++---------------- test/event-emitter.test.ts | 28 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/source/event-emitter.ts b/source/event-emitter.ts index f99bee4..caaa66d 100644 --- a/source/event-emitter.ts +++ b/source/event-emitter.ts @@ -22,8 +22,7 @@ type StoredListener = { interface ListenerEntry { listener: StoredListener; wrappedListener: StoredListener; - debounceTime?: number; - once?: boolean; + cancel: () => void; } /** @@ -49,9 +48,11 @@ export class EventEmitter { * @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 { + on(type: K, listener: Listener, debounceMilliseconds: number = 0): OffCallback { + const { cancel, listener: cancellableListener } = this.cancellable(listener); + // 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 (!this.#listeners.has(type)) { @@ -62,13 +63,9 @@ export class EventEmitter { const listenerEntry: ListenerEntry = { listener, wrappedListener, + cancel, }; - // Set the debounce time if specified. - if (debounceMilliseconds && debounceMilliseconds > 0) { - listenerEntry.debounceTime = debounceMilliseconds; - } - // Add the listener entry to the listeners map. this.#listeners.get(type)?.add(listenerEntry); @@ -83,15 +80,17 @@ export class EventEmitter { * @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 { + once(type: K, listener: Listener, debounceMilliseconds: number = 0): OffCallback { const wrappedListener: Listener = (detail: DeeplyReadonly) => { this.off(type, listener); listener(detail); }; + // Create a cancellable listener. + const { cancel, listener: cancellableListener } = this.cancellable(wrappedListener); + // Create a debounced listener. - const debouncedListener = - debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(wrappedListener, debounceMilliseconds) : wrappedListener; + const debouncedListener = debounceMilliseconds > 0 ? this.debounce(cancellableListener, debounceMilliseconds) : cancellableListener; // If the listeners map does not have the event type, create a new set. if (!this.#listeners.has(type)) { @@ -102,14 +101,9 @@ export class EventEmitter { const listenerEntry: ListenerEntry = { listener, 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. this.#listeners.get(type)?.add(listenerEntry); @@ -133,7 +127,7 @@ export class EventEmitter { // Remove the listener entries from the listeners set. 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. - entry.wrappedListener = (): void => {}; + entry.cancel(); // Remove the listener entry from the listeners set. listeners.delete(entry); @@ -251,4 +245,16 @@ export class EventEmitter { }, wait); }; } + + private cancellable(func: Listener): { cancel: () => void; listener: Listener } { + let cancelled = false; + + return { + cancel: (): boolean => (cancelled = true), + listener: (detail: DeeplyReadonly): void => { + if (cancelled) return; + func(detail); + }, + }; + } } diff --git a/test/event-emitter.test.ts b/test/event-emitter.test.ts index d14f6fc..415ba3f 100644 --- a/test/event-emitter.test.ts +++ b/test/event-emitter.test.ts @@ -261,6 +261,30 @@ const testEventEmitterOnceOffCallbackRemovesListener = (): void => { expect(listener).not.toHaveBeenCalled(); }; +/** + * Tests that debounced listeners do not receive the debounced event if the listener is removed. + */ +const testEventEmitterOffCancelsPendingDebouncedCallback = async (): Promise => { + vi.useFakeTimers(); + + try { + const emitter = new EventEmitter(); + 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. */ @@ -629,6 +653,10 @@ const runTests = async (): Promise => { 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: 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: resolves waitFor when a matching event is emitted', testEventEmitterWaitForResolvesOnMatch); test('EventEmitter: ignores non-matching events while waiting with waitFor', testEventEmitterWaitForIgnoresNonMatchingEvents); From ef797a5f2036883fa82d92b10c4a178a4daaf229 Mon Sep 17 00:00:00 2001 From: Harvmaster Date: Mon, 10 Aug 2026 02:03:58 +0000 Subject: [PATCH 5/8] Fix removeAllListeners --- source/event-emitter.ts | 10 +++++++--- test/event-emitter.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/source/event-emitter.ts b/source/event-emitter.ts index caaa66d..9c6b7ed 100644 --- a/source/event-emitter.ts +++ b/source/event-emitter.ts @@ -133,8 +133,8 @@ export class EventEmitter { listeners.delete(entry); }); - // If no listener was provided, remove the listeners set from the listeners map. - if (!listener) { + // If no listener was provided and no listeners are left for the event type, remove the listeners set from the listeners map. + if (!listener || this.#listeners.get(type)?.size === 0) { this.#listeners.delete(type); } } @@ -169,7 +169,11 @@ export class EventEmitter { * Remove all listeners. */ removeAllListeners(): void { - this.#listeners.clear(); + for (const [ type, listeners ] of this.#listeners.entries()) { + listeners.forEach((entry) => { + this.off(type, entry.listener); + }); + } } /** diff --git a/test/event-emitter.test.ts b/test/event-emitter.test.ts index 415ba3f..dd32b33 100644 --- a/test/event-emitter.test.ts +++ b/test/event-emitter.test.ts @@ -305,6 +305,31 @@ const testEventEmitterRemoveAllListeners = (): void => { expect(countListener).not.toHaveBeenCalled(); }; +/** + * Tests that removeAllListeners() cancels a pending debounced callback. + */ +const testEventEmitterRemoveAllListenersCancelsPendingDebouncedCallback = async (): Promise => { + vi.useFakeTimers(); + + try { + const emitter = new EventEmitter(); + const listener = vi.fn(); + + // Arm a debounce timer, then clear every listener before it expires. + emitter.on('message', listener, 100); + emitter.emit('message', 'should not arrive'); + emitter.removeAllListeners(); + + await vi.advanceTimersByTimeAsync(100); + + // Cleared listeners must not receive delayed debounced delivery. + expect(listener).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + vi.restoreAllMocks(); + } +}; + /** * Tests that waitFor() resolves when a matching event is emitted. */ @@ -658,6 +683,10 @@ const runTests = async (): Promise => { testEventEmitterOffCancelsPendingDebouncedCallback, ); test('EventEmitter: removes all listeners when removeAllListeners is called', testEventEmitterRemoveAllListeners); + test( + 'EventEmitter: cancels a pending debounced callback when removeAllListeners is called', + testEventEmitterRemoveAllListenersCancelsPendingDebouncedCallback, + ); 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); From 95dc998b93eacfa47a4b01e50ab2b4bb51509ed2 Mon Sep 17 00:00:00 2001 From: Harvmaster Date: Mon, 10 Aug 2026 02:04:30 +0000 Subject: [PATCH 6/8] Ignore errors thrown by listeners --- source/event-emitter.ts | 6 +++++- test/event-emitter.test.ts | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/source/event-emitter.ts b/source/event-emitter.ts index 9c6b7ed..96c200a 100644 --- a/source/event-emitter.ts +++ b/source/event-emitter.ts @@ -158,7 +158,11 @@ export class EventEmitter { // Emit the event to all listeners. listeners.forEach((entry) => { - entry.wrappedListener(readonlyPayload); + try { + entry.wrappedListener(readonlyPayload); + } catch (error) { + console.error(error); + } }); // Return true if there are listeners for the event, false otherwise. diff --git a/test/event-emitter.test.ts b/test/event-emitter.test.ts index dd32b33..865a68e 100644 --- a/test/event-emitter.test.ts +++ b/test/event-emitter.test.ts @@ -89,6 +89,24 @@ const testEventEmitterEmitReturnsTrueWithListeners = (): void => { expect(hasListeners).toBe(true); }; +/** + * Tests that EventEmitter.emit continues after a listener throws an error. + */ +const testEventEmitterEmitContinuesAfterListenerThrows = (): void => { + const emitter = new EventEmitter(); + const secondListener = vi.fn(); + + emitter.on('message', (): void => { + throw new Error('listener failure'); + }); + emitter.on('message', secondListener); + + emitter.emit('message', 'hello'); + + expect(secondListener).toHaveBeenCalledOnce(); + expect(secondListener).toHaveBeenCalledWith('hello'); +}; + /** * Tests that emitted events cannot be mutated. */ @@ -666,6 +684,7 @@ const runTests = async (): Promise => { 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: continues after a listener throws an error', testEventEmitterEmitContinuesAfterListenerThrows); test('EventEmitter: emitted events cannot be mutated', testEventEmitterEmittedEventsCannotBeMutated); 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); From c5b0d484d4aaffc329d095aacbe280c0a03e7a1d Mon Sep 17 00:00:00 2001 From: Harvmaster Date: Mon, 10 Aug 2026 02:04:51 +0000 Subject: [PATCH 7/8] Formatting --- source/event-emitter.ts | 6 +++++- source/misc.ts | 17 +++++++---------- source/types.ts | 7 ++----- test/event-emitter.test.ts | 1 + 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/source/event-emitter.ts b/source/event-emitter.ts index 96c200a..7a82c49 100644 --- a/source/event-emitter.ts +++ b/source/event-emitter.ts @@ -187,7 +187,11 @@ export class EventEmitter { * @param timeoutMs - The timeout in milliseconds. * @returns The event payload. */ - async waitFor(type: K, predicate: (payload: DeeplyReadonly) => boolean, timeoutMs?: number): Promise> { + async waitFor( + type: K, + predicate: (payload: DeeplyReadonly) => boolean, + timeoutMs?: number, + ): Promise> { // Create a promise to wait for the event to be emitted. return new Promise((resolve, reject) => { let timeoutId: ReturnType | undefined; diff --git a/source/misc.ts b/source/misc.ts index 157ef72..87aff34 100644 --- a/source/misc.ts +++ b/source/misc.ts @@ -1,4 +1,4 @@ -import { DeeplyReadonly } from "./types"; +import type { DeeplyReadonly } from './types'; /** * Recursively freezes an object by iterating over all properties and freezing them. @@ -6,20 +6,17 @@ import { DeeplyReadonly } from "./types"; * @returns The frozen object. */ export const deepFreeze = (value: T): DeeplyReadonly => { - if ( - value !== null && - (typeof value === 'object' || typeof value === 'function') - ) { + if (value !== null && (typeof value === 'object' || typeof value === 'function')) { for (const key of Reflect.ownKeys(value)) { - const descriptor = Reflect.getOwnPropertyDescriptor(value, key); + const descriptor = Reflect.getOwnPropertyDescriptor(value, key); - if (descriptor && 'value' in descriptor) { - deepFreeze(descriptor.value); - } + if (descriptor && 'value' in descriptor) { + deepFreeze(descriptor.value); + } } Object.freeze(value); } return value; -} +}; diff --git a/source/types.ts b/source/types.ts index b643cde..e1408bd 100644 --- a/source/types.ts +++ b/source/types.ts @@ -4,8 +4,5 @@ * @returns The deeply readonly type. */ export type DeeplyReadonly = { - readonly [K in keyof T]: - T[K] extends (...args: never[]) => unknown - ? T[K] - : DeeplyReadonly; -}; \ No newline at end of file + readonly [K in keyof T]: T[K] extends (...args: never[]) => unknown ? T[K] : DeeplyReadonly; +}; diff --git a/test/event-emitter.test.ts b/test/event-emitter.test.ts index 865a68e..3c66e1d 100644 --- a/test/event-emitter.test.ts +++ b/test/event-emitter.test.ts @@ -5,6 +5,7 @@ import { EventEmitter } from '../source/event-emitter.ts'; type TestEvents = { message: string; count: number; + nested: { nested: { value: number } }; }; /** From f8941dab01988f52d26503d2f259c3a3e22a9e32 Mon Sep 17 00:00:00 2001 From: Harvmaster Date: Mon, 10 Aug 2026 02:35:12 +0000 Subject: [PATCH 8/8] Audit --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41ad782..1be29c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7715,9 +7715,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ {