Skip to content

Commit 5362aa7

Browse files
committed
Add empty event
Signed-off-by: Richie Bendall <[email protected]>
1 parent 345e553 commit 5362aa7

File tree

4 files changed

+74
-54
lines changed

4 files changed

+74
-54
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"delay": "^5.0.0",
5858
"in-range": "^3.0.0",
5959
"nyc": "^15.1.0",
60+
"p-defer": "^4.0.0",
6061
"random-int": "^3.0.0",
6162
"time-span": "^5.0.0",
6263
"ts-node": "^10.4.0",

readme.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,10 +308,18 @@ queue.on('error', error => {
308308
queue.add(() => Promise.reject(new Error('error')));
309309
```
310310

311+
#### empty
312+
313+
Emitted every time the queue becomes empty.
314+
315+
Useful if you for example add additional items at a later time.
316+
311317
#### idle
312318

313319
Emitted every time the queue becomes empty and all promises have completed; `queue.size === 0 && queue.pending === 0`.
314320

321+
The difference with `empty` is that `idle` guarantees that all work from the queue has finished. `empty` merely signals that the queue is empty, but it could mean that some promises haven't completed yet.
322+
315323
```js
316324
import delay from 'delay';
317325
import PQueue from 'p-queue';

source/index.ts

Lines changed: 32 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,23 @@ import {Queue, RunFunction} from './queue.js';
44
import PriorityQueue from './priority-queue.js';
55
import {QueueAddOptions, Options, TaskOptions} from './options.js';
66

7-
type ResolveFunction<T = void> = (value?: T | PromiseLike<T>) => void;
8-
97
type Task<TaskResultType> =
108
| ((options: TaskOptions) => PromiseLike<TaskResultType>)
119
| ((options: TaskOptions) => TaskResultType);
1210

13-
// eslint-disable-next-line @typescript-eslint/no-empty-function
14-
const empty = (): void => {};
15-
1611
const timeoutError = new TimeoutError();
1712

1813
/**
1914
The error thrown by `queue.add()` when a job is aborted before it is run. See `signal`.
2015
*/
2116
export class AbortError extends Error {}
2217

18+
type EventName = 'active' | 'idle' | 'empty' | 'add' | 'next' | 'completed' | 'error';
19+
2320
/**
2421
Promise queue with concurrency control.
2522
*/
26-
export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsType> = PriorityQueue, EnqueueOptionsType extends QueueAddOptions = QueueAddOptions> extends EventEmitter<'active' | 'idle' | 'add' | 'next' | 'completed' | 'error'> {
23+
export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsType> = PriorityQueue, EnqueueOptionsType extends QueueAddOptions = QueueAddOptions> extends EventEmitter<EventName> {
2724
readonly #carryoverConcurrencyCount: boolean;
2825

2926
readonly #isIntervalIgnored: boolean;
@@ -51,13 +48,14 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
5148

5249
#isPaused: boolean;
5350

54-
#resolveEmpty: ResolveFunction = empty;
55-
56-
#resolveIdle: ResolveFunction = empty;
51+
readonly #throwOnTimeout: boolean;
5752

58-
#timeout?: number;
53+
/**
54+
Per-operation timeout in milliseconds. Operations fulfill once `timeout` elapses if they haven't already.
5955
60-
readonly #throwOnTimeout: boolean;
56+
Applies to each future operation.
57+
*/
58+
timeout?: number;
6159

6260
constructor(options?: Options<QueueType, EnqueueOptionsType>) {
6361
super();
@@ -88,7 +86,7 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
8886
this.#queue = new options.queueClass!();
8987
this.#queueClass = options.queueClass!;
9088
this.concurrency = options.concurrency!;
91-
this.#timeout = options.timeout;
89+
this.timeout = options.timeout;
9290
this.#throwOnTimeout = options.throwOnTimeout === true;
9391
this.#isPaused = options.autoStart === false;
9492
}
@@ -107,13 +105,10 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
107105
this.emit('next');
108106
}
109107

110-
#resolvePromises(): void {
111-
this.#resolveEmpty();
112-
this.#resolveEmpty = empty;
108+
#emitEvents(): void {
109+
this.emit('empty');
113110

114111
if (this.#pendingCount === 0) {
115-
this.#resolveIdle();
116-
this.#resolveIdle = empty;
117112
this.emit('idle');
118113
}
119114
}
@@ -124,7 +119,7 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
124119
this.#timeoutId = undefined;
125120
}
126121

127-
#isIntervalPaused(): boolean {
122+
get #isIntervalPaused(): boolean {
128123
const now = Date.now();
129124

130125
if (this.#intervalId === undefined) {
@@ -161,13 +156,13 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
161156

162157
this.#intervalId = undefined;
163158

164-
this.#resolvePromises();
159+
this.#emitEvents();
165160

166161
return false;
167162
}
168163

169164
if (!this.#isPaused) {
170-
const canInitializeInterval = !this.#isIntervalPaused();
165+
const canInitializeInterval = !this.#isIntervalPaused;
171166
if (this.#doesIntervalAllowAnother && this.#doesConcurrentAllowAnother) {
172167
const job = this.#queue.dequeue();
173168
if (!job) {
@@ -251,9 +246,9 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
251246
return;
252247
}
253248

254-
const operation = (this.#timeout === undefined && options.timeout === undefined) ? fn({signal: options.signal}) : pTimeout(
249+
const operation = (this.timeout === undefined && options.timeout === undefined) ? fn({signal: options.signal}) : pTimeout(
255250
Promise.resolve(fn({signal: options.signal})),
256-
(options.timeout === undefined ? this.#timeout : options.timeout)!,
251+
(options.timeout === undefined ? this.timeout : options.timeout)!,
257252
() => {
258253
if (options.throwOnTimeout === undefined ? this.#throwOnTimeout : options.throwOnTimeout) {
259254
reject(timeoutError);
@@ -331,13 +326,7 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
331326
return;
332327
}
333328

334-
return new Promise<void>(resolve => {
335-
const existingResolve = this.#resolveEmpty;
336-
this.#resolveEmpty = () => {
337-
existingResolve();
338-
resolve();
339-
};
340-
});
329+
await this.#onEvent('empty');
341330
}
342331

343332
/**
@@ -353,16 +342,7 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
353342
return;
354343
}
355344

356-
return new Promise<void>(resolve => {
357-
const listener = () => {
358-
if (this.#queue.size < limit) {
359-
this.removeListener('next', listener);
360-
resolve();
361-
}
362-
};
363-
364-
this.on('next', listener);
365-
});
345+
await this.#onEvent('next', () => this.#queue.size < limit);
366346
}
367347

368348
/**
@@ -376,12 +356,21 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
376356
return;
377357
}
378358

379-
return new Promise<void>(resolve => {
380-
const existingResolve = this.#resolveIdle;
381-
this.#resolveIdle = () => {
382-
existingResolve();
359+
await this.#onEvent('idle');
360+
}
361+
362+
async #onEvent(event: EventName, filter?: () => boolean): Promise<void> {
363+
return new Promise(resolve => {
364+
const listener = () => {
365+
if (filter && !filter()) {
366+
return;
367+
}
368+
369+
this.off(event, listener);
383370
resolve();
384371
};
372+
373+
this.on(event, listener);
385374
});
386375
}
387376

@@ -415,17 +404,6 @@ export default class PQueue<QueueType extends Queue<RunFunction, EnqueueOptionsT
415404
get isPaused(): boolean {
416405
return this.#isPaused;
417406
}
418-
419-
get timeout(): number | undefined {
420-
return this.#timeout;
421-
}
422-
423-
/**
424-
Set the timeout for future operations.
425-
*/
426-
set timeout(milliseconds: number | undefined) {
427-
this.#timeout = milliseconds;
428-
}
429407
}
430408

431409
// TODO: Rename `DefaultAddOptions` to `QueueAddOptions` in next major version

test/test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import delay from 'delay';
55
import inRange from 'in-range';
66
import timeSpan from 'time-span';
77
import randomInt from 'random-int';
8+
import pDefer from 'p-defer';
89
import PQueue, {AbortError} from '../source/index.js';
910

1011
const fixture = Symbol('fixture');
@@ -892,6 +893,38 @@ test('should emit idle event when idle', async t => {
892893
t.is(timesCalled, 2);
893894
});
894895

896+
test('should emit empty event when empty', async t => {
897+
const queue = new PQueue({concurrency: 1});
898+
899+
let timesCalled = 0;
900+
queue.on('empty', () => {
901+
timesCalled++;
902+
});
903+
904+
const {resolve: resolveJob1, promise: job1Promise} = pDefer();
905+
const {resolve: resolveJob2, promise: job2Promise} = pDefer();
906+
907+
const job1 = queue.add(async () => job1Promise);
908+
const job2 = queue.add(async () => job2Promise);
909+
t.is(queue.size, 1);
910+
t.is(queue.pending, 1);
911+
t.is(timesCalled, 0);
912+
913+
resolveJob1();
914+
await job1;
915+
916+
t.is(queue.size, 0);
917+
t.is(queue.pending, 1);
918+
t.is(timesCalled, 0);
919+
920+
resolveJob2();
921+
await job2;
922+
923+
t.is(queue.size, 0);
924+
t.is(queue.pending, 0);
925+
t.is(timesCalled, 1);
926+
});
927+
895928
test('should emit add event when adding task', async t => {
896929
const queue = new PQueue({concurrency: 1});
897930

0 commit comments

Comments
 (0)