Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
58824c2
test(angular-pacer): define adapter rework contracts
benjavicente Oct 8, 2026
98046ef
feat(angular-pacer): rebuild the signal adapters
benjavicente Oct 8, 2026
0a2cc06
docs(examples): migrate Angular examples to the reworked APIs
benjavicente Oct 8, 2026
69084a0
docs(angular): restore adapter imports in the retry guide
benjavicente Oct 8, 2026
eacd23d
refactor(angular-pacer): require exhaustive method decisions
benjavicente Oct 8, 2026
7f90610
fix(angular-pacer): disambiguate callable initial values
benjavicente Oct 9, 2026
bd5bc57
fix(angular-pacer): align limiter cleanup with sibling adapters
benjavicente Oct 9, 2026
b09d2b2
docs(angular): remove breaking changes tracking file
benjavicente Oct 9, 2026
5ac95d7
ci: apply automated fixes
autofix-ci[bot] Oct 9, 2026
c4f9936
fix(angular-pacer): skip initial processing for reflected values
benjavicente Oct 11, 2026
3002709
fix(angular-pacer): follow WritableSignal semantics
benjavicente Oct 11, 2026
35602a5
fix(angular-pacer): bind lazy instance cleanup to owner destruction
benjavicente Oct 11, 2026
250baae
fix(angular-pacer): run processing callbacks inside NgZone
benjavicente Oct 11, 2026
a2a4d0b
feat(angular-pacer): support injected options factories
benjavicente Oct 11, 2026
f97ec76
refactor(angular-pacer): align reflected helper names
benjavicente Oct 11, 2026
600adff
fix(angular-pacer): name public APIs in injection context errors
benjavicente Oct 11, 2026
4ec84d7
ci: apply automated fixes
autofix-ci[bot] Oct 11, 2026
19d5c9e
fix(deps): repair pnpm lockfile snapshots after zone.js addition
KevinVandy Oct 11, 2026
30106bf
Merge branch 'main' into feat-angular-rework-tests
KevinVandy Oct 11, 2026
cd81aa3
Merge upstream/main into feat-angular-rework-tests
benjavicente Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .changeset/angular-adapter-rework.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@tanstack/angular-pacer': minor
---

Rebuild Angular utility adapters with lazy construction, readonly selected
state, explicit methods, reactive options, and owner-bound instance cleanup. Replace the
Angular Store dependency with core Store integration and cover the adapters with
per-utility Angular behavior tests.

Support Angular 20 and up, including all Angular LTS versions, using stable public
signal, effect, and pending-task APIs.

Expose editable helpers as Angular writable signals with paced `set` and `update`
methods and a live `asReadonly()` view. Function-valued replacements remain data,
while updater callbacks run against the committed value when processed.

Dispose lazily created instances on owner destruction even when work starts
before the first effect runs, without creating unused instances during cleanup.

Keep callable signal values type-safe by allowing raw utility writes only as
updaters returning those values; `set` continues to accept function-valued data.

Avoid processing an unchanged initial source value in debounced, throttled, and rate-limited reflected helpers, preserving stability and capacity for real changes.

Keep core scheduling outside NgZone while running the provided processing function inside NgZone, so plain component field updates render in zone-based applications.

Allow `providePacerOptions` to accept a defaults factory evaluated in its injection context.

Rename reflected helpers from `*Computed` to `*Value`, align Queuer Items source filenames with their APIs, and update examples and guides.

