Skip to content

Commit 8907801

Browse files
committed
Add rejectOnClear option
Fixes #25
1 parent ce9d71c commit 8907801

5 files changed

Lines changed: 79 additions & 10 deletions

File tree

index.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export type LimitFunction = {
2020
This might be useful if you want to teardown the queue at the end of your program's lifecycle or discard any function calls referencing an intermediary state of your app.
2121
2222
Note: This does not cancel promises that are already running.
23+
24+
When `rejectOnClear` is enabled, pending promises are rejected with an `AbortError`.
25+
This is recommended if you await the returned promises, for example with `Promise.all`, so pending tasks do not remain unresolved after `clearQueue()`.
2326
*/
2427
clearQueue: () => void;
2528

@@ -92,6 +95,15 @@ export type Options = {
9295
Minimum: `1`.
9396
*/
9497
readonly concurrency: number;
98+
99+
/**
100+
Reject pending promises with an `AbortError` when `clearQueue()` is called.
101+
102+
Default: `false`.
103+
104+
This is recommended if you await the returned promises, for example with `Promise.all`, so pending tasks do not remain unresolved after `clearQueue()`.
105+
*/
106+
readonly rejectOnClear?: boolean;
95107
};
96108

97109
/**

index.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
import Queue from 'yocto-queue';
22

33
export default function pLimit(concurrency) {
4+
let rejectOnClear = false;
5+
46
if (typeof concurrency === 'object') {
5-
concurrency = concurrency.concurrency;
7+
({concurrency, rejectOnClear = false} = concurrency);
68
}
79

810
validateConcurrency(concurrency);
911

12+
if (typeof rejectOnClear !== 'boolean') {
13+
throw new TypeError('Expected `rejectOnClear` to be a boolean');
14+
}
15+
1016
const queue = new Queue();
1117
let activeCount = 0;
1218

1319
const resumeNext = () => {
1420
// Process the next queued function if we're under the concurrency limit
1521
if (activeCount < concurrency && queue.size > 0) {
1622
activeCount++;
17-
queue.dequeue()();
23+
queue.dequeue().run();
1824
}
1925
};
2026

@@ -41,11 +47,14 @@ export default function pLimit(concurrency) {
4147
next();
4248
};
4349

44-
const enqueue = (function_, resolve, arguments_) => {
50+
const enqueue = (function_, resolve, reject, arguments_) => {
51+
const queueItem = {reject};
52+
4553
// Queue the internal resolve function instead of the run function
4654
// to preserve the asynchronous execution context.
4755
new Promise(internalResolve => { // eslint-disable-line promise/param-names
48-
queue.enqueue(internalResolve);
56+
queueItem.run = internalResolve;
57+
queue.enqueue(queueItem);
4958
}).then(run.bind(undefined, function_, resolve, arguments_)); // eslint-disable-line promise/prefer-await-to-then
5059

5160
// Start processing immediately if we haven't reached the concurrency limit
@@ -54,8 +63,8 @@ export default function pLimit(concurrency) {
5463
}
5564
};
5665

57-
const generator = (function_, ...arguments_) => new Promise(resolve => {
58-
enqueue(function_, resolve, arguments_);
66+
const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
67+
enqueue(function_, resolve, reject, arguments_);
5968
});
6069

6170
Object.defineProperties(generator, {
@@ -67,7 +76,16 @@ export default function pLimit(concurrency) {
6776
},
6877
clearQueue: {
6978
value() {
70-
queue.clear();
79+
if (!rejectOnClear) {
80+
queue.clear();
81+
return;
82+
}
83+
84+
const abortError = AbortSignal.abort().reason;
85+
86+
while (queue.size > 0) {
87+
queue.dequeue().reject(abortError);
88+
}
7189
},
7290
},
7391
concurrency: {
@@ -97,8 +115,7 @@ export default function pLimit(concurrency) {
97115
}
98116

99117
export function limitFunction(function_, options) {
100-
const {concurrency} = options;
101-
const limit = pLimit(concurrency);
118+
const limit = pLimit(options);
102119

103120
return (...arguments_) => limit(() => function_(...arguments_));
104121
}

index.test-d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {expectType, expectError} from 'tsd';
22
import pLimit, {limitFunction} from './index.js';
33

44
const limit = pLimit(1);
5+
const limitWithRejectOnClear = pLimit({concurrency: 1, rejectOnClear: true});
56

67
const input = [
78
limit(async () => 'foo'),
@@ -18,12 +19,14 @@ expectType<number>(limit.activeCount);
1819
expectType<number>(limit.pendingCount);
1920

2021
expectType<void>(limit.clearQueue());
22+
expectType<void>(limitWithRejectOnClear.clearQueue());
2123

2224
// LimitFunction should require a Promise-returning function
2325
const lf = limitFunction(async (_a: string) => 'ok', {concurrency: 1});
2426
expectType<Promise<string>>(lf('input'));
2527

2628
expectError(limitFunction((_a: string) => 'x', {concurrency: 1}));
29+
expectError(pLimit({concurrency: 1, rejectOnClear: 'nope'}));
2730

2831
// LimitFunction.map accepts iterables
2932
expectType<Promise<string[]>>(limit.map(new Set(['a', 'b', 'c']), async x => x + x));

readme.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,15 @@ Minimum: `1`
4141

4242
Concurrency limit.
4343

44-
You can pass a number or an options object with a `concurrency` property:
44+
You can pass a number or an options object with a `concurrency` property.
45+
46+
#### rejectOnClear
47+
48+
Type: `boolean`\
49+
Default: `false`
50+
51+
Reject pending promises with an `AbortError` when `clearQueue()` is called.
52+
This is recommended if you await the returned promises, for example with `Promise.all`, so pending tasks do not remain unresolved after `clearQueue()`.
4553

4654
```js
4755
import pLimit from 'p-limit';
@@ -93,6 +101,9 @@ This might be useful if you want to teardown the queue at the end of your progra
93101

94102
Note: This does not cancel promises that are already running.
95103

104+
When `rejectOnClear` is enabled, pending promises are rejected with an `AbortError`.
105+
This is recommended if you await the returned promises, for example with `Promise.all`, so pending tasks do not remain unresolved after `clearQueue()`.
106+
96107
### limit.concurrency
97108

98109
Get or set the concurrency limit.
@@ -135,6 +146,14 @@ Minimum: `1`
135146

136147
Concurrency limit.
137148

149+
#### rejectOnClear
150+
151+
Type: `boolean`\
152+
Default: `false`
153+
154+
Reject pending promises with an `AbortError` when `clearQueue()` is called.
155+
This is recommended if you await the returned promises, for example with `Promise.all`, so pending tasks do not remain unresolved after `clearQueue()`.
156+
138157
## Recipes
139158

140159
See [recipes.md](recipes.md) for common use cases and patterns.

test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,24 @@ test('clearQueue', async t => {
192192
t.is(limit.pendingCount, 0);
193193
});
194194

195+
test('clearQueue rejects pending promises when enabled', async t => {
196+
const limit = pLimit({concurrency: 1, rejectOnClear: true});
197+
198+
const runningPromise = limit(() => delay(100));
199+
const pendingPromiseOne = limit(() => delay(10));
200+
const pendingPromiseTwo = limit(() => delay(10));
201+
202+
await Promise.resolve();
203+
t.is(limit.pendingCount, 2);
204+
205+
limit.clearQueue();
206+
t.is(limit.pendingCount, 0);
207+
208+
await runningPromise;
209+
await t.throwsAsync(pendingPromiseOne, {name: 'AbortError'});
210+
await t.throwsAsync(pendingPromiseTwo, {name: 'AbortError'});
211+
});
212+
195213
test('map', async t => {
196214
const limit = pLimit(1);
197215
const results = await limit.map([1, 2, 3, 4, 5, 6, 7], input => input + 1);

0 commit comments

Comments
 (0)