diff --git a/bench.ts b/bench.ts index 4694971..3a5f606 100644 --- a/bench.ts +++ b/bench.ts @@ -76,6 +76,18 @@ addBenchmark('large-fifo-backlog', async () => { await queue.onIdle(); }); +addBenchmark('add-no-options-paused', async () => { + const queue = new PQueue({autoStart: false}); + + for (let i = 0; i < largeBacklogTaskCount; i++) { + queue.add(noop); + } + + if (queue.size !== largeBacklogTaskCount) { + throw new Error('Queued task count mismatch'); + } +}); + addBenchmark('priority-queue-dequeue', async () => { const queue = new PriorityQueue(); diff --git a/source/index.ts b/source/index.ts index 7859e7d..3972965 100644 --- a/source/index.ts +++ b/source/index.ts @@ -441,32 +441,44 @@ export default class PQueue(function_: Task, options?: Partial): Promise; - async add(function_: Task, options: Partial = {}): Promise { - // Create a copy to avoid mutating the original options object - options = { - timeout: this.timeout, - ...options, - // Assign unique ID if not provided - id: options.id ?? (this.#idAssigner++).toString(), - }; + async add(function_: Task, options?: Partial): Promise { + const id = options?.id ?? (this.#idAssigner++).toString(); + + let options_: Partial; + if (options === undefined) { + // Hot path for `queue.add(fn)`: avoid object spread and only set the fields we need. + options_ = {}; + options_.timeout = this.timeout; + options_.id = id; + } else { + // Create a copy to avoid mutating the original options object + options_ = { + timeout: this.timeout, + ...options, + id, + }; + } + + const {signal} = options_ as TaskOptions; + const {timeout, priority = 0} = options_; return new Promise((resolve, reject) => { // Create a unique symbol for tracking this task - const taskSymbol = Symbol(`task-${options.id}`); + const taskSymbol = Symbol(`task-${id}`); - let cleanupQueueAbortHandler = () => undefined; + let cleanupQueueAbortHandler: (() => void) | undefined; const run = async () => { // Task is now running — remove the queued-state abort listener - cleanupQueueAbortHandler(); + cleanupQueueAbortHandler?.(); this.#pending++; // Track this running task this.#runningTasks.set(taskSymbol, { - id: options.id, - priority: options.priority ?? 0, // Match priority-queue default + id, + priority, // Match priority-queue default startTime: Date.now(), - timeout: options.timeout, + timeout, }); let eventListener: (() => void) | undefined; @@ -475,7 +487,7 @@ export default class PQueue((_resolve, reject) => { eventListener = () => { reject(signal.reason); @@ -517,7 +527,7 @@ export default class PQueue { if (this.#queue instanceof PriorityQueue) { @@ -538,33 +548,33 @@ export default class PQueue { - cleanupQueueAbortHandler(); + cleanupQueueAbortHandler?.(); removeQueuedTask(); reject(signal.reason); this.#tryToStartAnother(); this.emit('next'); }; - cleanupQueueAbortHandler = () => { + const cleanup = () => { signal.removeEventListener('abort', queueAbortHandler); - this.#queueAbortListenerCleanupFunctions.delete(cleanupQueueAbortHandler); + this.#queueAbortListenerCleanupFunctions.delete(cleanup); }; + cleanupQueueAbortHandler = cleanup; + if (signal.aborted) { queueAbortHandler(); return; } signal.addEventListener('abort', queueAbortHandler, {once: true}); - this.#queueAbortListenerCleanupFunctions.add(cleanupQueueAbortHandler); + this.#queueAbortListenerCleanupFunctions.add(cleanup); } this.emit('add');