From 5e4684098bee024f014d8ba00c3954a0f1e14a32 Mon Sep 17 00:00:00 2001 From: Federico Date: Fri, 15 Nov 2024 17:29:51 +0700 Subject: [PATCH 1/4] First pass --- source/add-listener.ts | 26 ++++++++++++++++++++++++++ source/index.ts | 1 + source/one-event.ts | 34 ++++++++++++++++++---------------- 3 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 source/add-listener.ts diff --git a/source/add-listener.ts b/source/add-listener.ts new file mode 100644 index 0000000..dee3b8d --- /dev/null +++ b/source/add-listener.ts @@ -0,0 +1,26 @@ +type AnyFunction = (...parameters: any[]) => void; + +type RemovableEvent unknown> = { + removeListener(callback: T): void; + addListener(callback: T): void; +}; + +export function addListener>( + event: Event, + listener: (...parameters: Parameters[0]>) => void, + { + signal, + }: { + signal: AbortSignal; + }, +): void { + if (signal?.aborted) { + return; + } + + event.addListener(listener); + + signal.addEventListener('abort', () => { + event.removeListener(listener); + }, {once: true}); +} diff --git a/source/index.ts b/source/index.ts index 54b682b..9ea7896 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,3 +1,4 @@ export * from './on-context-invalidated.js'; export * from './on-extension-start.js'; export * from './one-event.js'; +export * from './add-listener.js'; diff --git a/source/one-event.ts b/source/one-event.ts index cd2c72f..b7a4c39 100644 --- a/source/one-event.ts +++ b/source/one-event.ts @@ -1,3 +1,5 @@ +import {addListener} from './add-listener.js'; + type AnyFunction = (...parameters: any[]) => void; type RemovableEvent unknown> = { @@ -23,21 +25,21 @@ export async function oneEvent>( return; } - await new Promise(resolve => { - // TODO: VoidFunction should not be necessary, it's equivalent to using "any" - const listener: VoidFunction = (...parameters: EventParameters) => { - if (!filter || filter(...parameters)) { - resolve(); - event.removeListener(listener); - } - }; - - event.addListener(listener); - - // TODO: The abort listener is left behind if never aborted - signal?.addEventListener('abort', () => { - resolve(); - event.removeListener(listener); - }); + const controller = new AbortController(); + const complete = controller.abort.bind(controller); + signal?.addEventListener('abort', complete, {once: true}); + + const listener = filter ? (...parameters: EventParameters) => { + if (filter(...parameters)) { + complete(); + } + } : complete; + + addListener(event, listener, { + signal: controller.signal, + }); + + await new Promise(resolve => { + controller.signal.addEventListener('abort', resolve, {once: true}); }); } From 1334ccd2737e6010fe8718685e5e1c98a66254bd Mon Sep 17 00:00:00 2001 From: fregante Date: Fri, 15 Nov 2024 17:31:57 +0700 Subject: [PATCH 2/4] Discard changes to source/one-event.ts --- source/one-event.ts | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/source/one-event.ts b/source/one-event.ts index b7a4c39..cd2c72f 100644 --- a/source/one-event.ts +++ b/source/one-event.ts @@ -1,5 +1,3 @@ -import {addListener} from './add-listener.js'; - type AnyFunction = (...parameters: any[]) => void; type RemovableEvent unknown> = { @@ -25,21 +23,21 @@ export async function oneEvent>( return; } - const controller = new AbortController(); - const complete = controller.abort.bind(controller); - signal?.addEventListener('abort', complete, {once: true}); - - const listener = filter ? (...parameters: EventParameters) => { - if (filter(...parameters)) { - complete(); - } - } : complete; - - addListener(event, listener, { - signal: controller.signal, - }); - - await new Promise(resolve => { - controller.signal.addEventListener('abort', resolve, {once: true}); + await new Promise(resolve => { + // TODO: VoidFunction should not be necessary, it's equivalent to using "any" + const listener: VoidFunction = (...parameters: EventParameters) => { + if (!filter || filter(...parameters)) { + resolve(); + event.removeListener(listener); + } + }; + + event.addListener(listener); + + // TODO: The abort listener is left behind if never aborted + signal?.addEventListener('abort', () => { + resolve(); + event.removeListener(listener); + }); }); } From 8d36744ed5ff73c53877823627f736b7c69b0cc2 Mon Sep 17 00:00:00 2001 From: Federico Date: Sun, 17 Nov 2024 16:38:08 +0700 Subject: [PATCH 3/4] Tests and docs --- source/add-listener.md | 34 ++++++++++++++++++++++++++++++++ source/add-listener.test.ts | 23 +++++++++++++++++++++ source/on-context-invalidated.md | 2 ++ source/on-extension-start.md | 2 ++ source/one-event.md | 2 ++ 5 files changed, 63 insertions(+) create mode 100644 source/add-listener.md create mode 100644 source/add-listener.test.ts diff --git a/source/add-listener.md b/source/add-listener.md new file mode 100644 index 0000000..735ce14 --- /dev/null +++ b/source/add-listener.md @@ -0,0 +1,34 @@ +# add-listener + +Utility method to add listeners to events and remove them when the signal is aborted. + +Currently this **requires** a `signal`. Without a `signal`, you should just use the native `addListener` method as this has no advantages over it. + +```js +import {addListener} from 'webext-events'; + +addListener(chrome.tabs.onCreated, (tab) => { + console.log('Hurray, a new tab was created') +}, {signal: AbortSignal.timeout(1000)}); +``` + +> [!NOTE] +> Background workers are unloaded and the status of `signal`s created within them is reset. Dealing with this is outside the responsibility of this library. + +## Compatibility + +- Any browser + +## Permissions + +- No special permissions + +## Context + +- Any context + +## Related + +- [abort-utils](https://github.com/fregante/abort-utils) - Utility functions to use and combine `AbortSignal` and `AbortController` with Promises. + +## [Main page ⏎](../readme.md) diff --git a/source/add-listener.test.ts b/source/add-listener.test.ts new file mode 100644 index 0000000..c3195b2 --- /dev/null +++ b/source/add-listener.test.ts @@ -0,0 +1,23 @@ +import { + describe, it, vi, expect, +} from 'vitest'; +import {addListener} from './add-listener.js'; + +describe('addListener', () => { + it('should remove the listener when the signal is aborted', () => { + const event = { + addListener: vi.fn(), + removeListener: vi.fn(), + }; + const listener = vi.fn(); + const controller = new AbortController(); + addListener(event, listener, {signal: controller.signal}); + + expect(event.addListener).toHaveBeenCalledWith(listener); + + controller.abort(); + + expect(event.removeListener).toHaveBeenCalledWith(listener); + }); +}); + diff --git a/source/on-context-invalidated.md b/source/on-context-invalidated.md index 87be57b..5e1c91f 100644 --- a/source/on-context-invalidated.md +++ b/source/on-context-invalidated.md @@ -34,3 +34,5 @@ fetch('/api', {signal: onContextInvalidated.signal}) - any context Some contexts like the background page/worker and standalone tabs will be closed by the browser automatically so the event doesn't apply there. + +## [Main page ⏎](../readme.md) diff --git a/source/on-extension-start.md b/source/on-extension-start.md index f683122..809c802 100644 --- a/source/on-extension-start.md +++ b/source/on-extension-start.md @@ -35,3 +35,5 @@ onExtensionStart.addListener(listener); - background worker - background page - event page (not in Chrome) + +## [Main page ⏎](../readme.md) diff --git a/source/one-event.md b/source/one-event.md index b1acf40..c94d518 100644 --- a/source/one-event.md +++ b/source/one-event.md @@ -57,3 +57,5 @@ if (timeout.aborted) { ## Related - [one-event](https://github.com/fregante/one-event) - The same thing, but for regular browser events. + +## [Main page ⏎](../readme.md) From cdb881a462903b4616e48a035eee99c8b33ff160 Mon Sep 17 00:00:00 2001 From: Federico Date: Sun, 17 Nov 2024 16:42:12 +0700 Subject: [PATCH 4/4] Test types --- source/add-listener.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/source/add-listener.test.ts b/source/add-listener.test.ts index c3195b2..e50d3e6 100644 --- a/source/add-listener.test.ts +++ b/source/add-listener.test.ts @@ -1,5 +1,5 @@ import { - describe, it, vi, expect, + describe, it, vi, expect, expectTypeOf, } from 'vitest'; import {addListener} from './add-listener.js'; @@ -19,5 +19,12 @@ describe('addListener', () => { expect(event.removeListener).toHaveBeenCalledWith(listener); }); + + it('should have the correct types', () => { + addListener(chrome.tabs.onMoved, (tabId, tab) => { + expectTypeOf(tabId).toEqualTypeOf(); + expectTypeOf(tab).toEqualTypeOf(); + }, {signal: AbortSignal.timeout(1000)}); + }); });