Skip to content

Commit 8184655

Browse files
committed
Fix .add() and .addAll()
Fixes #158, fixes #168 Signed-off-by: Richie Bendall <richiebendall@gmail.com>
1 parent c905aaf commit 8184655

File tree

4 files changed

+93
-55
lines changed

4 files changed

+93
-55
lines changed

package.json

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
"node": ">=12"
1212
},
1313
"scripts": {
14-
"build": "del dist && tsc",
15-
"test": "xo && npm run build && nyc ava",
14+
"build": "tsc --build --clean",
15+
"test": "xo && ava",
1616
"bench": "node --loader=ts-node/esm bench.ts",
17-
"prepare": "npm run build"
17+
"prepare": "tsc --build --clean"
1818
},
1919
"files": [
2020
"dist"
@@ -50,18 +50,15 @@
5050
"@sindresorhus/tsconfig": "^2.0.0",
5151
"@types/benchmark": "^2.1.1",
5252
"@types/node": "^17.0.13",
53-
"ava": "^4.0.1",
53+
"ava": "^5.1.1",
5454
"benchmark": "^2.1.4",
55-
"codecov": "^3.8.3",
56-
"del-cli": "^4.0.1",
5755
"delay": "^5.0.0",
5856
"in-range": "^3.0.0",
59-
"nyc": "^15.1.0",
6057
"p-defer": "^4.0.0",
6158
"random-int": "^3.0.0",
6259
"time-span": "^5.0.0",
63-
"ts-node": "^10.4.0",
64-
"typescript": "^4.5.5",
60+
"ts-node": "^10.9.1",
61+
"typescript": "^4.8.4",
6562
"xo": "^0.44.0"
6663
},
6764
"ava": {

source/index.ts

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ type Task<TaskResultType> =
88
| ((options: TaskOptions) => PromiseLike<TaskResultType>)
99
| ((options: TaskOptions) => TaskResultType);
1010

11-
const timeoutError = new TimeoutError();
12-
1311
/**
1412
The error thrown by `queue.add()` when a job is aborted before it is run. See `signal`.
1513
*/
@@ -41,7 +39,7 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
4139

4240
readonly #queueClass: new () => QueueType;
4341

44-
#pendingCount = 0;
42+
#pending = 0;
4543

4644
// The `!` is needed because of https://github.com/microsoft/TypeScript/issues/32194
4745
#concurrency!: number;
@@ -96,23 +94,15 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
9694
}
9795

9896
get #doesConcurrentAllowAnother(): boolean {
99-
return this.#pendingCount < this.#concurrency;
97+
return this.#pending < this.#concurrency;
10098
}
10199

102100
#next(): void {
103-
this.#pendingCount--;
101+
this.#pending--;
104102
this.#tryToStartAnother();
105103
this.emit('next');
106104
}
107105

108-
#emitEvents(): void {
109-
this.emit('empty');
110-
111-
if (this.#pendingCount === 0) {
112-
this.emit('idle');
113-
}
114-
}
115-
116106
#onResumeInterval(): void {
117107
this.#onInterval();
118108
this.#initializeIntervalIfNeeded();
@@ -127,7 +117,7 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
127117
if (delay < 0) {
128118
// Act as the interval was done
129119
// We don't need to resume it here because it will be resumed on line 160
130-
this.#intervalCount = (this.#carryoverConcurrencyCount) ? this.#pendingCount : 0;
120+
this.#intervalCount = (this.#carryoverConcurrencyCount) ? this.#pending : 0;
131121
} else {
132122
// Act as the interval is pending
133123
if (this.#timeoutId === undefined) {
@@ -156,7 +146,11 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
156146

157147
this.#intervalId = undefined;
158148

159-
this.#emitEvents();
149+
this.emit('empty');
150+
151+
if (this.#pending === 0) {
152+
this.emit('idle');
153+
}
160154

161155
return false;
162156
}
@@ -199,12 +193,12 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
199193
}
200194

