Freeze object when emitting

This commit is contained in:
2026-08-07 01:42:55 +00:00
parent abd7cc619e
commit 2b6f4e2f8e
+9 -6
View File
@@ -1,6 +1,6 @@
export type EventMap = Record<string, unknown>; export type EventMap = Record<string, unknown>;
type Listener<T> = (detail: T) => void; type Listener<T> = (detail: Readonly<T>) => void;
/** /**
* Internally permits listeners for individual event payloads to be stored * Internally permits listeners for individual event payloads to be stored
@@ -72,7 +72,7 @@ export class EventEmitter<T extends EventMap> {
* @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): OffCallback {
const wrappedListener: Listener<T[K]> = (detail: T[K]) => { const wrappedListener: Listener<T[K]> = (detail: Readonly<T[K]>) => {
this.off(type, listener); this.off(type, listener);
listener(detail); listener(detail);
}; };
@@ -131,9 +131,12 @@ export class EventEmitter<T extends EventMap> {
const listeners = this.listeners.get(type); const listeners = this.listeners.get(type);
if (!listeners) return false; if (!listeners) return false;
// Freeze the payload to make it readonly.
const readonlyPayload = Object.freeze(payload);
// Emit the event to all listeners. // Emit the event to all listeners.
listeners.forEach((entry) => { listeners.forEach((entry) => {
entry.wrappedListener(payload); entry.wrappedListener(readonlyPayload);
}); });
// Return true if there are listeners for the event, false otherwise. // Return true if there are listeners for the event, false otherwise.
@@ -154,13 +157,13 @@ export class EventEmitter<T extends EventMap> {
* @param timeoutMs - The timeout in milliseconds. * @param timeoutMs - The timeout in milliseconds.
* @returns The event payload. * @returns The event payload.
*/ */
async waitFor<K extends keyof T>(type: K, predicate: (payload: T[K]) => boolean, timeoutMs?: number): Promise<T[K]> { async waitFor<K extends keyof T>(type: K, predicate: (payload: Readonly<T[K]>) => boolean, timeoutMs?: number): Promise<Readonly<T[K]>> {
// Create a promise to wait for the event to be emitted. // Create a promise to wait for the event to be emitted.
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let timeoutId: ReturnType<typeof setTimeout> | undefined; let timeoutId: ReturnType<typeof setTimeout> | undefined;
// Create a listener function. // Create a listener function.
const listener = (payload: T[K]): void => { const listener = (payload: Readonly<T[K]>): void => {
if (predicate(payload)) { if (predicate(payload)) {
// Clean up // Clean up
this.off(type, listener); this.off(type, listener);
@@ -195,7 +198,7 @@ export class EventEmitter<T extends EventMap> {
// Create a timeout variable. // Create a timeout variable.
let timeout: ReturnType<typeof setTimeout>; let timeout: ReturnType<typeof setTimeout>;
return (detail: T[K]) => { return (detail: Readonly<T[K]>) => {
// If a debounce timer is already pending, clear it before scheduling the next one. // If a debounce timer is already pending, clear it before scheduling the next one.
if (timeout !== undefined) { if (timeout !== undefined) {
clearTimeout(timeout); clearTimeout(timeout);