From b9f7dca6829e92ab37dd130f5dde7f16a8269413 Mon Sep 17 00:00:00 2001 From: Calum Jarvis Date: Tue, 31 Mar 2026 10:42:26 +0100 Subject: [PATCH 1/2] fix(async-queuer): keep pendingTick true during wait period When addItem() is called on a running queue during the wait period, it checks isRunning && !pendingTick to decide whether to trigger #tick(). Previously, pendingTick was set to false synchronously at the end of #tick(), even when async work (execute + wait timer) was still pending. This caused addItem() to trigger immediate processing that bypassed the configured wait. This fix tracks whether async work was scheduled in the while loop and only clears pendingTick when no async work is pending. Closes TanStack/pacer#188 --- .changeset/fix-async-queuer-pending-tick.md | 9 ++++++ packages/pacer/src/async-queuer.ts | 6 +++- packages/pacer/tests/async-queuer.test.ts | 34 +++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-async-queuer-pending-tick.md diff --git a/.changeset/fix-async-queuer-pending-tick.md b/.changeset/fix-async-queuer-pending-tick.md new file mode 100644 index 000000000..e9591d7c0 --- /dev/null +++ b/.changeset/fix-async-queuer-pending-tick.md @@ -0,0 +1,9 @@ +--- +'@tanstack/pacer': patch +--- + +fix(async-queuer): keep pendingTick true during wait period + +When `addItem()` is called on a running queue during the wait period, it checks `isRunning && !pendingTick` to decide whether to trigger `#tick()`. Previously, `pendingTick` was set to `false` synchronously at the end of `#tick()`, even when async work was still pending. This caused `addItem()` to trigger immediate processing that bypassed the configured `wait` delay. + +This fix tracks whether async work was scheduled and only clears `pendingTick` when no async work is pending. diff --git a/packages/pacer/src/async-queuer.ts b/packages/pacer/src/async-queuer.ts index 22925a170..8fe4cbefa 100644 --- a/packages/pacer/src/async-queuer.ts +++ b/packages/pacer/src/async-queuer.ts @@ -431,6 +431,7 @@ export class AsyncQueuer { this.#checkExpiredItems() // Process items concurrently up to the concurrency limit + let scheduledAsyncWork = false const activeItems = this.store.state.activeItems while ( activeItems.length < this.#getConcurrency() && @@ -444,6 +445,7 @@ export class AsyncQueuer { this.#setState({ activeItems, }) + scheduledAsyncWork = true ;(async () => { await this.execute() @@ -458,7 +460,9 @@ export class AsyncQueuer { })() } - this.#setState({ pendingTick: false }) + if (!scheduledAsyncWork) { + this.#setState({ pendingTick: false }) + } } /** diff --git a/packages/pacer/tests/async-queuer.test.ts b/packages/pacer/tests/async-queuer.test.ts index 5497797bc..1478335de 100644 --- a/packages/pacer/tests/async-queuer.test.ts +++ b/packages/pacer/tests/async-queuer.test.ts @@ -836,6 +836,40 @@ describe('AsyncQueuer', () => { expect(results).toHaveLength(3) expect(results[2]).toBe('third') }) + + it('should respect wait period when addItem is called during processing', async () => { + const results: Array = [] + const asyncQueuer = new AsyncQueuer( + async (item) => { + results.push(item) + return item + }, + { + wait: 100, + concurrency: 1, + started: false, + }, + ) + + asyncQueuer.addItem('first') + asyncQueuer.start() + + // 'first' processes immediately + await vi.advanceTimersByTimeAsync(0) + expect(results).toEqual(['first']) + + // During the 100ms wait period, add a new item + await vi.advanceTimersByTimeAsync(50) + asyncQueuer.addItem('second') + + // 'second' should NOT have processed yet — still in the wait period + await vi.advanceTimersByTimeAsync(0) + expect(results).toEqual(['first']) + + // After the remaining wait time, 'second' should process + await vi.advanceTimersByTimeAsync(50) + expect(results).toEqual(['first', 'second']) + }) }) describe('error handling', () => { From d1a8fe1b113dedad4bb10b42f298feb5110af2bc Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:50:11 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- docs/reference/classes/AsyncQueuer.md | 30 +++++++++++++------------- docs/reference/functions/asyncQueue.md | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/reference/classes/AsyncQueuer.md b/docs/reference/classes/AsyncQueuer.md index f09c0ee28..4ff6056f4 100644 --- a/docs/reference/classes/AsyncQueuer.md +++ b/docs/reference/classes/AsyncQueuer.md @@ -168,7 +168,7 @@ Defined in: [async-queuer.ts:316](https://github.com/TanStack/pacer/blob/main/pa abort(): void; ``` -Defined in: [async-queuer.ts:836](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L836) +Defined in: [async-queuer.ts:840](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L840) Aborts all ongoing executions with the internal abort controllers. Does NOT clear out the items. @@ -188,7 +188,7 @@ addItem( runOnItemsChange): boolean; ``` -Defined in: [async-queuer.ts:474](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L474) +Defined in: [async-queuer.ts:478](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L478) Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. Items can be inserted based on priority or at the front/back depending on configuration. @@ -226,7 +226,7 @@ queuer.addItem('task2', 'front'); clear(): void; ``` -Defined in: [async-queuer.ts:801](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L801) +Defined in: [async-queuer.ts:805](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L805) Removes all pending items from the queue. Does NOT affect active tasks. @@ -243,7 +243,7 @@ Does NOT affect active tasks. execute(position?): Promise; ``` -Defined in: [async-queuer.ts:609](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L609) +Defined in: [async-queuer.ts:613](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L613) Removes and returns the next item from the queue and executes the task function with it. @@ -273,7 +273,7 @@ queuer.execute('back'); flush(numberOfItems, position?): Promise; ``` -Defined in: [async-queuer.ts:657](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L657) +Defined in: [async-queuer.ts:661](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L661) Processes a specified number of items to execute immediately with no wait time If no numberOfItems is provided, all items will be processed @@ -300,7 +300,7 @@ If no numberOfItems is provided, all items will be processed flushAsBatch(batchFunction): Promise; ``` -Defined in: [async-queuer.ts:671](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L671) +Defined in: [async-queuer.ts:675](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L675) Processes all items in the queue as a batch using the provided function as an argument The queue is cleared after processing @@ -323,7 +323,7 @@ The queue is cleared after processing getAbortSignal(executeCount?): AbortSignal | null; ``` -Defined in: [async-queuer.ts:826](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L826) +Defined in: [async-queuer.ts:830](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L830) Returns the AbortSignal for a specific execution. If no executeCount is provided, returns the signal for the most recent execution. @@ -364,7 +364,7 @@ const queuer = new AsyncQueuer( getNextItem(position): TValue | undefined; ``` -Defined in: [async-queuer.ts:557](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L557) +Defined in: [async-queuer.ts:561](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L561) Removes and returns the next item from the queue without executing the task function. Use for manual queue management. Normally, use execute() to process items. @@ -396,7 +396,7 @@ queuer.getNextItem('back'); peekActiveItems(): TValue[]; ``` -Defined in: [async-queuer.ts:763](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L763) +Defined in: [async-queuer.ts:767](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L767) Returns the items currently being processed (active tasks). @@ -412,7 +412,7 @@ Returns the items currently being processed (active tasks). peekAllItems(): TValue[]; ``` -Defined in: [async-queuer.ts:756](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L756) +Defined in: [async-queuer.ts:760](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L760) Returns a copy of all items in the queue, including active and pending items. @@ -428,7 +428,7 @@ Returns a copy of all items in the queue, including active and pending items. peekNextItem(position): TValue | undefined; ``` -Defined in: [async-queuer.ts:746](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L746) +Defined in: [async-queuer.ts:750](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L750) Returns the next item in the queue without removing it. @@ -457,7 +457,7 @@ queuer.peekNextItem('back'); // back peekPendingItems(): TValue[]; ``` -Defined in: [async-queuer.ts:770](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L770) +Defined in: [async-queuer.ts:774](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L774) Returns the items waiting to be processed (pending tasks). @@ -473,7 +473,7 @@ Returns the items waiting to be processed (pending tasks). reset(): void; ``` -Defined in: [async-queuer.ts:847](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L847) +Defined in: [async-queuer.ts:851](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L851) Resets the queuer state to its default values @@ -511,7 +511,7 @@ Updates the queuer options. New options are merged with existing options. start(): void; ``` -Defined in: [async-queuer.ts:777](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L777) +Defined in: [async-queuer.ts:781](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L781) Starts processing items in the queue. If already running, does nothing. @@ -527,7 +527,7 @@ Starts processing items in the queue. If already running, does nothing. stop(): void; ``` -Defined in: [async-queuer.ts:787](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L787) +Defined in: [async-queuer.ts:791](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L791) Stops processing items in the queue. Does not clear the queue. diff --git a/docs/reference/functions/asyncQueue.md b/docs/reference/functions/asyncQueue.md index 0c6d3d276..26982f63f 100644 --- a/docs/reference/functions/asyncQueue.md +++ b/docs/reference/functions/asyncQueue.md @@ -9,7 +9,7 @@ title: asyncQueue function asyncQueue(fn, initialOptions): (item, position, runOnItemsChange) => boolean; ``` -Defined in: [async-queuer.ts:919](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L919) +Defined in: [async-queuer.ts:923](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L923) Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks. The queuer is started automatically and ready to process items.