Name the public utility in development errors for calls outside an injection
context, with assertions guarded by `ngDevMode` for production removal.
24 changes: 16 additions & 8 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2354,20 +2354,24 @@
"to": "framework/angular/reference/functions/injectQueuer"
},
{
"label": "injectQueuedSignal",
"to": "framework/angular/reference/functions/injectQueuedSignal"
"label": "injectQueuerItems",
"to": "framework/angular/reference/functions/injectQueuerItems"
},
{
"label": "injectQueuedValue",
"to": "framework/angular/reference/functions/injectQueuedValue"
},
{
"label": "injectQueuedSignal",
"to": "framework/angular/reference/functions/injectQueuedSignal"
},
{
"label": "injectAsyncQueuer",
"to": "framework/angular/reference/functions/injectAsyncQueuer"
},
{
"label": "injectAsyncQueuedSignal",
"to": "framework/angular/reference/functions/injectAsyncQueuedSignal"
"label": "injectAsyncQueuerItems",
"to": "framework/angular/reference/functions/injectAsyncQueuerItems"
}
]
},
Expand Down Expand Up @@ -4069,20 +4073,24 @@
"to": "framework/angular/examples/injectQueuer"
},
{
"label": "injectQueuedSignal",
"to": "framework/angular/examples/injectQueuedSignal"
"label": "injectQueuerItems",
"to": "framework/angular/examples/injectQueuerItems"
},
{
"label": "injectQueuedValue",
"to": "framework/angular/examples/injectQueuedValue"
},
{
"label": "injectQueuedSignal",
"to": "framework/angular/examples/injectQueuedSignal"
},
{
"label": "injectAsyncQueuer",
"to": "framework/angular/examples/injectAsyncQueuer"
},
{
"label": "injectAsyncQueuedSignal",
"to": "framework/angular/examples/injectAsyncQueuedSignal"
"label": "injectAsyncQueuerItems",
"to": "framework/angular/examples/injectAsyncQueuerItems"
}
]
},
Expand Down
6 changes: 3 additions & 3 deletions docs/framework/angular/guides/async-batching.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ The adapter cancels the pending wait timer and aborts active work when its owner

## Configuration and reactive state

The adapter subscribes only to the state returned by the selector argument. Without a selector, the adapter state is empty. Create the utility in an Angular injection context, usually as a component or service field initializer and select only fields used by the view:
Pass a selector to expose the state used by your view through `state()`. Without a selector, `state()` returns `{}`:

```ts
const batcher = injectAsyncBatcher(
Expand All @@ -187,9 +187,9 @@ console.log(
)
```

Option functions and lifecycle callbacks receive the underlying public utility instance. The `.store.state` reads inside those callbacks in the examples above are supported. Rendering code should read the selected adapter state shown here.
Option callbacks receive the core utility instance. Read the selected `state()` signal in templates.

`wait` may be a number or a function that receives the batcher instance. `setOptions()` merges new options, and `asyncBatcherOptions()` creates reusable, type-checked option objects.
`wait` may be a number or a function that receives the batcher instance. `asyncBatcherOptions()` creates reusable, type-checked option objects.

Do not use `started` to pause a batcher. It is currently a no-op, so every `addItem()` call evaluates the configured triggers.

Expand Down
16 changes: 9 additions & 7 deletions docs/framework/angular/guides/async-debouncing.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,15 @@ search.reset()

## Configuration

`wait` and `enabled` may be values or functions that receive the debouncer instance. `setOptions()` merges new options into the current configuration.
`wait` and `enabled` may be values or functions that receive the debouncer instance. Use an options function for reactive configuration.

```ts
search.setOptions({
enabled: (debouncer) => debouncer.store.state.errorCount < 3,
wait: (debouncer) => (debouncer.store.state.successCount === 0 ? 200 : 500),
})
import { signal } from '@angular/core'

const wait = signal(500)
const search = injectAsyncDebouncer(searchApi, () => ({ wait: wait() }))

wait.set(250)
```

Changing `wait` does not reschedule an existing timeout. The new value applies when later work is scheduled.
Expand All @@ -142,7 +144,7 @@ The adapter cancels pending work and aborts active work when its owner is destro

## Reactive state

The adapter subscribes only to the state returned by the selector argument. Without a selector, the adapter state is empty. Create the utility in an Angular injection context, usually as a component or service field initializer and select only fields used by the view:
Pass a selector to expose the state used by your view through `state()`. Without a selector, `state()` returns `{}`:

```ts
const debouncer = injectAsyncDebouncer(
Expand All @@ -162,7 +164,7 @@ console.log(
)
```

Option functions and lifecycle callbacks receive the underlying public utility instance. The `.store.state` reads inside those callbacks in the examples above are supported. Rendering code should read the selected adapter state shown here.
Option callbacks receive the core utility instance. Read the selected `state()` signal in templates.

To restore selected state that your app has persisted, pass a partial snapshot through `initialState`. It is merged with the defaults. Restore only durable fields. Pending timers and active executions are not restored.

Expand Down
21 changes: 7 additions & 14 deletions docs/framework/angular/guides/async-queuing.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,19 @@ The queue controls start order. With concurrency greater than `1`, completion or

## Choose an API

- `injectAsyncQueuedSignal` for reactive pending items
- `injectAsyncQueuerItems` for reactive pending items
- `injectAsyncQueuer` for concurrency, ordering, and lifecycle control

## Angular example

