diff --git a/packages/octagonal-wheels/dist/events/EventHub.d.ts b/packages/octagonal-wheels/dist/events/EventHub.d.ts index 007a3846..97a0990a 100644 --- a/packages/octagonal-wheels/dist/events/EventHub.d.ts +++ b/packages/octagonal-wheels/dist/events/EventHub.d.ts @@ -22,6 +22,7 @@ export declare class EventHub { private _assigned; private _allAssigned; private _issueSignal; + private _listen; /** * Emits an event without data. * @@ -46,7 +47,8 @@ export declare class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. - * @returns A function to remove the event listener. + * @param options - Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. + * @returns An idempotent function that removes only this registration. */ on(event: EventTypeWithoutData, callback: (e: Event) => void | Promise, options?: AddEventListenerOptions): () => void; /** @@ -56,13 +58,17 @@ export declare class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. - * @returns A function to remove the event listener. + * @param options - Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. + * @returns An idempotent function that removes only this registration. */ on(event: EventTypeWithData, callback: (e: Event, data: ET[K]) => void | Promise, options?: AddEventListenerOptions): () => void; /** - * Removes an event listener for a specific event. - * @param event - * @param callback + * Removes current event registrations in bulk. + * + * Prefer the disposer returned by `on`, `onEvent`, `once`, or `onceEvent` when removing one registration. + * + * @param event - The event whose registrations should be removed. + * @param callback - The callback whose registrations should be removed. Omit it to remove every registration for the event. */ off(event: EventType, callback?: CallableFunction): void; /** @@ -76,7 +82,8 @@ export declare class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. - * @returns A function to remove the event listener. + * @param options - Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. + * @returns An idempotent function that removes only this registration. */ onEvent(event: EventTypeWithoutData, callback: () => any, options?: AddEventListenerOptions): () => void; /** @@ -86,7 +93,8 @@ export declare class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. - * @returns A function to remove the event listener. + * @param options - Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. + * @returns An idempotent function that removes only this registration. */ onEvent(event: EventTypeWithData, callback: (data: ET[K]) => any, options?: AddEventListenerOptions): () => void; /** @@ -96,8 +104,10 @@ export declare class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. + * @param options - Native `EventTarget` listener options. The `once` option is always treated as `true`. + * @returns An idempotent function that removes only this registration. */ - once(event: EventTypeWithoutData, callback: (e: Event) => void): () => void; + once(event: EventTypeWithoutData, callback: (e: Event) => void, options?: AddEventListenerOptions): () => void; /** * Registers a one-time event listener for a specific event. * @@ -105,8 +115,10 @@ export declare class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. + * @param options - Native `EventTarget` listener options. The `once` option is always treated as `true`. + * @returns An idempotent function that removes only this registration. */ - once(event: EventTypeWithData, callback: (e: Event, data: ET[K]) => void): () => void; + once(event: EventTypeWithData, callback: (e: Event, data: ET[K]) => void, options?: AddEventListenerOptions): () => void; /** * Registers a one-time event listener for a specific event, with a callback that only receives the event data. * @@ -114,8 +126,10 @@ export declare class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. + * @param options - Native `EventTarget` listener options. The `once` option is always treated as `true`. + * @returns An idempotent function that removes only this registration. */ - onceEvent(event: EventTypeWithoutData, callback: () => void): () => void; + onceEvent(event: EventTypeWithoutData, callback: () => void, options?: AddEventListenerOptions): () => void; /** * Registers a one-time event listener for a specific event, with a callback that only receives the event data. * @@ -123,8 +137,10 @@ export declare class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. + * @param options - Native `EventTarget` listener options. The `once` option is always treated as `true`. + * @returns An idempotent function that removes only this registration. */ - onceEvent(event: EventTypeWithData, callback: (data: ET[K]) => void): () => void; + onceEvent(event: EventTypeWithData, callback: (data: ET[K]) => void, options?: AddEventListenerOptions): () => void; /** * Waits for a specific event to be emitted. * diff --git a/packages/octagonal-wheels/dist/events/EventHub.js b/packages/octagonal-wheels/dist/events/EventHub.js index 8c1e4cb5..7d54a5cd 100644 --- a/packages/octagonal-wheels/dist/events/EventHub.js +++ b/packages/octagonal-wheels/dist/events/EventHub.js @@ -36,54 +36,79 @@ class EventHub { }); this._emitter = emitter ?? new EventTarget(); } - _issueSignal(key, callback) { - let assigned = this._assigned.get(key); - if (assigned === undefined) { - assigned = new WeakMap(); - } - const controllerRef = assigned.get(callback); - let controller = controllerRef?.deref(); - if (!controller || controller.signal.aborted) { - controller = new AbortController(); - const refController = new FallbackWeakRef(controller); - controller.signal.addEventListener("abort", () => { - this._assigned.get(key)?.delete(callback); - this._allAssigned.get(key)?.delete(refController); - }, { once: true }); - assigned.set(callback, refController); - this._assigned.set(key, assigned); - const allAssigned = this._allAssigned.get(key) ?? new Set(); - allAssigned.add(refController); - this._allAssigned.set(key, allAssigned); - return controller; + _issueSignal(key, callback, sourceSignal) { + const controller = new AbortController(); + const controllerRef = new FallbackWeakRef(controller); + const assigned = this._assigned.get(key) ?? new WeakMap(); + const callbackControllers = assigned.get(callback) ?? new Set(); + const allAssigned = this._allAssigned.get(key) ?? new Set(); + const abortFromSource = () => controller.abort(); + controller.signal.addEventListener("abort", () => { + sourceSignal?.removeEventListener("abort", abortFromSource); + callbackControllers.delete(controllerRef); + if (callbackControllers.size === 0) { + assigned.delete(callback); + } + allAssigned.delete(controllerRef); + if (allAssigned.size === 0) { + this._assigned.delete(key); + this._allAssigned.delete(key); + } + }, { once: true }); + callbackControllers.add(controllerRef); + assigned.set(callback, callbackControllers); + this._assigned.set(key, assigned); + allAssigned.add(controllerRef); + this._allAssigned.set(key, allAssigned); + if (sourceSignal) { + sourceSignal.addEventListener("abort", abortFromSource, { once: true }); + if (sourceSignal.aborted) { + controller.abort(); + } } return controller; } + _listen(key, callback, listener, options) { + const controller = this._issueSignal(key, callback, options?.signal); + const controlledListener = options?.once + ? (event) => { + try { + listener(event); + } + finally { + controller.abort(); + } + } + : listener; + this._emitter.addEventListener(key, controlledListener, { ...options, signal: controller.signal }); + return controller; + } emitEvent(event, data) { this._emitter.dispatchEvent(new CustomEvent(`${event.toString()}`, { detail: data ?? undefined })); } on(event, callback, options) { const onEvent = (e) => void callback(e, e instanceof CustomEvent ? e?.detail : undefined); const key = event; - const controller = this._issueSignal(key, callback); - this._emitter.addEventListener(key, onEvent, { ...options, signal: controller.signal }); - return () => this.off(event, callback); + const controller = this._listen(key, callback, onEvent, options); + return () => controller.abort(); } /** - * Removes an event listener for a specific event. - * @param event - * @param callback + * Removes current event registrations in bulk. + * + * Prefer the disposer returned by `on`, `onEvent`, `once`, or `onceEvent` when removing one registration. + * + * @param event - The event whose registrations should be removed. + * @param callback - The callback whose registrations should be removed. Omit it to remove every registration for the event. */ off(event, callback) { const key = event; if (callback) { - const w = this._assigned.get(key)?.get(callback); - const controller = w?.deref(); - controller?.abort(); + const controllers = this._assigned.get(key)?.get(callback); + controllers?.forEach((controllerRef) => controllerRef.deref()?.abort()); } else { - this._allAssigned.get(key)?.forEach((w) => { - const controller = w.deref(); + this._allAssigned.get(key)?.forEach((controllerRef) => { + const controller = controllerRef.deref(); controller?.abort(); }); } @@ -99,15 +124,14 @@ class EventHub { onEvent(event, callback, options) { const onEvent = (e) => void callback(e instanceof CustomEvent ? e?.detail : undefined); const key = event; - const controller = this._issueSignal(key, callback); - this._emitter.addEventListener(key, onEvent, { ...options, signal: controller.signal }); - return () => this.off(event, callback); + const controller = this._listen(key, callback, onEvent, options); + return () => controller.abort(); } - once(event, callback) { - return this.on(event, callback, { once: true }); + once(event, callback, options) { + return this.on(event, callback, { ...options, once: true }); } - onceEvent(event, callback) { - return this.on(event, (_, data) => callback(data), { once: true }); + onceEvent(event, callback, options) { + return this.on(event, (_, data) => callback(data), { ...options, once: true }); } waitFor(event) { return new Promise((resolve) => { diff --git a/packages/octagonal-wheels/dist/events/EventHub.js.map b/packages/octagonal-wheels/dist/events/EventHub.js.map index 28f0b1e6..cb10d544 100644 --- a/packages/octagonal-wheels/dist/events/EventHub.js.map +++ b/packages/octagonal-wheels/dist/events/EventHub.js.map @@ -1 +1 @@ -{"version":3,"file":"EventHub.js","sources":["../../src/events/EventHub.ts"],"sourcesContent":[null],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"EventHub.js","sources":["../../src/events/EventHub.ts"],"sourcesContent":[null],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/octagonal-wheels/docs/events/EventHub/README.md b/packages/octagonal-wheels/docs/events/EventHub/README.md index b8c8b170..119c1e69 100644 --- a/packages/octagonal-wheels/docs/events/EventHub/README.md +++ b/packages/octagonal-wheels/docs/events/EventHub/README.md @@ -107,7 +107,9 @@ off(event: EventType, callback?: CallableFunction): void; Defined in: [src/events/EventHub.ts:139](https://github.com/vrtmrz/octagonal-wheels/blob/main/src/events/EventHub.ts#L139) -Removes an event listener for a specific event. +Removes current event registrations in bulk. + +Prefer the disposer returned by `on`, `onEvent`, `once`, or `onceEvent` when removing one registration. #### Type Parameters @@ -120,8 +122,8 @@ Removes an event listener for a specific event. | Parameter | Type | Description | | ------ | ------ | ------ | -| `event` | [`EventType`](../EventType/README.md)\<`K`\> | | -| `callback?` | `CallableFunction` | | +| `event` | [`EventType`](../EventType/README.md)\<`K`\> | The event whose registrations should be removed. | +| `callback?` | `CallableFunction` | The callback whose registrations should be removed. Omit it to remove every registration for the event. | #### Returns @@ -173,11 +175,11 @@ Registers an event listener for a specific event. | ------ | ------ | ------ | | `event` | [`EventTypeWithoutData`](../EventTypeWithoutData/README.md)\<`ET`, `K`\> | The event to listen for. | | `callback` | (`e`: `Event`) => `void` \| `Promise`\<`void`\> | The callback to execute when the event is triggered. | -| `options?` | `AddEventListenerOptions` | - | +| `options?` | `AddEventListenerOptions` | Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. | ##### Returns -A function to remove the event listener. +An idempotent function that removes only this registration. ```ts (): void; @@ -213,11 +215,11 @@ Registers an event listener for a specific event. | ------ | ------ | ------ | | `event` | [`EventTypeWithData`](../EventTypeWithData/README.md)\<`ET`, `K`\> | The event to listen for. | | `callback` | (`e`: `Event`, `data`: `ET`\[`K`\]) => `void` \| `Promise`\<`void`\> | The callback to execute when the event is triggered. | -| `options?` | `AddEventListenerOptions` | - | +| `options?` | `AddEventListenerOptions` | Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. | ##### Returns -A function to remove the event listener. +An idempotent function that removes only this registration. ```ts (): void; @@ -234,7 +236,10 @@ A function to remove the event listener. #### Call Signature ```ts -once(event: EventTypeWithoutData, callback: (e: Event) => void): () => void; +once( + event: EventTypeWithoutData, + callback: (e: Event) => void, + options?: AddEventListenerOptions): () => void; ``` Defined in: [src/events/EventHub.ts:210](https://github.com/vrtmrz/octagonal-wheels/blob/main/src/events/EventHub.ts#L210) @@ -254,9 +259,12 @@ Registers a one-time event listener for a specific event. | ------ | ------ | ------ | | `event` | [`EventTypeWithoutData`](../EventTypeWithoutData/README.md)\<`ET`, `K`\> | The event to listen for. | | `callback` | (`e`: `Event`) => `void` | The callback to execute when the event is triggered. | +| `options?` | `AddEventListenerOptions` | Native `EventTarget` listener options. The `once` option is always treated as `true`. | ##### Returns +An idempotent function that removes only this registration. + ```ts (): void; ``` @@ -268,7 +276,10 @@ Registers a one-time event listener for a specific event. #### Call Signature ```ts -once(event: EventTypeWithData, callback: (e: Event, data: ET[K]) => void): () => void; +once( + event: EventTypeWithData, + callback: (e: Event, data: ET[K]) => void, + options?: AddEventListenerOptions): () => void; ``` Defined in: [src/events/EventHub.ts:222](https://github.com/vrtmrz/octagonal-wheels/blob/main/src/events/EventHub.ts#L222) @@ -288,9 +299,12 @@ Registers a one-time event listener for a specific event. | ------ | ------ | ------ | | `event` | [`EventTypeWithData`](../EventTypeWithData/README.md)\<`ET`, `K`\> | The event to listen for. | | `callback` | (`e`: `Event`, `data`: `ET`\[`K`\]) => `void` | The callback to execute when the event is triggered. | +| `options?` | `AddEventListenerOptions` | Native `EventTarget` listener options. The `once` option is always treated as `true`. | ##### Returns +An idempotent function that removes only this registration. + ```ts (): void; ``` @@ -306,7 +320,10 @@ Registers a one-time event listener for a specific event. #### Call Signature ```ts -onceEvent(event: EventTypeWithoutData, callback: () => void): () => void; +onceEvent( + event: EventTypeWithoutData, + callback: () => void, + options?: AddEventListenerOptions): () => void; ``` Defined in: [src/events/EventHub.ts:241](https://github.com/vrtmrz/octagonal-wheels/blob/main/src/events/EventHub.ts#L241) @@ -326,9 +343,12 @@ Registers a one-time event listener for a specific event, with a callback that o | ------ | ------ | ------ | | `event` | [`EventTypeWithoutData`](../EventTypeWithoutData/README.md)\<`ET`, `K`\> | The event to listen for. | | `callback` | () => `void` | The callback to execute when the event is triggered. | +| `options?` | `AddEventListenerOptions` | Native `EventTarget` listener options. The `once` option is always treated as `true`. | ##### Returns +An idempotent function that removes only this registration. + ```ts (): void; ``` @@ -340,7 +360,10 @@ Registers a one-time event listener for a specific event, with a callback that o #### Call Signature ```ts -onceEvent(event: EventTypeWithData, callback: (data: ET[K]) => void): () => void; +onceEvent( + event: EventTypeWithData, + callback: (data: ET[K]) => void, + options?: AddEventListenerOptions): () => void; ``` Defined in: [src/events/EventHub.ts:253](https://github.com/vrtmrz/octagonal-wheels/blob/main/src/events/EventHub.ts#L253) @@ -360,9 +383,12 @@ Registers a one-time event listener for a specific event, with a callback that o | ------ | ------ | ------ | | `event` | [`EventTypeWithData`](../EventTypeWithData/README.md)\<`ET`, `K`\> | The event to listen for. | | `callback` | (`data`: `ET`\[`K`\]) => `void` | The callback to execute when the event is triggered. | +| `options?` | `AddEventListenerOptions` | Native `EventTarget` listener options. The `once` option is always treated as `true`. | ##### Returns +An idempotent function that removes only this registration. + ```ts (): void; ``` @@ -401,11 +427,11 @@ Registers an event listener for a specific event, with a callback that only rece | ------ | ------ | ------ | | `event` | [`EventTypeWithoutData`](../EventTypeWithoutData/README.md)\<`ET`, `K`\> | The event to listen for. | | `callback` | () => `any` | The callback to execute when the event is triggered. | -| `options?` | `AddEventListenerOptions` | - | +| `options?` | `AddEventListenerOptions` | Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. | ##### Returns -A function to remove the event listener. +An idempotent function that removes only this registration. ```ts (): void; @@ -441,11 +467,11 @@ Registers an event listener for a specific event, with a callback that only rece | ------ | ------ | ------ | | `event` | [`EventTypeWithData`](../EventTypeWithData/README.md)\<`ET`, `K`\> | The event to listen for. | | `callback` | (`data`: `ET`\[`K`\]) => `any` | The callback to execute when the event is triggered. | -| `options?` | `AddEventListenerOptions` | - | +| `options?` | `AddEventListenerOptions` | Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. | ##### Returns -A function to remove the event listener. +An idempotent function that removes only this registration. ```ts (): void; diff --git a/packages/octagonal-wheels/src/events/EventHub.test.ts b/packages/octagonal-wheels/src/events/EventHub.test.ts index e0f09963..f997390e 100644 --- a/packages/octagonal-wheels/src/events/EventHub.test.ts +++ b/packages/octagonal-wheels/src/events/EventHub.test.ts @@ -292,3 +292,153 @@ describe("multiple-hubs", () => { expect(callback2).toHaveBeenCalledWith("data2"); }); }); + +describe("EventHub-listener-options", () => { + it("should remove an on listener when the supplied signal is aborted", () => { + const hub = createEventHub(); + const controller = new AbortController(); + const callback = vi.fn(); + + hub.on("world_test", callback, { signal: controller.signal }); + controller.abort(); + hub.emitEvent("world_test"); + + expect(callback).not.toHaveBeenCalled(); + }); + + it("should not register an on listener with an already-aborted signal", () => { + const hub = createEventHub(); + const controller = new AbortController(); + const callback = vi.fn(); + controller.abort(); + + hub.on("world_test", callback, { signal: controller.signal }); + hub.emitEvent("world_test"); + + expect(callback).not.toHaveBeenCalled(); + }); + + it("should remove an onEvent listener when the supplied signal is aborted", () => { + const hub = createEventHub(); + const controller = new AbortController(); + const callback = vi.fn(); + + hub.onEvent("world_test", callback, { signal: controller.signal }); + controller.abort(); + hub.emitEvent("world_test"); + + expect(callback).not.toHaveBeenCalled(); + }); + + it("should remove only the registration owned by the supplied signal", () => { + const hub = createEventHub(); + const controller = new AbortController(); + const callback = vi.fn(); + + hub.onEvent("world_test", callback, { signal: controller.signal }); + hub.onEvent("world_test", callback); + + controller.abort(); + hub.emitEvent("world_test"); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it("should accept a signal for once", () => { + const hub = createEventHub(); + const controller = new AbortController(); + const callback = vi.fn(); + + hub.once("world_test", callback, { signal: controller.signal }); + controller.abort(); + hub.emitEvent("world_test"); + + expect(callback).not.toHaveBeenCalled(); + }); + + it("should accept a signal for onceEvent", () => { + const hub = createEventHub(); + const controller = new AbortController(); + const callback = vi.fn(); + + hub.onceEvent("world_test", callback, { signal: controller.signal }); + controller.abort(); + hub.emitEvent("world_test"); + + expect(callback).not.toHaveBeenCalled(); + }); + + it("should remain a one-time listener when once is false in the supplied options", () => { + const hub = createEventHub(); + const callback = vi.fn(); + + hub.once("world_test", callback, { once: false }); + hub.emitEvent("world_test"); + hub.emitEvent("world_test"); + + expect(callback).toHaveBeenCalledTimes(1); + }); +}); + +describe("EventHub-returned-disposer", () => { + it("should remove only the subscription that returned it", () => { + const hub = createEventHub(); + const callback = vi.fn(); + + const offFirst = hub.onEvent("world_test", callback); + const offSecond = hub.onEvent("world_test", callback); + + offFirst(); + hub.emitEvent("world_test"); + expect(callback).toHaveBeenCalledTimes(1); + + offFirst(); + hub.emitEvent("world_test"); + expect(callback).toHaveBeenCalledTimes(2); + + offSecond(); + hub.emitEvent("world_test"); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it("should not remove a replacement subscription when an old disposer is called again", () => { + const hub = createEventHub(); + const callback = vi.fn(); + + const oldOff = hub.onEvent("world_test", callback); + oldOff(); + hub.onEvent("world_test", callback); + + oldOff(); + hub.emitEvent("world_test"); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it("should not remove a later subscription through a disposer whose once listener has fired", () => { + const hub = createEventHub(); + const callback = vi.fn(); + + const oldOff = hub.once("world_test", callback); + hub.emitEvent("world_test"); + hub.on("world_test", callback); + + oldOff(); + hub.emitEvent("world_test"); + + expect(callback).toHaveBeenCalledTimes(2); + }); + + it("should remove every matching subscription through off", () => { + const hub = createEventHub(); + const callback = vi.fn(); + + hub.onEvent("world_test", callback); + hub.onEvent("world_test", callback); + + hub.off("world_test", callback); + hub.emitEvent("world_test"); + + expect(callback).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/octagonal-wheels/src/events/EventHub.ts b/packages/octagonal-wheels/src/events/EventHub.ts index 41a6eed4..5ffa7ded 100644 --- a/packages/octagonal-wheels/src/events/EventHub.ts +++ b/packages/octagonal-wheels/src/events/EventHub.ts @@ -39,34 +39,67 @@ export class EventHub { this._emitter = emitter ?? new EventTarget(); } - private _assigned = new Map>>(); + private _assigned = new Map>>>(); private _allAssigned = new Map>>(); - private _issueSignal(key: string, callback: CallableFunction) { - let assigned = this._assigned.get(key); - if (assigned === undefined) { - assigned = new WeakMap(); - } - const controllerRef = assigned.get(callback); - let controller: AbortController | undefined = controllerRef?.deref(); - if (!controller || controller.signal.aborted) { - controller = new AbortController(); - const refController = new FallbackWeakRef(controller); - controller.signal.addEventListener( - "abort", - () => { - this._assigned.get(key)?.delete(callback); - this._allAssigned.get(key)?.delete(refController); - }, - { once: true } - ); - assigned.set(callback, refController); - this._assigned.set(key, assigned); - const allAssigned = this._allAssigned.get(key) ?? new Set(); - allAssigned.add(refController); - this._allAssigned.set(key, allAssigned); - return controller; + private _issueSignal(key: string, callback: CallableFunction, sourceSignal?: AbortSignal) { + const controller = new AbortController(); + const controllerRef = new FallbackWeakRef(controller); + const assigned = this._assigned.get(key) ?? new WeakMap(); + const callbackControllers = assigned.get(callback) ?? new Set(); + const allAssigned = this._allAssigned.get(key) ?? new Set(); + const abortFromSource = () => controller.abort(); + + controller.signal.addEventListener( + "abort", + () => { + sourceSignal?.removeEventListener("abort", abortFromSource); + callbackControllers.delete(controllerRef); + if (callbackControllers.size === 0) { + assigned.delete(callback); + } + allAssigned.delete(controllerRef); + if (allAssigned.size === 0) { + this._assigned.delete(key); + this._allAssigned.delete(key); + } + }, + { once: true } + ); + + callbackControllers.add(controllerRef); + assigned.set(callback, callbackControllers); + this._assigned.set(key, assigned); + allAssigned.add(controllerRef); + this._allAssigned.set(key, allAssigned); + + if (sourceSignal) { + sourceSignal.addEventListener("abort", abortFromSource, { once: true }); + if (sourceSignal.aborted) { + controller.abort(); + } } + + return controller; + } + + private _listen( + key: string, + callback: CallableFunction, + listener: EventListener, + options?: AddEventListenerOptions + ) { + const controller = this._issueSignal(key, callback, options?.signal); + const controlledListener: EventListener = options?.once + ? (event) => { + try { + listener(event); + } finally { + controller.abort(); + } + } + : listener; + this._emitter.addEventListener(key, controlledListener, { ...options, signal: controller.signal }); return controller; } @@ -98,7 +131,8 @@ export class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. - * @returns A function to remove the event listener. + * @param options - Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. + * @returns An idempotent function that removes only this registration. */ on( event: EventTypeWithoutData, @@ -112,7 +146,8 @@ export class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. - * @returns A function to remove the event listener. + * @param options - Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. + * @returns An idempotent function that removes only this registration. */ on( event: EventTypeWithData, @@ -126,25 +161,26 @@ export class EventHub { ): () => void { const onEvent = (e: Event) => void callback(e, e instanceof CustomEvent ? (e?.detail as ET[K]) : undefined); const key = event; - const controller = this._issueSignal(key, callback); - this._emitter.addEventListener(key, onEvent, { ...options, signal: controller.signal }); - return () => this.off(event, callback); + const controller = this._listen(key, callback, onEvent, options); + return () => controller.abort(); } /** - * Removes an event listener for a specific event. - * @param event - * @param callback + * Removes current event registrations in bulk. + * + * Prefer the disposer returned by `on`, `onEvent`, `once`, or `onceEvent` when removing one registration. + * + * @param event - The event whose registrations should be removed. + * @param callback - The callback whose registrations should be removed. Omit it to remove every registration for the event. */ off(event: EventType, callback?: CallableFunction): void { const key = event; if (callback) { - const w = this._assigned.get(key)?.get(callback); - const controller = w?.deref(); - controller?.abort(); + const controllers = this._assigned.get(key)?.get(callback); + controllers?.forEach((controllerRef) => controllerRef.deref()?.abort()); } else { - this._allAssigned.get(key)?.forEach((w) => { - const controller = w.deref(); + this._allAssigned.get(key)?.forEach((controllerRef) => { + const controller = controllerRef.deref(); controller?.abort(); }); } @@ -166,7 +202,8 @@ export class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. - * @returns A function to remove the event listener. + * @param options - Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. + * @returns An idempotent function that removes only this registration. */ onEvent( event: EventTypeWithoutData, @@ -180,7 +217,8 @@ export class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. - * @returns A function to remove the event listener. + * @param options - Native `EventTarget` listener options. A supplied signal removes only this registration when aborted. + * @returns An idempotent function that removes only this registration. */ onEvent( event: EventTypeWithData, @@ -194,9 +232,8 @@ export class EventHub { ): () => void { const onEvent = (e: Event) => void callback(e instanceof CustomEvent ? (e?.detail as ET[K]) : undefined); const key = event; - const controller = this._issueSignal(key, callback); - this._emitter.addEventListener(key, onEvent, { ...options, signal: controller.signal }); - return () => this.off(event, callback); + const controller = this._listen(key, callback, onEvent, options); + return () => controller.abort(); } /** @@ -206,10 +243,13 @@ export class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. + * @param options - Native `EventTarget` listener options. The `once` option is always treated as `true`. + * @returns An idempotent function that removes only this registration. */ once( event: EventTypeWithoutData, - callback: (e: Event) => void + callback: (e: Event) => void, + options?: AddEventListenerOptions ): () => void; /** * Registers a one-time event listener for a specific event. @@ -218,16 +258,20 @@ export class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. + * @param options - Native `EventTarget` listener options. The `once` option is always treated as `true`. + * @returns An idempotent function that removes only this registration. */ once( event: EventTypeWithData, - callback: (e: Event, data: ET[K]) => void + callback: (e: Event, data: ET[K]) => void, + options?: AddEventListenerOptions ): () => void; once( event: EventType, - callback: (e: Event, data?: ET[K]) => void + callback: (e: Event, data?: ET[K]) => void, + options?: AddEventListenerOptions ): () => void { - return this.on(event as any, callback, { once: true }); + return this.on(event as any, callback, { ...options, once: true }); } /** @@ -237,10 +281,13 @@ export class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. + * @param options - Native `EventTarget` listener options. The `once` option is always treated as `true`. + * @returns An idempotent function that removes only this registration. */ onceEvent( event: EventTypeWithoutData, - callback: () => void + callback: () => void, + options?: AddEventListenerOptions ): () => void; /** * Registers a one-time event listener for a specific event, with a callback that only receives the event data. @@ -249,16 +296,20 @@ export class EventHub { * @template K - The key of the event. * @param event - The event to listen for. * @param callback - The callback to execute when the event is triggered. + * @param options - Native `EventTarget` listener options. The `once` option is always treated as `true`. + * @returns An idempotent function that removes only this registration. */ onceEvent( event: EventTypeWithData, - callback: (data: ET[K]) => void + callback: (data: ET[K]) => void, + options?: AddEventListenerOptions ): () => void; onceEvent( event: EventType, - callback: (data?: ET[K]) => void + callback: (data?: ET[K]) => void, + options?: AddEventListenerOptions ): () => void { - return this.on(event as any, (_: any, data: any) => callback(data), { once: true }); + return this.on(event as any, (_: any, data: any) => callback(data), { ...options, once: true }); } /** diff --git a/packages/octagonal-wheels/updates.md b/packages/octagonal-wheels/updates.md index 0924e694..c4a62c78 100644 --- a/packages/octagonal-wheels/updates.md +++ b/packages/octagonal-wheels/updates.md @@ -2,6 +2,14 @@ ## Unreleased +### Fixed + +- `EventHub.on` and `EventHub.onEvent` now honour caller-supplied abort signals. Their returned disposers are registration-scoped and idempotent, so an old disposer cannot remove a later subscription. + +### Improved + +- `EventHub.once` and `EventHub.onceEvent` now accept native `AddEventListenerOptions`, including abort signals, while retaining one-time delivery. + ## 0.1.53 ### Fixed