201195
#onInterval(): void {
202-
if (this.#intervalCount === 0 && this.#pendingCount === 0 && this.#intervalId) {
196+
if (this.#intervalCount === 0 && this.#pending === 0 && this.#intervalId) {
203197
clearInterval(this.#intervalId);
204198
this.#intervalId = undefined;
205199
}
206200

207-
this.#intervalCount = this.#carryoverConcurrencyCount ? this.#pendingCount : 0;
201+
this.#intervalCount = this.#carryoverConcurrencyCount ? this.#pending : 0;
208202
this.#processQueue();
209203
}
210204

@@ -230,48 +224,69 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
230224
this.#processQueue();
231225
}
232226

227+
async #throwOnAbort(signal: AbortSignal): Promise<never> {
228+
return new Promise((_resolve, reject) => {
229+
signal.addEventListener('abort', () => {
230+
// TODO: Reject with signal.throwIfAborted() when targeting Node.js 18
231+
// TODO: Use ABORT_ERR code when targeting Node.js 16 (https://nodejs.org/docs/latest-v16.x/api/errors.html#abort_err)
232+
reject(new AbortError('The task was aborted.'));
233+
}, {once: true});
234+
});
235+
}
236+
233237
/**
234238
Adds a sync or async task to the queue. Always returns a promise.
235239
*/
236-
async add<TaskResultType>(fn: Task<TaskResultType>, options: Partial<EnqueueOptionsType> = {}): Promise<TaskResultType> {
237-
return new Promise<TaskResultType>((resolve, reject) => {
238-
const run = async (): Promise<void> => {
239-
this.#pendingCount++;
240+
async add<TaskResultType>(function_: Task<TaskResultType>, options?: Partial<EnqueueOptionsType>): Promise<TaskResultType | void>;
241+
async add<TaskResultType>(function_: Task<TaskResultType>, options: {throwOnTimeout: true} & Exclude<EnqueueOptionsType, 'throwOnTimeout'>): Promise<TaskResultType>;
242+
async add<TaskResultType>(function_: Task<TaskResultType>, options: Partial<EnqueueOptionsType> = {}): Promise<TaskResultType | void> {
243+
options = {
244+
timeout: this.timeout,
245+
throwOnTimeout: this.#throwOnTimeout,
246+
...options,
247+
};
248+
249+
return new Promise((resolve, reject) => {
250+
this.#queue.enqueue(async () => {
251+
this.#pending++;
240252
this.#intervalCount++;
241253

242254
try {
255+
// TODO: Use options.signal?.throwIfAborted() when targeting Node.js 18
243256
if (options.signal?.aborted) {
244257
// TODO: Use ABORT_ERR code when targeting Node.js 16 (https://nodejs.org/docs/latest-v16.x/api/errors.html#abort_err)
245-
reject(new AbortError('The task was aborted.'));
246-
return;
258+
throw new AbortError('The task was aborted.');
247259
}
248260

249-
const operation = (this.timeout === undefined && options.timeout === undefined) ? fn({signal: options.signal}) : pTimeout(
250-
Promise.resolve(fn({signal: options.signal})),
251-
(options.timeout === undefined ? this.timeout : options.timeout)!,
252-
() => {
253-
if (options.throwOnTimeout === undefined ? this.#throwOnTimeout : options.throwOnTimeout) {
254-
reject(timeoutError);
255-
}
261+
let operation = function_({signal: options.signal});
256262

257-
return undefined;
258-
},
259-
);
263+
if (options.timeout) {
264+
operation = pTimeout(Promise.resolve(operation), options.timeout);
265+
}
266+
267+
if (options.signal) {
268+
operation = Promise.race([operation, this.#throwOnAbort(options.signal)]);
269+
}
260270

261271
const result = await operation;
262-
resolve(result!);
272+
resolve(result);
263273
this.emit('completed', result);
264274
} catch (error: unknown) {
275+
if (error instanceof TimeoutError && !options.throwOnTimeout) {
276+
resolve();
277+
return;
278+
}
279+
265280
reject(error);
266281
this.emit('error', error);
282+
} finally {
283+
this.#next();
267284
}
285+
}, options);
268286

269-
this.#next();
270-
};
287+
this.emit('add');
271288

272-
this.#queue.enqueue(run, options);
273289
this.#tryToStartAnother();
274-
this.emit('add');
275290
});
276291
}
277292

@@ -282,8 +297,16 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
282297
*/
283298
async addAll<TaskResultsType>(
284299
functions: ReadonlyArray<Task<TaskResultsType>>,
285-
options?: EnqueueOptionsType,
286-
): Promise<TaskResultsType[]> {
300+
options?: Partial<EnqueueOptionsType>,
301+
): Promise<Array<TaskResultsType | void>>;
302+
async addAll<TaskResultsType>(
303+
functions: ReadonlyArray<Task<TaskResultsType>>,
304+
options?: {throwOnTimeout: true} & Partial<Exclude<EnqueueOptionsType, 'throwOnTimeout'>>,
305+
): Promise<TaskResultsType[]>
306+
async addAll<TaskResultsType>(
307+
functions: ReadonlyArray<Task<TaskResultsType>>,
308+
options?: Partial<EnqueueOptionsType>,
309+
): Promise<Array<TaskResultsType | void>> {
287310
return Promise.all(functions.map(async function_ => this.add(function_, options)));
288311
}
289312

@@ -352,7 +375,7 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
352375
*/
353376
async onIdle(): Promise<void> {
354377
// Instantly resolve if none pending and if nothing else is queued
355-
if (this.#pendingCount === 0 && this.#queue.size === 0) {
378+
if (this.#pending === 0 && this.#queue.size === 0) {
356379
return;
357380
}
358381

@@ -395,7 +418,7 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
395418
Number of running items (no longer in the queue).
396419
*/
397420
get pending(): number {
398-
return this.#pendingCount;
421+
return this.#pending;
399422
}
400423

401424
/**

test/test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -773,8 +773,8 @@ test('pause should work when throttled', async t => {
773773
autoStart: false,
774774
});
775775

776-
const values = [0, 1, 2, 3];
777-
const firstValue = [0, 1];
776+
const values = [0, 1, 2, 3];
777+
const firstValue = [0, 1];
778778
const secondValue = [0, 1, 2, 3];
779779

780780
for (const value of values) {
@@ -1115,3 +1115,22 @@ test('should pass AbortSignal instance to job', async t => {
11151115
t.is(controller.signal, signal!);
11161116
}, {signal: controller.signal});
11171117
});
1118+
1119+
test('aborting multiple jobs at the same time', async t => {
1120+
const queue = new PQueue({concurrency: 1});
1121+
1122+
const controller1 = new AbortController();
1123+
const controller2 = new AbortController();
1124+
1125+
const task1 = queue.add(async () => new Promise(() => {}), {signal: controller1.signal}); // eslint-disable-line @typescript-eslint/no-empty-function
1126+
const task2 = queue.add(async () => new Promise(() => {}), {signal: controller2.signal}); // eslint-disable-line @typescript-eslint/no-empty-function
1127+
1128+
setTimeout(() => {
1129+
controller1.abort();
1130+
controller2.abort();
1131+
}, 0);
1132+
1133+
await t.throwsAsync(task1, {instanceOf: AbortError});
1134+
await t.throwsAsync(task2, {instanceOf: AbortError});
1135+
t.like(queue, {size: 0, pending: 0});
1136+
});

tsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
{
22
"extends": "@sindresorhus/tsconfig",
33
"compilerOptions": {
4-
"outDir": "dist",
5-
"noPropertyAccessFromIndexSignature": false
4+
"outDir": "dist"
65
},
76
"include": [
87
"source"

0 commit comments

Comments
 (0)