```ts
import { injectAsyncQueuedSignal } from '@tanstack/angular-pacer'
import { injectAsyncQueuerItems } from '@tanstack/angular-pacer'

export class UploadComponent {
readonly queued = injectAsyncQueuedSignal(
uploadFile,
{ concurrency: 2 },
(state) => ({
items: state.items,
activeItems: state.activeItems,
}),
)
readonly queued = injectAsyncQueuerItems(uploadFile, { concurrency: 2 })

add() {
this.queued.addItem(nextFile())
this.queued.queuer.addItem(nextFile())
}
}
```
Expand Down Expand Up @@ -184,7 +177,7 @@ The adapter stops automatic processing and aborts active work when its owner is

## Configuration and reactive state

The adapter subscribes only to the state returned by the selector argument. Without a selector, the adapter state is empty. Create the utility in an Angular injection context, usually as a component or service field initializer and select only fields used by the view:
Pass a selector to expose the state used by your view through `state()`. Without a selector, `state()` returns `{}`:

```ts
const queue = injectAsyncQueuer(processJob, { concurrency: 2 }, (state) => ({
Expand All @@ -196,9 +189,9 @@ const queue = injectAsyncQueuer(processJob, { concurrency: 2 }, (state) => ({
console.log(queue.state().size, queue.state().activeItems, queue.state().status)
```

Option functions and lifecycle callbacks receive the underlying public utility instance. The `.store.state` reads inside those callbacks in the examples above are supported. Rendering code should read the selected adapter state shown here.
Option callbacks receive the core utility instance. Read the selected `state()` signal in templates.

`concurrency` and `wait` may be values or functions that receive the queue instance. `setOptions()` merges new options, and `asyncQueuerOptions()` creates reusable, type-checked option objects.
`concurrency` and `wait` may be values or functions that receive the queue instance. `asyncQueuerOptions()` creates reusable, type-checked option objects.

`initialState` can restore selected queue state that your app has persisted. If it includes `items`, they take precedence over `initialItems`; `initialState.isRunning` likewise takes precedence over `started`. Restore only durable fields. Pending timers and active executions are not restored.

Expand Down
16 changes: 9 additions & 7 deletions docs/framework/angular/guides/async-rate-limiting.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,15 @@ Resetting restores capacity immediately. Only do this when starting a genuinely

## Configuration

`enabled`, `limit`, and `window` may be values or functions that receive the limiter instance. `setOptions()` merges new options into the current configuration.
`enabled`, `limit`, and `window` may be values or functions that receive the limiter instance. Use an options function for reactive configuration.

```ts
limiter.setOptions({
enabled: (limiter) => limiter.store.state.errorCount < 3,
limit: (limiter) => (limiter.store.state.rejectionCount > 10 ? 2 : 5),
})
import { signal } from '@angular/core'

const limit = signal(5)
const limiter = injectAsyncRateLimiter(sendRequest, () => ({ limit: limit(), window: 60_000 }))

limit.set(10)
```

