Skip to content

fix: falsy queue items, AsyncRetryer memory leak, wait bypass, and Awaited return types - #246

Merged
KevinVandy merged 4 commits into
mainfrom
fix/async-utils-issue-batch
Aug 7, 2026
Merged

fix: falsy queue items, AsyncRetryer memory leak, wait bypass, and Awaited return types#246
KevinVandy merged 4 commits into
mainfrom
fix/async-utils-issue-batch

Conversation

@KevinVandy

@KevinVandy KevinVandy commented Aug 6, 2026

Copy link
Copy Markdown
Member

@tanstack/pacer

  • AsyncQueuer / Queuer — falsy items (0, '', false) are no longer silently skipped by the processing loop (=== undefined check), and addItem(null) no longer throws reading .priority. Closes AsyncQueuer silently drops falsy queue items (0, "", false, null) #200
  • AsyncRetryer — removed the devtools event-client integration. Retryers are created per-execution by the async utils, so every execution permanently accumulated an event listener, a live registry entry ("<key>-retryer-N", or "undefined-retryer-N" with no key), and queued devtools events — unbounded memory growth, worst in Node. No devtools package ever consumed retryer events, so no functionality is lost; parents now pass asyncRetryerOptions through unmodified and key remains a plain identifier. Closes AsyncQueuer leaks memory in Node.js via unintended retryer key propagation #198
  • AsyncQueuerpendingTick now stays true while executions or wait timers are pending (matching the sync Queuer), so addItem() during the wait window no longer triggers immediate processing. Task errors no longer kill the processing chain or emit unhandled rejections, and flush/flushAsBatch restart the tick chain they interrupt. Closes Bug: AsyncQueuer addItem() bypasses wait period when called during active processing #188
  • AsyncDebouncer / AsyncThrottler / AsyncRateLimitermaybeExecute, flush, lastResult state, and onSuccess (plus the asyncDebounce/asyncThrottle/asyncRateLimit helpers) now use Awaited<ReturnType<TFn>> instead of double-wrapping promises. Closes Incorrect inferred type for the async functions #156

@tanstack/react-pacer, @tanstack/preact-pacer

  • useAsyncDebouncedCallback, useAsyncThrottledCallback, and useAsyncRateLimitedCallback now return Promise<Awaited<ReturnType<TFn>> | undefined>, matching the angular adapter and actual runtime behavior.

Builds on the diagnoses in #201, #199, #189, and #157 — authors are co-credited on the commit. #189's approach needed extension: without error handling in the tick chain and pendingTick reconciliation in flush, the queue could deadlock.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Preserved falsy and nullish queue items during processing.
    • Improved asynchronous queue timing, flushing, recovery, and error handling.
    • Rejected unsupported undefined queue items with appropriate rejection handling.
    • Improved concurrent execution tracking for asynchronous rate limiting.
    • Prevented unnecessary internal retry activity from contributing to devtools memory growth.
    • Corrected async callback results to resolve with awaited values or undefined when applicable.
  • Documentation

    • Updated React and Preact callback documentation to reflect awaited results and possible undefined values.
    • Refreshed API source references, queue behavior, and return type descriptions.

…Awaited return types

Closes #200, #198, #188, #156

Co-Authored-By: Simon Meyer <33751665+simonmeyerrr@users.noreply.github.com>
Co-Authored-By: Calum Jarvis <92122419+caluhm@users.noreply.github.com>
Co-Authored-By: Vincent Ricard <vincent.ricard@albertapp.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 6, 2026 23:06
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 491aae1d-5c9e-434b-bd79-8ee03e246fd8

📥 Commits

Reviewing files that changed from the base of the PR and between 58a9745 and edb6b61.

📒 Files selected for processing (13)
  • docs/framework/preact/reference/functions/useAsyncThrottledCallback.md
  • docs/framework/react/reference/functions/useAsyncThrottledCallback.md
  • docs/reference/classes/AsyncQueuer.md
  • docs/reference/classes/AsyncRateLimiter.md
  • docs/reference/functions/asyncQueue.md
  • docs/reference/functions/asyncRateLimit.md
  • packages/pacer/src/async-queuer.ts
  • packages/pacer/src/async-rate-limiter.ts
  • packages/pacer/tests/async-queuer.test.ts
  • packages/pacer/tests/async-rate-limiter.test.ts
  • packages/pacer/tests/async-throttler.test.ts
  • packages/preact-pacer/src/async-throttler/useAsyncThrottledCallback.ts
  • packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • docs/reference/functions/asyncQueue.md
  • docs/framework/preact/reference/functions/useAsyncThrottledCallback.md
  • packages/pacer/tests/async-throttler.test.ts
  • docs/reference/functions/asyncRateLimit.md
  • packages/pacer/tests/async-rate-limiter.test.ts
  • packages/pacer/tests/async-queuer.test.ts
  • packages/preact-pacer/src/async-throttler/useAsyncThrottledCallback.ts
  • packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts
  • packages/pacer/src/async-queuer.ts
  • docs/framework/react/reference/functions/useAsyncThrottledCallback.md
  • packages/pacer/src/async-rate-limiter.ts
  • docs/reference/classes/AsyncQueuer.md

