From e3fc65165cd8bb17963ed1a6121e4a169fe91fe7 Mon Sep 17 00:00:00 2001 From: hanquliu Date: Mon, 20 May 2024 20:53:38 +0800 Subject: [PATCH 1/4] Support AbortController --- index.d.ts | 40 +++++++++++++++++++++++++++++----------- index.js | 16 +++++----------- index.test-d.ts | 12 +++++++++--- readme.md | 9 +++++---- test.js | 31 +++++++++++++++++++------------ 5 files changed, 67 insertions(+), 41 deletions(-) diff --git a/index.d.ts b/index.d.ts index 723505f..33db557 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,9 +1,3 @@ -export class AbortError extends Error { - readonly name: 'AbortError'; - - private constructor(); -} - type AnyFunction = (...arguments_: readonly any[]) => unknown; export type ThrottledFunction = F & { @@ -18,11 +12,6 @@ export type ThrottledFunction = F & { The number of queued items waiting to be executed. */ readonly queueSize: number; - - /** - Abort pending executions. All unresolved promises are rejected with a `pThrottle.AbortError` error. - */ - abort(): void; }; export type Options = { @@ -43,6 +32,35 @@ export type Options = { */ readonly strict?: boolean; + /** + Abort signal + + @example + ``` + import pThrottle from 'p-throttle'; + + const controller = new AbortController(); + const throttle = pThrottle({ + limit: 2, + interval: 1000, + signal: controller.signal + }); + + const throttled = throttle(() => { + console.log('Executing...'); + }); + + await throttled(); + await throttled(); + controller.abort('aborted') + await throttled(); + //=> Executing... + //=> Executing... + //=> Promise rejected with reason `aborted` + ``` + */ + signal?: AbortSignal; + /** Get notified when function calls are delayed due to exceeding the `limit` of allowed calls within the given `interval`. diff --git a/index.js b/index.js index 40ab78f..9ae3f5b 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,4 @@ -export class AbortError extends Error { - constructor() { - super('Throttled function aborted'); - this.name = 'AbortError'; - } -} - -export default function pThrottle({limit, interval, strict, onDelay}) { +export default function pThrottle({limit, interval, strict, signal, onDelay}) { if (!Number.isFinite(limit)) { throw new TypeError('Expected `limit` to be a finite number'); } @@ -91,15 +84,16 @@ export default function pThrottle({limit, interval, strict, onDelay}) { }); }; - throttled.abort = () => { + signal?.throwIfAborted(); + signal?.addEventListener('abort', () => { for (const timeout of queue.keys()) { clearTimeout(timeout); - queue.get(timeout)(new AbortError()); + queue.get(timeout)(signal.reason); } queue.clear(); strictTicks.splice(0, strictTicks.length); - }; + }); throttled.isEnabled = true; diff --git a/index.test-d.ts b/index.test-d.ts index 91027fc..4d5bea6 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,28 +1,34 @@ import {expectType} from 'tsd'; import pThrottle, {type ThrottledFunction} from './index.js'; +const unicornController = new AbortController(); const throttledUnicorn = pThrottle({ limit: 1, interval: 1000, + signal: unicornController.signal, })((_index: string) => '🦄'); +const lazyUnicornController = new AbortController(); const throttledLazyUnicorn = pThrottle({ limit: 1, interval: 1000, + signal: lazyUnicornController.signal, })(async (_index: string) => '🦄'); +const taggedUnicornController = new AbortController(); const throttledTaggedUnicorn = pThrottle({ limit: 1, interval: 1000, + signal: taggedUnicornController.signal, })((_index: number, tag: string) => `${tag}: 🦄`); expectType(throttledUnicorn('')); expectType(await throttledLazyUnicorn('')); expectType(throttledTaggedUnicorn(1, 'foo')); -throttledUnicorn.abort(); -throttledLazyUnicorn.abort(); -throttledTaggedUnicorn.abort(); +unicornController.abort(); +lazyUnicornController.abort(); +taggedUnicornController.abort(); expectType(throttledUnicorn.isEnabled); expectType(throttledUnicorn.queueSize); diff --git a/readme.md b/readme.md index 6eb9a5c..a9ec6de 100644 --- a/readme.md +++ b/readme.md @@ -75,6 +75,11 @@ Default: `false` Use a strict, more resource intensive, throttling algorithm. The default algorithm uses a windowed approach that will work correctly in most cases, limiting the total number of calls at the specified limit per interval window. The strict algorithm throttles each call individually, ensuring the limit is not exceeded for any interval. +#### signal +Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) + +Allow to abort pending executions by calling `AbortController.abort()`. All unresolved promises are rejected with `signal.reason`. + ##### onDelay Type: `Function` @@ -119,10 +124,6 @@ Type: `Function` A promise-returning/async function or a normal function. -### throttledFn.abort() - -Abort pending executions. All unresolved promises are rejected with a `pThrottle.AbortError` error. - ### throttledFn.isEnabled Type: `boolean`\ diff --git a/test.js b/test.js index 6648ddb..37f1271 100644 --- a/test.js +++ b/test.js @@ -2,7 +2,7 @@ import test from 'ava'; import inRange from 'in-range'; import timeSpan from 'time-span'; import delay from 'delay'; -import pThrottle, {AbortError} from './index.js'; +import pThrottle from './index.js'; const fixture = Symbol('fixture'); @@ -131,23 +131,29 @@ test('passes arguments through', async t => { t.is(await throttled(fixture), fixture); }); +test('throw if aborted', t => { + const error = t.throws(() => { + const controller = new AbortController(); + controller.abort(new Error('aborted')); + pThrottle({limit: 1, interval: 100, signal: controller.signal})(async x => x); + }); + + t.is(error.message, 'aborted'); +}); + test('can be aborted', async t => { const limit = 1; const interval = 10_000; // 10 seconds const end = timeSpan(); - const throttled = pThrottle({limit, interval})(async () => {}); + const controller = new AbortController(); + const throttled = pThrottle({limit, interval, signal: controller.signal})(async () => {}); await throttled(); const promise = throttled(); - throttled.abort(); - let error; - try { - await promise; - } catch (error_) { - error = error_; - } + controller.abort(new Error('aborted')); - t.true(error instanceof AbortError); + const error = await t.throwsAsync(promise); + t.is(error.message, 'aborted'); t.true(end() < 100); }); @@ -289,14 +295,15 @@ test('handles simultaneous calls', async t => { test('clears queue after abort', async t => { const limit = 2; const interval = 100; - const throttled = pThrottle({limit, interval})(() => Date.now()); + const controller = new AbortController(); + const throttled = pThrottle({limit, interval, signal: controller.signal})(() => Date.now()); try { await throttled(); await throttled(); } catch {} - throttled.abort(); + controller.abort(); t.is(throttled.queueSize, 0); }); From 650feb2bba74ac4c8eb5c56f37afbf6e12cb1a04 Mon Sep 17 00:00:00 2001 From: hanquliu Date: Mon, 27 May 2024 19:41:11 +0800 Subject: [PATCH 2/4] Add listener when signal is not aborted --- index.d.ts | 11 ++++++----- index.js | 20 +++++++++++--------- readme.md | 3 ++- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/index.d.ts b/index.d.ts index 33db557..d2db745 100644 --- a/index.d.ts +++ b/index.d.ts @@ -33,13 +33,14 @@ export type Options = { readonly strict?: boolean; /** - Abort signal + Abort pending executions. When aborted, all unresolved promises are rejected with `signal.reason`. @example ``` import pThrottle from 'p-throttle'; const controller = new AbortController(); + const throttle = pThrottle({ limit: 2, interval: 1000, @@ -47,8 +48,8 @@ export type Options = { }); const throttled = throttle(() => { - console.log('Executing...'); - }); + console.log('Executing...'); + }); await throttled(); await throttled(); @@ -79,8 +80,8 @@ export type Options = { }); const throttled = throttle(() => { - console.log('Executing...'); - }); + console.log('Executing...'); + }); await throttled(); await throttled(); diff --git a/index.js b/index.js index 9ae3f5b..6572b79 100644 --- a/index.js +++ b/index.js @@ -85,15 +85,17 @@ export default function pThrottle({limit, interval, strict, signal, onDelay}) { }; signal?.throwIfAborted(); - signal?.addEventListener('abort', () => { - for (const timeout of queue.keys()) { - clearTimeout(timeout); - queue.get(timeout)(signal.reason); - } - - queue.clear(); - strictTicks.splice(0, strictTicks.length); - }); + if (signal && !signal.aborted) { + signal.addEventListener('abort', () => { + for (const timeout of queue.keys()) { + clearTimeout(timeout); + queue.get(timeout)(signal.reason); + } + + queue.clear(); + strictTicks.splice(0, strictTicks.length); + }, {once: true}); + } throttled.isEnabled = true; diff --git a/readme.md b/readme.md index a9ec6de..ec9e693 100644 --- a/readme.md +++ b/readme.md @@ -76,9 +76,10 @@ Default: `false` Use a strict, more resource intensive, throttling algorithm. The default algorithm uses a windowed approach that will work correctly in most cases, limiting the total number of calls at the specified limit per interval window. The strict algorithm throttles each call individually, ensuring the limit is not exceeded for any interval. #### signal + Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) -Allow to abort pending executions by calling `AbortController.abort()`. All unresolved promises are rejected with `signal.reason`. +Abort pending executions. When aborted, all unresolved promises are rejected with `signal.reason`. ##### onDelay From 082d65a05e7ab1a469de773c109e4d620ed4e4d5 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Mon, 27 May 2024 23:36:16 +0200 Subject: [PATCH 3/4] Update index.js --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 6572b79..da5003d 100644 --- a/index.js +++ b/index.js @@ -91,7 +91,7 @@ export default function pThrottle({limit, interval, strict, signal, onDelay}) { clearTimeout(timeout); queue.get(timeout)(signal.reason); } - + queue.clear(); strictTicks.splice(0, strictTicks.length); }, {once: true}); From 46553e45f8d4418c3cba080772bbca7933bc822a Mon Sep 17 00:00:00 2001 From: hanquliu Date: Tue, 28 May 2024 19:50:21 +0800 Subject: [PATCH 4/4] Remove redundant check --- index.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index da5003d..9b31a42 100644 --- a/index.js +++ b/index.js @@ -85,17 +85,15 @@ export default function pThrottle({limit, interval, strict, signal, onDelay}) { }; signal?.throwIfAborted(); - if (signal && !signal.aborted) { - signal.addEventListener('abort', () => { - for (const timeout of queue.keys()) { - clearTimeout(timeout); - queue.get(timeout)(signal.reason); - } + signal?.addEventListener('abort', () => { + for (const timeout of queue.keys()) { + clearTimeout(timeout); + queue.get(timeout)(signal.reason); + } - queue.clear(); - strictTicks.splice(0, strictTicks.length); - }, {once: true}); - } + queue.clear(); + strictTicks.splice(0, strictTicks.length); + }, {once: true}); throttled.isEnabled = true;