Changing `limit`, `window`, or `windowType` does not erase existing execution history. Call `reset()` if a new configuration should begin with a fresh window. A disabled limiter does not execute the function or consume capacity; its calls resolve with `undefined`.
Expand All @@ -151,7 +153,7 @@ The adapter aborts active work when its owner is destroyed. Providing `onUnmount

## Reactive state

The adapter subscribes only to the state returned by the selector argument. Without a selector, the adapter state is empty. Create the utility in an Angular injection context, usually as a component or service field initializer and select only fields used by the view:
Pass a selector to expose the state used by your view through `state()`. Without a selector, `state()` returns `{}`:

```ts
const limiter = injectAsyncRateLimiter(
Expand All @@ -171,7 +173,7 @@ console.log(
)
```

Option functions and lifecycle callbacks receive the underlying public utility instance. The `.store.state` reads inside those callbacks in the examples above are supported. Rendering code should read the selected adapter state shown here.
Option callbacks receive the core utility instance. Read the selected `state()` signal in templates.

To restore selected state that your app has persisted, pass a partial snapshot through `initialState`. It is merged with the defaults. Restore only durable fields. Pending timers and active executions are not restored.

Expand Down
8 changes: 7 additions & 1 deletion docs/framework/angular/guides/async-retrying.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ Retrying runs an async operation again after it fails. It can make transient fai
> [!NOTE]
> `AsyncRetryer` is an alpha API and may change before 1.0. Its current design also supports the retry behavior inside Pacer's other async utilities.

Retrying is the exception among these framework guides: TanStack Pacer does not provide an Angular-specific retry primitive. The adapter re-exports the public `asyncRetry` function and `AsyncRetryer` class, so this guide uses those APIs and connects long-lived instances to the Angular lifecycle.
Retrying is the exception among these framework guides: TanStack Pacer does not provide an Angular-specific retry primitive. Use `asyncRetry` and `AsyncRetryer` re-exported from `@tanstack/angular-pacer` and connect long-lived instances to the Angular lifecycle.

Install the Angular adapter to use these re-exported APIs:

```sh
npm install @tanstack/angular-pacer
```

If TanStack Query already owns the request, use its retry support so one system controls request state and cancellation.

Expand Down
18 changes: 10 additions & 8 deletions docs/framework/angular/guides/async-throttling.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,18 @@ saver.reset()

## Configuration

`wait` and `enabled` may be values or functions that receive the throttler instance. `setOptions()` merges new options into the existing configuration.
`wait` and `enabled` may be values or functions that receive the throttler instance. Use an options function for reactive configuration.

```ts
saver.setOptions({
enabled: (throttler) => throttler.store.state.errorCount < 3,
wait: (throttler) => (throttler.store.state.successCount < 10 ? 500 : 1000),
})
import { signal } from '@angular/core'

const wait = signal(1000)
const saver = injectAsyncThrottler(savePositionToServer, () => ({ wait: wait() }))

wait.set(250)
```

A changed `wait` value does not reschedule existing trailing work. It applies to later scheduling and executions. Disabling the throttler through `setOptions()` cancels pending trailing work.
A changed `wait` value does not reschedule existing trailing work. It applies to later scheduling and executions. Disabling the throttler cancels pending trailing work.

Use `asyncThrottlerOptions()` to define reusable, type-checked option objects.

Expand All @@ -145,7 +147,7 @@ The adapter cancels pending work and aborts active work when its owner is destro

## Reactive state

The adapter subscribes only to the state returned by the selector argument. Without a selector, the adapter state is empty. Create the utility in an Angular injection context, usually as a component or service field initializer and select only fields used by the view:
Pass a selector to expose the state used by your view through `state()`. Without a selector, `state()` returns `{}`:

```ts
const throttler = injectAsyncThrottler(
Expand All @@ -165,7 +167,7 @@ console.log(
)
```

Option functions and lifecycle callbacks receive the underlying public utility instance. The `.store.state` reads inside those callbacks in the examples above are supported. Rendering code should read the selected adapter state shown here.
Option callbacks receive the core utility instance. Read the selected `state()` signal in templates.

To restore selected state that your app has persisted, pass a partial snapshot through `initialState`. It is merged with the defaults. Restore only durable fields. Pending timers and active executions are not restored.

Expand Down
16 changes: 9 additions & 7 deletions docs/framework/angular/guides/batching.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,15 @@ batcher.reset()

## Configuring and observing batches

Use `setOptions()` to update future trigger behavior:
Use an options function to read reactive configuration:

```ts
batcher.setOptions({
maxSize: 20,
wait: 500,
})
import { signal } from '@angular/core'

const options = signal({ maxSize: 10, wait: 100 })
const batcher = injectBatcher(processBatch, options)

options.set({ maxSize: 20, wait: 500 })
```

Changing `wait` does not reschedule an existing timer. The next `addItem()` call replaces that timer using the current value.
Expand Down Expand Up @@ -190,7 +192,7 @@ The adapter cancels the pending wait timer while retaining collected items when

## Reactive state

The adapter subscribes only to the state returned by the selector argument. Without a selector, the adapter state is empty. Create the utility in an Angular injection context, usually as a component or service field initializer and select only fields used by the view:
Pass a selector to expose the state used by your view through `state()`. Without a selector, `state()` returns `{}`:

```ts
const batcher = injectBatcher(
Expand All @@ -205,7 +207,7 @@ const batcher = injectBatcher(
console.log(batcher.state().size, batcher.state().isPending)
```

Option functions and lifecycle callbacks receive the underlying public utility instance. The `.store.state` reads inside those callbacks in the examples above are supported. Rendering code should read the selected adapter state shown here.
Option callbacks receive the core utility instance. Read the selected `state()` signal in templates.

To restore selected state that your app has persisted, pass a partial snapshot through `initialState`. It is merged with the defaults. Restore only durable fields. Pending timers are not restored.

Expand Down
Loading
Loading