📝 Walkthrough

Walkthrough

The PR fixes AsyncQueuer item and tick handling, removes internal AsyncRetryer devtools registration, corrects async return types with Awaited, updates React and Preact hooks, refreshes reference links, and adds tests and patch changesets.

Changes

Async utility behavior

Layer / File(s) Summary
AsyncQueuer processing and nullish items
packages/pacer/src/async-queuer.ts, packages/pacer/src/queuer.ts, packages/pacer/tests/async-queuer.test.ts, packages/pacer/tests/queuer.test.ts
Falsy and nullish items are processed. Pending ticks remain active during execution and wait periods. Errors, flushes, duplicate items, and restart behavior are covered.
AsyncRetryer devtools isolation
packages/pacer/src/async-retryer.ts, packages/pacer/tests/async-*.test.ts
Internal retryers no longer subscribe to or emit devtools events. Parent utilities retain their own registration behavior.
Awaited async result types
packages/pacer/src/async-debouncer.ts, packages/pacer/src/async-rate-limiter.ts, packages/pacer/src/async-throttler.ts, packages/react-pacer/src/..., packages/preact-pacer/src/...
Async results now use Awaited<ReturnType<TFn>>. Skipped, superseded, throttled, or disabled calls may resolve to undefined.
Reference documentation and release metadata
docs/reference/..., docs/framework/..., .changeset/*
API signatures, state and callback types, source links, retryer key behavior, framework hook behavior, and patch release notes are updated.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the PR's main fixes: falsy queue items, retryer memory growth, wait handling, and awaited return types.
Description check ✅ Passed The description gives detailed changes, motivation, affected packages, linked issues, and testing context, but omits the template checklist and release-impact headings.
Linked Issues check ✅ Passed The changes address the linked issues by fixing falsy and null items, retryer memory growth, wait handling, and nested async return types.
Out of Scope Changes check ✅ Passed The source changes, tests, documentation, and changesets are directly related to the linked issue objectives and stated PR goals.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/async-utils-issue-batch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/pacer/src/async-queuer.ts (1)

434-475: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Track active executions by identity.

Line 444 stores only the item value in activeItems. The execute cleanup removes every active entry equal to the completed item.

If two equal items execute concurrently, such as two 0 values, the first completion removes both entries. A later tick can then exceed concurrency.

Track each scheduled execution with a unique identity. Remove only the matching execution when it settles. Add a concurrency test with duplicate primitive values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pacer/src/async-queuer.ts` around lines 434 - 475, Update the
scheduling flow around peekNextItem, activeItems, and execute so each scheduled
execution has a unique identity rather than tracking only the item value. Ensure
settlement cleanup removes only the matching execution, preserving accurate
concurrency when duplicate primitive items run concurrently. Add a test covering
duplicate values such as two 0 items with concurrent execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/framework/preact/reference/functions/useAsyncThrottledCallback.md`:
- Around line 22-23: Update the return-value description for
useAsyncThrottledCallback to state that disabled calls resolve with undefined,
while throttled calls using trailing: true schedule a trailing execution whose
promise resolves or rejects with the callback result. Remove the claim that
every throttled call resolves with undefined and document the trailing behavior
explicitly.

In `@packages/pacer/src/async-debouncer.ts`:
- Around line 375-380: The async debouncer flow around
currentAsyncRetryer.execute must distinguish a successful result from undefined
returned for disabled or non-throwing retry failures. Only update
lastResult/successCount and invoke onSuccess when execution actually succeeds;
otherwise preserve the failure path and avoid passing undefined to onSuccess.
Add coverage for a rejected callback with asyncRetryerOptions.throwOnError set
to false.

In `@packages/pacer/src/async-queuer.ts`:
- Around line 676-684: Restore the tick chain for every flush outcome in
AsyncQueuer: at packages/pacer/src/async-queuer.ts:676-684, use
Promise.allSettled for direct execute calls, restore pendingTick and restart
processing before re-throwing any captured failure; at
packages/pacer/src/async-queuer.ts:694-697, move state restoration into finally
and call `#tick`() when the queuer is running and items were added while
batchFunction awaited.

---

Outside diff comments:
In `@packages/pacer/src/async-queuer.ts`:
- Around line 434-475: Update the scheduling flow around peekNextItem,
activeItems, and execute so each scheduled execution has a unique identity
rather than tracking only the item value. Ensure settlement cleanup removes only
the matching execution, preserving accurate concurrency when duplicate primitive
items run concurrently. Add a test covering duplicate values such as two 0 items
with concurrent execution.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d04db3b5-7d17-436c-adb5-315c6854972f

📥 Commits

Reviewing files that changed from the base of the PR and between bd96217 and 4ab7a8f.

📒 Files selected for processing (47)
  • .changeset/async-queuer-falsy-items.md
  • .changeset/async-queuer-pending-tick.md
  • .changeset/awaited-return-types.md
  • .changeset/retryer-key-guard.md
  • docs/framework/preact/reference/functions/useAsyncDebouncedCallback.md
  • docs/framework/preact/reference/functions/useAsyncRateLimitedCallback.md
  • docs/framework/preact/reference/functions/useAsyncThrottledCallback.md
  • docs/framework/react/reference/functions/useAsyncDebouncedCallback.md
  • docs/framework/react/reference/functions/useAsyncRateLimitedCallback.md
  • docs/framework/react/reference/functions/useAsyncThrottledCallback.md
  • docs/reference/classes/AsyncDebouncer.md
  • docs/reference/classes/AsyncQueuer.md
  • docs/reference/classes/AsyncRateLimiter.md
  • docs/reference/classes/AsyncRetryer.md
  • docs/reference/classes/AsyncThrottler.md
  • docs/reference/functions/asyncDebounce.md
  • docs/reference/functions/asyncQueue.md
  • docs/reference/functions/asyncRateLimit.md
  • docs/reference/functions/asyncRetry.md
  • docs/reference/functions/asyncRetryerOptions.md
  • docs/reference/functions/asyncThrottle.md
  • docs/reference/interfaces/AsyncDebouncerOptions.md
  • docs/reference/interfaces/AsyncDebouncerState.md
  • docs/reference/interfaces/AsyncRateLimiterOptions.md
  • docs/reference/interfaces/AsyncRateLimiterState.md
  • docs/reference/interfaces/AsyncRetryerOptions.md
  • docs/reference/interfaces/AsyncRetryerState.md
  • docs/reference/interfaces/AsyncThrottlerOptions.md
  • docs/reference/interfaces/AsyncThrottlerState.md
  • packages/pacer/src/async-debouncer.ts
  • packages/pacer/src/async-queuer.ts
  • packages/pacer/src/async-rate-limiter.ts
  • packages/pacer/src/async-retryer.ts
  • packages/pacer/src/async-throttler.ts
  • packages/pacer/src/queuer.ts
  • packages/pacer/tests/async-debouncer.test.ts
  • packages/pacer/tests/async-queuer.test.ts
  • packages/pacer/tests/async-rate-limiter.test.ts
  • packages/pacer/tests/async-retryer.test.ts
  • packages/pacer/tests/async-throttler.test.ts
  • packages/pacer/tests/queuer.test.ts
  • packages/preact-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts
  • packages/preact-pacer/src/async-rate-limiter/useAsyncRateLimitedCallback.ts
  • packages/preact-pacer/src/async-throttler/useAsyncThrottledCallback.ts
  • packages/react-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts
  • packages/react-pacer/src/async-rate-limiter/useAsyncRateLimitedCallback.ts
  • packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts

Comment thread docs/framework/preact/reference/functions/useAsyncThrottledCallback.md Outdated
Comment thread packages/pacer/src/async-debouncer.ts
Comment thread packages/pacer/src/async-queuer.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes several correctness and ergonomics issues across TanStack Pacer’s async utilities and framework adapters, focusing on queue item handling, wait semantics, memory usage, and TypeScript return types.

Changes:

  • Fix AsyncQueuer/Queuer item handling so falsy values are not skipped and null no longer crashes default priority handling.
  • Remove AsyncRetryer’s devtools event-client integration to prevent unbounded listener/queued-event memory growth from per-execution retryers.
  • Align async utility return types (and react/preact hooks) to Promise<Awaited<ReturnType<TFn>> | undefined> and update docs/tests accordingly.

Reviewed changes

Copilot reviewed 47 out of 47 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts Update hook return type to awaited + undefined and simplify callback wrapper.
packages/react-pacer/src/async-rate-limiter/useAsyncRateLimitedCallback.ts Update hook return type to awaited + undefined.
packages/react-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts Update hook return type to awaited + undefined and simplify callback wrapper.
packages/preact-pacer/src/async-throttler/useAsyncThrottledCallback.ts Update hook return type to awaited + undefined.
packages/preact-pacer/src/async-rate-limiter/useAsyncRateLimitedCallback.ts Update hook return type to awaited + undefined.
packages/preact-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts Update hook return type to awaited + undefined.
packages/pacer/tests/queuer.test.ts Add regression test for null items in sync Queuer.
packages/pacer/tests/async-throttler.test.ts Add devtools-registry and type-inference tests for AsyncThrottler.
packages/pacer/tests/async-retryer.test.ts Add test ensuring AsyncRetryer does not register in devtools registry.
packages/pacer/tests/async-rate-limiter.test.ts Add devtools-registry and type-inference tests for AsyncRateLimiter.
packages/pacer/tests/async-queuer.test.ts Add tests for falsy/null items, wait semantics, error continuity, and devtools registry behavior.
packages/pacer/tests/async-debouncer.test.ts Add devtools-registry and type-inference tests for AsyncDebouncer.
packages/pacer/src/queuer.ts Make default priority access null-safe for null items.
packages/pacer/src/async-throttler.ts Use Awaited<ReturnType<TFn>> in state/returns and stop keying internal retryers for devtools.
packages/pacer/src/async-retryer.ts Remove devtools event-client integration; clarify key as identifier-only.
packages/pacer/src/async-rate-limiter.ts Use Awaited<ReturnType<TFn>> in state/returns and stop keying internal retryers for devtools.
packages/pacer/src/async-queuer.ts Fix falsy-item peeking, wait/pendingTick behavior, error swallowing in tick chain, null-safe priority access, and flush chain restart.
packages/pacer/src/async-debouncer.ts Use Awaited<ReturnType<TFn>> in state/returns and stop keying internal retryers for devtools.
docs/reference/interfaces/AsyncThrottlerState.md Update lastResult docs to awaited type.
docs/reference/interfaces/AsyncThrottlerOptions.md Update onSuccess result type docs to awaited type.
docs/reference/interfaces/AsyncRetryerState.md Update source line references due to code changes.
docs/reference/interfaces/AsyncRetryerOptions.md Update retryer key documentation and source references.
docs/reference/interfaces/AsyncRateLimiterState.md Update lastResult docs to awaited type.
docs/reference/interfaces/AsyncRateLimiterOptions.md Update onSuccess result type docs to awaited type.
docs/reference/interfaces/AsyncDebouncerState.md Update lastResult docs to awaited type.
docs/reference/interfaces/AsyncDebouncerOptions.md Update onSuccess result type docs to awaited type.
docs/reference/functions/asyncThrottle.md Update helper return type docs to awaited type and source refs.
docs/reference/functions/asyncRetryerOptions.md Update source refs.
docs/reference/functions/asyncRetry.md Update source refs.
docs/reference/functions/asyncRateLimit.md Update helper return type docs to awaited type.
docs/reference/functions/asyncQueue.md Update source refs.
docs/reference/functions/asyncDebounce.md Update helper return type docs to awaited type and source refs.
docs/reference/classes/AsyncThrottler.md Update method return type docs + source refs.
docs/reference/classes/AsyncRetryer.md Update source refs after devtools integration removal.
docs/reference/classes/AsyncRateLimiter.md Update method return type docs.
docs/reference/classes/AsyncQueuer.md Update source refs after tick/flush changes.
docs/reference/classes/AsyncDebouncer.md Update method return type docs + source refs.
docs/framework/react/reference/functions/useAsyncThrottledCallback.md Update hook signature and description to include undefined.
docs/framework/react/reference/functions/useAsyncRateLimitedCallback.md Update hook signature to include undefined.
docs/framework/react/reference/functions/useAsyncDebouncedCallback.md Update hook signature and description to include undefined.
docs/framework/preact/reference/functions/useAsyncThrottledCallback.md Update hook signature and description to include undefined.
docs/framework/preact/reference/functions/useAsyncRateLimitedCallback.md Update hook signature to include undefined.
docs/framework/preact/reference/functions/useAsyncDebouncedCallback.md Update hook signature and description to include undefined.
.changeset/retryer-key-guard.md Changeset for removing retryer devtools event-client integration to fix memory growth.
.changeset/awaited-return-types.md Changeset for awaited return types across async utilities + react/preact adapters.
.changeset/async-queuer-pending-tick.md Changeset for AsyncQueuer wait/pendingTick semantics and chain resilience.
.changeset/async-queuer-falsy-items.md Changeset for falsy/null item handling in Queuer/AsyncQueuer.
Suppressed comments (1)

packages/pacer/src/async-queuer.ts:698

  • flushAsBatch() clears timeouts (killing the tick chain) and only sets pendingTick: false after awaiting batchFunction, but it never restarts #tick() if items are enqueued during that await. If pendingTick was true when flushAsBatch() started (common when a wait timer was pending), an addItem() during the batch will not trigger #tick(), and after the batch completes the queue can remain stuck until another item is added or start() is called again.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/pacer/src/async-queuer.ts
Comment thread packages/pacer/src/async-queuer.ts
Comment thread packages/pacer/src/queuer.ts
@nx-cloud

nx-cloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 4ab7a8f

Command Status Duration Result
nx affected --targets=test:eslint,test:sherif,t... ✅ Succeeded 5m 4s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 16s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-06 23:42:37 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 6, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-pacer

npm i https://pkg.pr.new/@tanstack/angular-pacer@246

@tanstack/pacer

npm i https://pkg.pr.new/@tanstack/pacer@246

@tanstack/pacer-devtools

npm i https://pkg.pr.new/@tanstack/pacer-devtools@246

@tanstack/pacer-lite

npm i https://pkg.pr.new/@tanstack/pacer-lite@246

@tanstack/preact-pacer

npm i https://pkg.pr.new/@tanstack/preact-pacer@246

@tanstack/preact-pacer-devtools

npm i https://pkg.pr.new/@tanstack/preact-pacer-devtools@246

@tanstack/react-pacer

npm i https://pkg.pr.new/@tanstack/react-pacer@246

@tanstack/react-pacer-devtools

npm i https://pkg.pr.new/@tanstack/react-pacer-devtools@246

@tanstack/solid-pacer

npm i https://pkg.pr.new/@tanstack/solid-pacer@246

@tanstack/solid-pacer-devtools

npm i https://pkg.pr.new/@tanstack/solid-pacer-devtools@246

commit: edb6b61

…and callback docs

Addresses review feedback: flush()/flushAsBatch() now restore the tick chain
even when a task or batch function rejects (flush uses allSettled and rethrows
after state restoration, with remaining items resuming at the normal wait
spacing); execute() removes a single activeItems occurrence so duplicate item
values cannot exceed the concurrency limit; react/preact async callback
docstrings no longer claim throttled/superseded calls resolve with undefined.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 6, 2026 23:19
@KevinVandy

Copy link
Copy Markdown
Member Author

Addressed the CodeRabbit review in aaf064b:

  • flush()/flushAsBatch() error paths (confirmed via failing test first): flush now uses Promise.allSettled, restores pendingTick, and restarts the tick chain before rethrowing the first failure — remaining items resume at the normal wait spacing. flushAsBatch restores the chain in a finally, picking up items added while the batch function was awaiting.
  • Duplicate-item concurrency accounting (pre-existing, confirmed — measured 3 concurrent executions with concurrency: 2 and duplicate 0 items): execute() now removes a single activeItems occurrence by index instead of filtering all equal values. Went with the minimal identity fix rather than execution-ID tracking; tests cover duplicate primitives at and above the limit.
  • Docs wording: the react/preact throttled/debounced callback docstrings no longer claim throttled or superseded calls resolve with undefined — trailing executions share their result, superseded calls resolve with the most recent result. Reference docs regenerated.
  • onSuccess(undefined) when the internal retryer swallows a failure: real but pre-existing and identical across all five async utilities (not just the debouncer), and the fix requires deciding what "success" means for swallowed errors and aborts — split out as a follow-up issue rather than expanding this PR.

New coverage: 9 tests around flush robustness (rejection, partial flush, batch rejection, stopped queuer, mid-batch adds) and duplicate-item concurrency accounting. 551 tests passing.

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/pacer/src/async-queuer.ts (2)

435-447: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Copy activeItems before mutation.

Line 435 takes the array reference from store state. Line 444 pushes into that same array, so the store state is mutated in place before #setState runs. Subscribers that compare activeItems by reference cannot detect the change, and the state before setState is already dirty. execute at lines 657-663 correctly copies before mutating; use the same pattern here.

♻️ Proposed change
-    const activeItems = this.store.state.activeItems
+    const activeItems = [...this.store.state.activeItems]
     while (
       activeItems.length < this.#getConcurrency() &&
       this.store.state.items.length > 0
     ) {
       const nextItem = this.peekNextItem()
       if (nextItem === undefined) {
         break
       }
       activeItems.push(nextItem)
       this.#setState({
-        activeItems,
+        activeItems: [...activeItems],
       })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pacer/src/async-queuer.ts` around lines 435 - 447, Copy
this.store.state.activeItems into a new array before the while loop in the
active-item scheduling flow, then mutate that copy and pass it to `#setState`.
Follow the existing copy-before-mutation pattern used by execute, while
preserving the current concurrency and next-item checks.

717-727: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider matching the wait spacing that flush applies.

flush restarts the chain through a setTimeout when wait > 0 (lines 689-700). flushAsBatch calls #tick() directly, so the first remaining item runs with no wait spacing after the batch settles. Align both paths if the wait contract must hold after every flush variant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pacer/src/async-queuer.ts` around lines 717 - 727, Update
flushAsBatch’s finally block to restart the tick chain using the same wait-aware
scheduling behavior as flush, rather than calling `#tick`() directly. Reuse the
existing wait handling from flush while preserving the current running-state and
non-empty-items checks.
packages/pacer/tests/async-queuer.test.ts (1)

1459-1471: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen this test so it verifies the finally restart.

flushAsBatch does not set pendingTick before it awaits. Line 1464 calls start(), which sees pendingTick === false and starts the tick chain right away. 'late' therefore processes before the batch settles, and the assertion passes even if the finally block at lines 721-727 is removed. Assert the intermediate state, or drive the item addition after the queuer is already running.

💚 Proposed test tightening
     asyncQueuer.addItem('late')
     asyncQueuer.start()
 
+    // 'late' must not be processed until the batch settles and the chain restarts
+    expect(results).toEqual([])
+
     await vi.advanceTimersByTimeAsync(50)
     await batchPromise
     await vi.advanceTimersByTimeAsync(100)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pacer/tests/async-queuer.test.ts` around lines 1459 - 1471,
Strengthen the flushAsBatch test around asyncQueuer.flushAsBatch,
asyncQueuer.addItem, and asyncQueuer.start so it proves the queued “late” item
is processed only after the batch promise settles and the finally-based restart
runs. Assert the intermediate batches/results state before awaiting
batchPromise, or add the item after the queuer is already running, while
preserving the final expected batch and result assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/pacer/src/async-queuer.ts`:
- Around line 435-447: Copy this.store.state.activeItems into a new array before
the while loop in the active-item scheduling flow, then mutate that copy and
pass it to `#setState`. Follow the existing copy-before-mutation pattern used by
execute, while preserving the current concurrency and next-item checks.
- Around line 717-727: Update flushAsBatch’s finally block to restart the tick
chain using the same wait-aware scheduling behavior as flush, rather than
calling `#tick`() directly. Reuse the existing wait handling from flush while
preserving the current running-state and non-empty-items checks.

In `@packages/pacer/tests/async-queuer.test.ts`:
- Around line 1459-1471: Strengthen the flushAsBatch test around
asyncQueuer.flushAsBatch, asyncQueuer.addItem, and asyncQueuer.start so it
proves the queued “late” item is processed only after the batch promise settles
and the finally-based restart runs. Assert the intermediate batches/results
state before awaiting batchPromise, or add the item after the queuer is already
running, while preserving the final expected batch and result assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ee971725-03c6-4f5f-a7ef-ceb4f55b4372

📥 Commits

Reviewing files that changed from the base of the PR and between 4ab7a8f and aaf064b.

📒 Files selected for processing (12)
  • docs/framework/preact/reference/functions/useAsyncDebouncedCallback.md
  • docs/framework/preact/reference/functions/useAsyncThrottledCallback.md
  • docs/framework/react/reference/functions/useAsyncDebouncedCallback.md
  • docs/framework/react/reference/functions/useAsyncThrottledCallback.md
  • docs/reference/classes/AsyncQueuer.md
  • docs/reference/functions/asyncQueue.md
  • packages/pacer/src/async-queuer.ts
  • packages/pacer/tests/async-queuer.test.ts
  • packages/preact-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts
  • packages/preact-pacer/src/async-throttler/useAsyncThrottledCallback.ts
  • packages/react-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts
  • packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/preact-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts
  • packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts
  • packages/preact-pacer/src/async-throttler/useAsyncThrottledCallback.ts
  • packages/react-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts
  • docs/reference/functions/asyncQueue.md
  • docs/framework/react/reference/functions/useAsyncThrottledCallback.md
  • docs/reference/classes/AsyncQueuer.md

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 47 out of 47 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/pacer/src/async-queuer.ts:666

  • isExecuting is always set to false when an execution settles, even if other executions are still in-flight (e.g. concurrency > 1 or a manual execute/flush call overlapping). This makes store.state.isExecuting inaccurate for consumers.

undefined is the internal "no item" sentinel used by peekNextItem/getNextItem,
so an enqueued undefined item would wedge the processing loop and block every
item behind it. addItem now rejects undefined through the standard rejection
path (rejectionCount + onReject + return false) in both queuers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 6, 2026 23:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 51 changed files in this pull request and generated no new comments.

Suppressed comments (5)

packages/pacer/tests/async-throttler.test.ts:1028

  • This test enables fake timers via vi.useFakeTimers() in the describe-level beforeEach, but never restores real timers. That can leak fake-timer state into subsequent tests in this file and cause hangs/flakiness. Ensure timers are restored (ideally via try/finally in the test body or an afterEach that calls vi.useRealTimers()).
    packages/preact-pacer/src/async-throttler/useAsyncThrottledCallback.ts:16
  • The JSDoc claims that calls made during the throttle wait window “share the trailing execution's result”. In AsyncThrottler.maybeExecute, each new call resolves the previous pending promise immediately via #resolvePreviousPromiseInternal() (with the most recent lastResult), so only the most recent call can resolve with the trailing execution’s result. The docs should reflect the actual promise resolution behavior.
    docs/framework/react/reference/functions/useAsyncThrottledCallback.md:23
  • This reference doc says calls during the wait period “share the trailing execution's result”, but AsyncThrottler.maybeExecute resolves the previous pending promise immediately on each new call (with the most recent lastResult). Only the most recent call can resolve with the trailing execution’s result, so this description should be updated.
regardless of how many times it is called. Calls made during the wait period can schedule a
single trailing execution with the latest arguments when `trailing` is enabled (the default),
and those calls share the trailing execution's result. The returned function always returns
a promise that resolves or rejects with the result of the original async function, and
resolves with `undefined` when the throttler is disabled.

docs/framework/preact/reference/functions/useAsyncThrottledCallback.md:23

  • This reference doc says calls during the wait period “share the trailing execution's result”, but AsyncThrottler.maybeExecute resolves the previous pending promise immediately on each new call (with the most recent lastResult). Only the most recent call can resolve with the trailing execution’s result, so this description should be updated.
regardless of how many times it is called. Calls made during the wait period can schedule a
single trailing execution with the latest arguments when `trailing` is enabled (the default),
and those calls share the trailing execution's result. The returned function always returns
a promise that resolves or rejects with the result of the original async function, and
resolves with `undefined` when the throttler is disabled.

packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts:16

  • The JSDoc claims that calls made during the throttle wait window “share the trailing execution's result”. In AsyncThrottler.maybeExecute, each new call resolves the previous pending promise immediately via #resolvePreviousPromiseInternal() (with the most recent lastResult), so only the most recent call can resolve with the trailing execution’s result. The docs should reflect the actual promise resolution behavior.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/pacer/src/queuer.ts`:
- Line 393: Update the addItem() return documentation in
packages/pacer/src/queuer.ts near lines 393-393 to state that it returns false
when the queue is full or when the item is undefined; regenerate or apply the
same return-contract update in docs/reference/classes/Queuer.md lines 174-177,
docs/framework/angular/reference/interfaces/QueuedSignal.md lines 41-42, and
docs/reference/functions/queue.md lines 73-74.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e13d8ea1-3350-4228-ac4d-0023a767e512

📥 Commits

Reviewing files that changed from the base of the PR and between aaf064b and 58a9745.

📒 Files selected for processing (10)
  • docs/framework/angular/reference/interfaces/AsyncQueuedSignal.md
  • docs/framework/angular/reference/interfaces/QueuedSignal.md
  • docs/reference/classes/AsyncQueuer.md
  • docs/reference/classes/Queuer.md
  • docs/reference/functions/asyncQueue.md
  • docs/reference/functions/queue.md
  • packages/pacer/src/async-queuer.ts
  • packages/pacer/src/queuer.ts
  • packages/pacer/tests/async-queuer.test.ts
  • packages/pacer/tests/queuer.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/reference/functions/asyncQueue.md
  • packages/pacer/src/async-queuer.ts
  • packages/pacer/tests/queuer.test.ts


/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
* `undefined` cannot be queued (it is the internal "no item" sentinel) and is always rejected.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the addItem() rejection documentation.

The new undefined branch returns false when the queue is not full. Update the source JSDoc and regenerate the affected references so the return contract includes both full-queue rejection and undefined rejection.

  • packages/pacer/src/queuer.ts#L393-L393: update the return description near Line 396.
  • docs/reference/classes/Queuer.md#L174-L177: update the return description near Line 180.
  • docs/framework/angular/reference/interfaces/QueuedSignal.md#L41-L42: update the return description near Line 45.
  • docs/reference/functions/queue.md#L73-L74: update the return description near Line 77.
📍 Affects 4 files
  • packages/pacer/src/queuer.ts#L393-L393 (this comment)
  • docs/reference/classes/Queuer.md#L174-L177
  • docs/framework/angular/reference/interfaces/QueuedSignal.md#L41-L42
  • docs/reference/functions/queue.md#L73-L74
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pacer/src/queuer.ts` at line 393, Update the addItem() return
documentation in packages/pacer/src/queuer.ts near lines 393-393 to state that
it returns false when the queue is full or when the item is undefined;
regenerate or apply the same return-contract update in
docs/reference/classes/Queuer.md lines 174-177,
docs/framework/angular/reference/interfaces/QueuedSignal.md lines 41-42, and
docs/reference/functions/queue.md lines 73-74.

…executions, and throttled callback docs

#tick now copies activeItems before mutating so store state is never dirtied
in place and array-reference selectors observe changes. AsyncQueuer and
AsyncRateLimiter keep isExecuting true until every overlapping execution
settles (derived from the live retryer count) instead of clearing it when the
first one finishes. The flushAsBatch mid-batch test now pins the queue via
pendingTick so it genuinely exercises the finally-restart. Throttled callback
docstrings now describe the actual promise resolution: only the most recent
call gets the trailing result; earlier calls resolve with the previous result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 6, 2026 23:34
@KevinVandy

Copy link
Copy Markdown
Member Author

Addressed the latest CodeRabbit nitpicks and Copilot suppressed comments in edb6b61 (these were review-body comments, not resolvable threads):

Fixed

  • #tick now copies activeItems before mutating (matching execute's pattern), so store state is never dirtied in place and array-reference selectors observe the change.
  • isExecuting now stays true until every overlapping execution settles, derived from the live internal-retryer count — fixes the pre-existing inaccuracy in both AsyncQueuer (concurrency > 1) and AsyncRateLimiter (overlapping window executions), with tests using staggered completions.
  • The flushAsBatch mid-batch test was strengthened, though not with the suggested diff — expect(results).toEqual([]) after start() would fail because the task fn runs synchronously on the start() stack. Instead the test now begins with a running queue mid-wait (pendingTick: true), so the added item cannot be processed by anything except the finally-restart; removing that block now fails the test.
  • The throttled-callback docstrings from the previous round were still wrong (good catch): only the most recent call during the wait period resolves with the trailing execution's result — each superseded call's promise resolves immediately with the previous result. Docstrings and reference docs now describe that.
  • Added the missing afterEach timer restore to the new throttler test describe.

Declined

  • Aligning flushAsBatch's restart with flush's wait-scheduled restart: the items present after a batch settles were added during the batch to an empty queue — processing them immediately matches addItem-to-idle semantics. flush's remaining items sat behind just-executed tasks, which is why the wait spacing applies there.

555 tests passing.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 51 changed files in this pull request and generated no new comments.

Suppressed comments (2)

packages/pacer/src/queuer.ts:436

  • When getPriority is left at its default, addItem bypasses defaultOptions.getPriority (which defaults missing/nullish priorities to 0) and instead reads (item as any)?.priority directly. This changes semantics: items with no priority (or null) end up with priority === undefined, skipping the priority-insertion branch and potentially ordering incorrectly vs negative/zero priorities. Align the fast-path with defaultOptions.getPriority by applying ?? 0 (and same for existingPriority).
    packages/pacer/src/async-queuer.ts:522
  • Like Queuer.addItem, the default getPriority is item?.priority ?? 0, but the fast-path here reads (item as any)?.priority directly when getPriority is unchanged. That means null items (and items with missing priority) get priority === undefined instead of 0, which can skip priority insertion or mis-order items compared to negative/zero priorities. Use ?? 0 to match the documented default behavior (and apply the same change to existingPriority).

@KevinVandy
KevinVandy merged commit dc47121 into main Aug 7, 2026
10 checks passed
@KevinVandy
KevinVandy deleted the fix/async-utils-issue-batch branch August 7, 2026 02:07
@github-actions github-actions Bot mentioned this pull request Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants