diff --git a/.changeset/quick-teams-happen.md b/.changeset/quick-teams-happen.md new file mode 100644 index 000000000..27e26d82b --- /dev/null +++ b/.changeset/quick-teams-happen.md @@ -0,0 +1,14 @@ +--- +'@tanstack/react-pacer': minor +'@tanstack/solid-pacer': minor +'@tanstack/pacer': minor +--- + +- breaking: Removed most "get" methods that can now be read directly from the state (e.g. `debouncer.getExecutionCount()` -> `debouncer.store.state.executionCount` or `debouncer.state.executionCount` in framework adapters) +- breaking: Removed `getOptions` and other option resolver methods such as `getEnabled` and `getWait` +- feat: Rewrote TanStack Pacer to use TanStack Store for state management +- feat: Added `flush` methods to all utils to trigger pending executions to execute immediately. +- feat: Added an `initialState` option to all utils to set the initial state for persistence features +- feat: Added status state to all utils except rate-limiters for pending, excution, etc. states. +- feat: Added new AsyncBatcher utility +- fix: Multiple bug fixes diff --git a/.changeset/sharp-mirrors-clap.md b/.changeset/sharp-mirrors-clap.md new file mode 100644 index 000000000..f9fe10b4a --- /dev/null +++ b/.changeset/sharp-mirrors-clap.md @@ -0,0 +1,6 @@ +--- +'@tanstack/react-persister': minor +'@tanstack/persister': minor +--- + +Created TanStack Persister packages diff --git a/.cursor/rules/commands.mdc b/.cursor/rules/commands.mdc new file mode 100644 index 000000000..8fbc97816 --- /dev/null +++ b/.cursor/rules/commands.mdc @@ -0,0 +1,6 @@ +--- +description: running commands +globs: +alwaysApply: false +--- +- use pnpm instead of npm \ No newline at end of file diff --git a/docs/config.json b/docs/config.json index ead093a21..480682f7b 100644 --- a/docs/config.json +++ b/docs/config.json @@ -81,6 +81,10 @@ { "label": "Batching Guide", "to": "guides/batching" + }, + { + "label": "Async Batching Guide", + "to": "guides/async-batching" } ] }, @@ -119,17 +123,29 @@ "label": "Debouncer API Reference", "children": [ { - "label": "Debouncer Options", + "label": "DebouncerOptions", "to": "reference/interfaces/debounceroptions" }, { - "label": "AsyncDebouncer Options", + "label": "DebouncerState", + "to": "reference/interfaces/debouncerstate" + }, + { + "label": "AsyncDebouncerOptions", "to": "reference/interfaces/asyncdebounceroptions" }, + { + "label": "AsyncDebouncerState", + "to": "reference/interfaces/asyncdebouncerstate" + }, { "label": "debounce", "to": "reference/functions/debounce" }, + { + "label": "asyncDebounce", + "to": "reference/functions/asyncdebounce" + }, { "label": "Debouncer", "to": "reference/classes/debouncer" @@ -143,6 +159,14 @@ { "label": "react", "children": [ + { + "label": "ReactDebouncer", + "to": "framework/react/reference/interfaces/reactdebouncer" + }, + { + "label": "ReactAsyncDebouncer", + "to": "framework/react/reference/interfaces/reactasyncdebouncer" + }, { "label": "useDebouncer", "to": "framework/react/reference/functions/usedebouncer" @@ -162,6 +186,10 @@ { "label": "useAsyncDebouncer", "to": "framework/react/reference/functions/useasyncdebouncer" + }, + { + "label": "useAsyncDebouncedCallback", + "to": "framework/react/reference/functions/useasyncdebouncedcallback" } ] }, @@ -172,12 +200,16 @@ "label": "SolidDebouncer", "to": "framework/solid/reference/interfaces/soliddebouncer" }, + { + "label": "SolidAsyncDebouncer", + "to": "framework/solid/reference/interfaces/solidasyncdebouncer" + }, { "label": "createDebouncer", "to": "framework/solid/reference/functions/createdebouncer" }, { - "label": "createDebouncedState", + "label": "createDebouncedSignal", "to": "framework/solid/reference/functions/createdebouncedsignal" }, { @@ -198,17 +230,29 @@ "label": "Throttler API Reference", "children": [ { - "label": "Throttler Options", + "label": "ThrottlerOptions", "to": "reference/interfaces/throttleroptions" }, { - "label": "AsyncThrottler Options", + "label": "ThrottlerState", + "to": "reference/interfaces/throttlerstate" + }, + { + "label": "AsyncThrottlerOptions", "to": "reference/interfaces/asyncthrottleroptions" }, + { + "label": "AsyncThrottlerState", + "to": "reference/interfaces/asyncthrottlerstate" + }, { "label": "throttle", "to": "reference/functions/throttle" }, + { + "label": "asyncThrottle", + "to": "reference/functions/asyncthrottle" + }, { "label": "Throttler", "to": "reference/classes/throttler" @@ -222,6 +266,14 @@ { "label": "react", "children": [ + { + "label": "ReactThrottler", + "to": "framework/react/reference/interfaces/reactthrottler" + }, + { + "label": "ReactAsyncThrottler", + "to": "framework/react/reference/interfaces/reactasyncthrottler" + }, { "label": "useThrottler", "to": "framework/react/reference/functions/usethrottler" @@ -241,6 +293,10 @@ { "label": "useAsyncThrottler", "to": "framework/react/reference/functions/useasyncthrottler" + }, + { + "label": "useAsyncThrottledCallback", + "to": "framework/react/reference/functions/useasyncthrottledcallback" } ] }, @@ -251,6 +307,10 @@ "label": "SolidThrottler", "to": "framework/solid/reference/interfaces/solidthrottler" }, + { + "label": "SolidAsyncThrottler", + "to": "framework/solid/reference/interfaces/solidasyncthrottler" + }, { "label": "createThrottler", "to": "framework/solid/reference/functions/createthrottler" @@ -280,10 +340,18 @@ "label": "RateLimiterOptions", "to": "reference/interfaces/ratelimiteroptions" }, + { + "label": "RateLimiterState", + "to": "reference/interfaces/ratelimiterstate" + }, { "label": "AsyncRateLimiterOptions", "to": "reference/interfaces/asyncratelimiteroptions" }, + { + "label": "AsyncRateLimiterState", + "to": "reference/interfaces/asyncratelimiterstate" + }, { "label": "rateLimit", "to": "reference/functions/ratelimit" @@ -305,10 +373,22 @@ { "label": "react", "children": [ + { + "label": "ReactRateLimiter", + "to": "framework/react/reference/interfaces/reactratelimiter" + }, + { + "label": "ReactAsyncRateLimiter", + "to": "framework/react/reference/interfaces/reactasyncratelimiter" + }, { "label": "useRateLimiter", "to": "framework/react/reference/functions/useratelimiter" }, + { + "label": "useRateLimitedCallback", + "to": "framework/react/reference/functions/useratelimitedcallback" + }, { "label": "useRateLimitedState", "to": "framework/react/reference/functions/useratelimitedstate" @@ -320,6 +400,10 @@ { "label": "useAsyncRateLimiter", "to": "framework/react/reference/functions/useasyncratelimiter" + }, + { + "label": "useAsyncRateLimitedCallback", + "to": "framework/react/reference/functions/useasyncratelimitedcallback" } ] }, @@ -330,12 +414,16 @@ "label": "SolidRateLimiter", "to": "framework/solid/reference/interfaces/solidratelimiter" }, + { + "label": "SolidAsyncRateLimiter", + "to": "framework/solid/reference/interfaces/solidasyncratelimiter" + }, { "label": "createRateLimiter", "to": "framework/solid/reference/functions/createratelimiter" }, { - "label": "createRateLimitedState", + "label": "createRateLimitedSignal", "to": "framework/solid/reference/functions/createratelimitedsignal" }, { @@ -360,16 +448,16 @@ "to": "reference/interfaces/queueroptions" }, { - "label": "AsyncQueuerOptions", - "to": "reference/interfaces/asyncqueueroptions" + "label": "QueuerState", + "to": "reference/interfaces/queuerstate" }, { - "label": "Queuer", - "to": "reference/classes/queuer" + "label": "AsyncQueuerOptions", + "to": "reference/interfaces/asyncqueueroptions" }, { - "label": "AsyncQueuer", - "to": "reference/classes/asyncqueuer" + "label": "AsyncQueuerState", + "to": "reference/interfaces/asyncqueuerstate" }, { "label": "queue", @@ -378,23 +466,47 @@ { "label": "asyncQueue", "to": "reference/functions/asyncqueue" + }, + { + "label": "Queuer", + "to": "reference/classes/queuer" + }, + { + "label": "AsyncQueuer", + "to": "reference/classes/asyncqueuer" } ], "frameworks": [ { "label": "react", "children": [ + { + "label": "ReactQueuer", + "to": "framework/react/reference/interfaces/reactqueuer" + }, + { + "label": "ReactAsyncQueuer", + "to": "framework/react/reference/interfaces/reactasyncqueuer" + }, { "label": "useQueuer", "to": "framework/react/reference/functions/usequeuer" }, { "label": "useQueuedState", - "to": "framework/react/reference/functions/usequeuerstate" + "to": "framework/react/reference/functions/usequeuedstate" + }, + { + "label": "useQueuedValue", + "to": "framework/react/reference/functions/usequeuedvalue" }, { "label": "useAsyncQueuer", "to": "framework/react/reference/functions/useasyncqueuer" + }, + { + "label": "useAsyncQueuedState", + "to": "framework/react/reference/functions/useasyncqueuedstate" } ] }, @@ -405,6 +517,10 @@ "label": "SolidQueuer", "to": "framework/solid/reference/interfaces/solidqueuer" }, + { + "label": "SolidAsyncQueuer", + "to": "framework/solid/reference/interfaces/solidasyncqueuer" + }, { "label": "createQueuer", "to": "framework/solid/reference/functions/createqueuer" @@ -426,18 +542,54 @@ "label": "BatcherOptions", "to": "reference/interfaces/batcheroptions" }, + { + "label": "BatcherState", + "to": "reference/interfaces/batcherstate" + }, + { + "label": "AsyncBatcherOptions", + "to": "reference/interfaces/asyncbatcheroptions" + }, + { + "label": "AsyncBatcherState", + "to": "reference/interfaces/asyncbatcherstate" + }, + { + "label": "batch", + "to": "reference/functions/batch" + }, + { + "label": "asyncBatch", + "to": "reference/functions/asyncbatch" + }, { "label": "Batcher", "to": "reference/classes/batcher" + }, + { + "label": "AsyncBatcher", + "to": "reference/classes/asyncbatcher" } ], "frameworks": [ { "label": "react", "children": [ + { + "label": "ReactBatcher", + "to": "framework/react/reference/interfaces/reactbatcher" + }, + { + "label": "ReactAsyncBatcher", + "to": "framework/react/reference/interfaces/reactasyncbatcher" + }, { "label": "useBatcher", "to": "framework/react/reference/functions/usebatcher" + }, + { + "label": "useAsyncBatcher", + "to": "framework/react/reference/functions/useasyncbatcher" } ] }, @@ -448,9 +600,17 @@ "label": "SolidBatcher", "to": "framework/solid/reference/interfaces/solidbatcher" }, + { + "label": "SolidAsyncBatcher", + "to": "framework/solid/reference/interfaces/solidasyncbatcher" + }, { "label": "createBatcher", "to": "framework/solid/reference/functions/createbatcher" + }, + { + "label": "createAsyncBatcher", + "to": "framework/solid/reference/functions/createasyncbatcher" } ] } @@ -726,6 +886,10 @@ { "label": "useBatcher", "to": "framework/react/examples/useBatcher" + }, + { + "label": "useAsyncBatcher", + "to": "framework/react/examples/useAsyncBatcher" } ] }, @@ -739,6 +903,10 @@ { "label": "createBatcher", "to": "framework/solid/examples/createBatcher" + }, + { + "label": "createAsyncBatcher", + "to": "framework/solid/examples/createAsyncBatcher" } ] } diff --git a/docs/framework/react/reference/functions/useasyncbatcher.md b/docs/framework/react/reference/functions/useasyncbatcher.md new file mode 100644 index 000000000..b9a2c2f60 --- /dev/null +++ b/docs/framework/react/reference/functions/useasyncbatcher.md @@ -0,0 +1,97 @@ +--- +id: useAsyncBatcher +title: useAsyncBatcher +--- + + + +# Function: useAsyncBatcher() + +```ts +function useAsyncBatcher( + fn, + options, +selector?): ReactAsyncBatcher +``` + +Defined in: [react-pacer/src/async-batcher/useAsyncBatcher.ts:77](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-batcher/useAsyncBatcher.ts#L77) + +A React hook that creates an `AsyncBatcher` instance for managing asynchronous batches of items. + +This is the async version of the useBatcher hook. Unlike the sync version, this async batcher: +- Handles promises and returns results from batch executions +- Provides error handling with configurable error behavior +- Tracks success, error, and settle counts separately +- Has state tracking for when batches are executing +- Returns the result of the batch function execution + +Features: +- Configurable batch size and wait time +- Custom batch processing logic via getShouldExecute +- Event callbacks for monitoring batch operations +- Error handling for failed batch operations +- Automatic or manual batch processing + +The batcher collects items and processes them in batches based on: +- Maximum batch size (number of items per batch) +- Time-based batching (process after X milliseconds) +- Custom batch processing logic via getShouldExecute + +Error Handling: +- If an `onError` handler is provided, it will be called with the error and batcher instance +- If `throwOnError` is true (default when no onError handler is provided), the error will be thrown +- If `throwOnError` is false (default when onError handler is provided), the error will be swallowed +- Both onError and throwOnError can be used together - the handler will be called before any error is thrown +- The error state can be checked using the underlying AsyncBatcher instance + +## Type Parameters + +• **TValue** + +• **TSelected** = `AsyncBatcherState`\<`TValue`\> + +## Parameters + +### fn + +(`items`) => `Promise`\<`any`\> + +### options + +`AsyncBatcherOptions`\<`TValue`\> = `{}` + +### selector? + +(`state`) => `TSelected` + +## Returns + +[`ReactAsyncBatcher`](../../interfaces/reactasyncbatcher.md)\<`TValue`, `TSelected`\> + +## Example + +```tsx +// Basic async batcher for API requests +const asyncBatcher = useAsyncBatcher( + async (items) => { + const results = await Promise.all(items.map(item => processItem(item))); + return results; + }, + { + maxSize: 10, + wait: 2000, + onSuccess: (result) => { + console.log('Batch processed successfully:', result); + }, + onError: (error) => { + console.error('Batch processing failed:', error); + } + } +); + +// Add items to batch +asyncBatcher.addItem(newItem); + +// Manually execute batch +const result = await asyncBatcher.execute(); +``` diff --git a/docs/framework/react/reference/functions/useasyncdebouncedcallback.md b/docs/framework/react/reference/functions/useasyncdebouncedcallback.md new file mode 100644 index 000000000..4d088995a --- /dev/null +++ b/docs/framework/react/reference/functions/useasyncdebouncedcallback.md @@ -0,0 +1,88 @@ +--- +id: useAsyncDebouncedCallback +title: useAsyncDebouncedCallback +--- + + + +# Function: useAsyncDebouncedCallback() + +```ts +function useAsyncDebouncedCallback( + fn, + options, +selector): (...args) => Promise> +``` + +Defined in: [react-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts:46](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts#L46) + +A React hook that creates a debounced version of an async callback function. +This hook is a convenient wrapper around the `useAsyncDebouncer` hook, +providing a stable, debounced async function reference for use in React components. + +The debounced async function will only execute after the specified wait time has elapsed +since its last invocation. If called again before the wait time expires, the timer +resets and starts waiting again. The returned function always returns a promise +that resolves or rejects with the result of the original async function. + +This hook provides a simpler API compared to `useAsyncDebouncer`, making it ideal for basic +async debouncing needs. However, it does not expose the underlying AsyncDebouncer instance. + +For advanced usage requiring features like: +- Manual cancellation +- Access to execution/error state +- Custom useCallback dependencies + +Consider using the `useAsyncDebouncer` hook instead. + +## Type Parameters + +• **TFn** *extends* `AnyAsyncFunction` + +• **TSelected** = \{\} + +## Parameters + +### fn + +`TFn` + +### options + +`AsyncDebouncerOptions`\<`TFn`\> + +### selector + +(`state`) => `TSelected` + +## Returns + +`Function` + +### Parameters + +#### args + +...`Parameters`\<`TFn`\> + +### Returns + +`Promise`\<`ReturnType`\<`TFn`\>\> + +## Example + +```tsx +// Debounce an async search handler +const handleSearch = useAsyncDebouncedCallback(async (query: string) => { + const results = await fetchSearchResults(query); + return results; +}, { + wait: 500 // Wait 500ms between executions +}); + +// Use in an input + handleSearch(e.target.value)} +/> +``` diff --git a/docs/framework/react/reference/functions/useasyncdebouncer.md b/docs/framework/react/reference/functions/useasyncdebouncer.md index d3f709478..c44086e68 100644 --- a/docs/framework/react/reference/functions/useasyncdebouncer.md +++ b/docs/framework/react/reference/functions/useasyncdebouncer.md @@ -8,10 +8,13 @@ title: useAsyncDebouncer # Function: useAsyncDebouncer() ```ts -function useAsyncDebouncer(fn, options): AsyncDebouncer +function useAsyncDebouncer( + fn, + options, +selector?): ReactAsyncDebouncer ``` -Defined in: [react-pacer/src/async-debouncer/useAsyncDebouncer.ts:60](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts#L60) +Defined in: [react-pacer/src/async-debouncer/useAsyncDebouncer.ts:75](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts#L75) A low-level React hook that creates an `AsyncDebouncer` instance to delay execution of an async function. @@ -40,6 +43,8 @@ Error Handling: • **TFn** *extends* `AnyAsyncFunction` +• **TSelected** = `AsyncDebouncerState`\<`TFn`\> + ## Parameters ### fn @@ -50,9 +55,13 @@ Error Handling: `AsyncDebouncerOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -`AsyncDebouncer`\<`TFn`\> +[`ReactAsyncDebouncer`](../../interfaces/reactasyncdebouncer.md)\<`TFn`, `TSelected`\> ## Example diff --git a/docs/framework/react/reference/functions/useasyncqueuedstate.md b/docs/framework/react/reference/functions/useasyncqueuedstate.md index c159eef39..ebcbca9c0 100644 --- a/docs/framework/react/reference/functions/useasyncqueuedstate.md +++ b/docs/framework/react/reference/functions/useasyncqueuedstate.md @@ -8,7 +8,10 @@ title: useAsyncQueuedState # Function: useAsyncQueuedState() ```ts -function useAsyncQueuedState(fn, options): [TValue[], AsyncQueuer] +function useAsyncQueuedState( + fn, + options, + selector?): [TValue[], ReactAsyncQueuer] ``` Defined in: [react-pacer/src/async-queuer/useAsyncQueuedState.ts:53](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-queuer/useAsyncQueuedState.ts#L53) @@ -36,6 +39,8 @@ The state will automatically update whenever items are: • **TValue** +• **TSelected** *extends* `Pick`\<`AsyncQueuerState`\<`TValue`\>, `"items"`\> = `AsyncQueuerState`\<`TValue`\> + ## Parameters ### fn @@ -46,9 +51,13 @@ The state will automatically update whenever items are: `AsyncQueuerOptions`\<`TValue`\> = `{}` +### selector? + +(`state`) => `TSelected` + ## Returns -\[`TValue`[], `AsyncQueuer`\<`TValue`\>\] +\[`TValue`[], [`ReactAsyncQueuer`](../../interfaces/reactasyncqueuer.md)\<`TValue`, `TSelected`\>\] ## Example diff --git a/docs/framework/react/reference/functions/useasyncqueuer.md b/docs/framework/react/reference/functions/useasyncqueuer.md index 18e5cea0c..b42084af0 100644 --- a/docs/framework/react/reference/functions/useasyncqueuer.md +++ b/docs/framework/react/reference/functions/useasyncqueuer.md @@ -8,10 +8,13 @@ title: useAsyncQueuer # Function: useAsyncQueuer() ```ts -function useAsyncQueuer(fn, options): AsyncQueuer +function useAsyncQueuer( + fn, + options, +selector?): ReactAsyncQueuer ``` -Defined in: [react-pacer/src/async-queuer/useAsyncQueuer.ts:51](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-queuer/useAsyncQueuer.ts#L51) +Defined in: [react-pacer/src/async-queuer/useAsyncQueuer.ts:64](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-queuer/useAsyncQueuer.ts#L64) A lower-level React hook that creates an `AsyncQueuer` instance for managing an async queue of items. @@ -38,6 +41,8 @@ Error Handling: • **TValue** +• **TSelected** = `AsyncQueuerState`\<`TValue`\> + ## Parameters ### fn @@ -48,9 +53,13 @@ Error Handling: `AsyncQueuerOptions`\<`TValue`\> = `{}` +### selector? + +(`state`) => `TSelected` + ## Returns -`AsyncQueuer`\<`TValue`\> +[`ReactAsyncQueuer`](../../interfaces/reactasyncqueuer.md)\<`TValue`, `TSelected`\> ## Example diff --git a/docs/framework/react/reference/functions/useasyncratelimitedcallback.md b/docs/framework/react/reference/functions/useasyncratelimitedcallback.md new file mode 100644 index 000000000..bb44b1031 --- /dev/null +++ b/docs/framework/react/reference/functions/useasyncratelimitedcallback.md @@ -0,0 +1,103 @@ +--- +id: useAsyncRateLimitedCallback +title: useAsyncRateLimitedCallback +--- + + + +# Function: useAsyncRateLimitedCallback() + +```ts +function useAsyncRateLimitedCallback( + fn, + options, +selector): (...args) => Promise> +``` + +Defined in: [react-pacer/src/async-rate-limiter/useAsyncRateLimitedCallback.ts:61](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimitedCallback.ts#L61) + +A React hook that creates a rate-limited version of an async callback function. +This hook is a convenient wrapper around the `useAsyncRateLimiter` hook, +providing a stable, async rate-limited function reference for use in React components. + +Async rate limiting is a "hard limit" approach for async functions: it allows all calls +until the limit is reached, then blocks (rejects) subsequent calls until the window resets. +Unlike throttling or debouncing, it does not attempt to space out or collapse calls. +This can lead to bursts of rapid executions followed by periods where all calls are blocked. + +The async rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All executions within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows executions as old ones expire. This provides a more + consistent rate of execution over time. + +For smoother execution patterns, consider: +- useAsyncThrottledCallback: When you want consistent spacing between executions (e.g. UI updates) +- useAsyncDebouncedCallback: When you want to collapse rapid calls into a single execution (e.g. search input) + +Async rate limiting should primarily be used when you need to enforce strict limits +on async operations, like API rate limits or other scenarios requiring hard caps +on execution frequency. + +This hook provides a simpler API compared to `useAsyncRateLimiter`, making it ideal for basic +async rate limiting needs. However, it does not expose the underlying AsyncRateLimiter instance. + +For advanced usage requiring features like: +- Manual cancellation +- Access to execution counts +- Custom useCallback dependencies + +Consider using the `useAsyncRateLimiter` hook instead. + +## Type Parameters + +• **TFn** *extends* `AnyAsyncFunction` + +• **TSelected** = \{\} + +## Parameters + +### fn + +`TFn` + +### options + +`AsyncRateLimiterOptions`\<`TFn`\> + +### selector + +(`state`) => `TSelected` + +## Returns + +`Function` + +### Parameters + +#### args + +...`Parameters`\<`TFn`\> + +### Returns + +`Promise`\<`ReturnType`\<`TFn`\>\> + +## Example + +```tsx +// Rate limit async API calls to maximum 5 calls per minute with a sliding window +const makeApiCall = useAsyncRateLimitedCallback( + async (data: ApiData) => { + return fetch('/api/endpoint', { method: 'POST', body: JSON.stringify(data) }); + }, + { + limit: 5, + window: 60000, // 1 minute + windowType: 'sliding', + onReject: () => { + console.warn('API rate limit reached. Please wait before trying again.'); + } + } +); +``` diff --git a/docs/framework/react/reference/functions/useasyncratelimiter.md b/docs/framework/react/reference/functions/useasyncratelimiter.md index fdbfeef74..62c5b75ca 100644 --- a/docs/framework/react/reference/functions/useasyncratelimiter.md +++ b/docs/framework/react/reference/functions/useasyncratelimiter.md @@ -8,10 +8,13 @@ title: useAsyncRateLimiter # Function: useAsyncRateLimiter() ```ts -function useAsyncRateLimiter(fn, options): AsyncRateLimiter +function useAsyncRateLimiter( + fn, + options, +selector?): ReactAsyncRateLimiter ``` -Defined in: [react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts:67](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts#L67) +Defined in: [react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts:82](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts#L82) A low-level React hook that creates an `AsyncRateLimiter` instance to limit how many times an async function can execute within a time window. @@ -44,6 +47,8 @@ Error Handling: • **TFn** *extends* `AnyAsyncFunction` +• **TSelected** = `AsyncRateLimiterState`\<`TFn`\> + ## Parameters ### fn @@ -54,9 +59,13 @@ Error Handling: `AsyncRateLimiterOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -`AsyncRateLimiter`\<`TFn`\> +[`ReactAsyncRateLimiter`](../../interfaces/reactasyncratelimiter.md)\<`TFn`, `TSelected`\> ## Example diff --git a/docs/framework/react/reference/functions/useasyncthrottledcallback.md b/docs/framework/react/reference/functions/useasyncthrottledcallback.md new file mode 100644 index 000000000..f3b908fb2 --- /dev/null +++ b/docs/framework/react/reference/functions/useasyncthrottledcallback.md @@ -0,0 +1,86 @@ +--- +id: useAsyncThrottledCallback +title: useAsyncThrottledCallback +--- + + + +# Function: useAsyncThrottledCallback() + +```ts +function useAsyncThrottledCallback( + fn, + options, +selector): (...args) => Promise> +``` + +Defined in: [react-pacer/src/async-throttler/useAsyncThrottledCallback.ts:44](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts#L44) + +A React hook that creates a throttled version of an async callback function. +This hook is a convenient wrapper around the `useAsyncThrottler` hook, +providing a stable, throttled async function reference for use in React components. + +The throttled async function will execute at most once within the specified wait time period, +regardless of how many times it is called. If called multiple times during the wait period, +only the first invocation will execute, and subsequent calls will be ignored until +the wait period has elapsed. The returned function always returns a promise +that resolves or rejects with the result of the original async function. + +This hook provides a simpler API compared to `useAsyncThrottler`, making it ideal for basic +async throttling needs. However, it does not expose the underlying AsyncThrottler instance. + +For advanced usage requiring features like: +- Manual cancellation +- Access to execution/error state +- Custom useCallback dependencies + +Consider using the `useAsyncThrottler` hook instead. + +## Type Parameters + +• **TFn** *extends* `AnyAsyncFunction` + +• **TSelected** = \{\} + +## Parameters + +### fn + +`TFn` + +### options + +`AsyncThrottlerOptions`\<`TFn`\> + +### selector + +(`state`) => `TSelected` + +## Returns + +`Function` + +### Parameters + +#### args + +...`Parameters`\<`TFn`\> + +### Returns + +`Promise`\<`ReturnType`\<`TFn`\>\> + +## Example + +```tsx +// Throttle an async API call +const handleApiCall = useAsyncThrottledCallback(async (data) => { + const result = await sendDataToServer(data); + return result; +}, { + wait: 200 // Execute at most once every 200ms +}); + +// Use in an event handler + +``` diff --git a/docs/framework/react/reference/functions/useasyncthrottler.md b/docs/framework/react/reference/functions/useasyncthrottler.md index 11e17fc7c..9338610db 100644 --- a/docs/framework/react/reference/functions/useasyncthrottler.md +++ b/docs/framework/react/reference/functions/useasyncthrottler.md @@ -8,10 +8,13 @@ title: useAsyncThrottler # Function: useAsyncThrottler() ```ts -function useAsyncThrottler(fn, options): AsyncThrottler +function useAsyncThrottler( + fn, + options, +selector?): ReactAsyncThrottler ``` -Defined in: [react-pacer/src/async-throttler/useAsyncThrottler.ts:55](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts#L55) +Defined in: [react-pacer/src/async-throttler/useAsyncThrottler.ts:70](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts#L70) A low-level React hook that creates an `AsyncThrottler` instance to limit how often an async function can execute. @@ -37,6 +40,8 @@ Error Handling: • **TFn** *extends* `AnyAsyncFunction` +• **TSelected** = `AsyncThrottlerState`\<`TFn`\> + ## Parameters ### fn @@ -47,9 +52,13 @@ Error Handling: `AsyncThrottlerOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -`AsyncThrottler`\<`TFn`\> +[`ReactAsyncThrottler`](../../interfaces/reactasyncthrottler.md)\<`TFn`, `TSelected`\> ## Example diff --git a/docs/framework/react/reference/functions/usebatcher.md b/docs/framework/react/reference/functions/usebatcher.md index e17ad2508..c7cf7e33a 100644 --- a/docs/framework/react/reference/functions/usebatcher.md +++ b/docs/framework/react/reference/functions/usebatcher.md @@ -8,10 +8,13 @@ title: useBatcher # Function: useBatcher() ```ts -function useBatcher(fn, options): Batcher +function useBatcher( + fn, + options, +selector?): ReactBatcher ``` -Defined in: [react-pacer/src/batcher/useBatcher.ts:43](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/batcher/useBatcher.ts#L43) +Defined in: [react-pacer/src/batcher/useBatcher.ts:53](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/batcher/useBatcher.ts#L53) A React hook that creates and manages a Batcher instance. @@ -28,6 +31,8 @@ The Batcher collects items and processes them in batches based on configurable c • **TValue** +• **TSelected** = `BatcherState`\<`TValue`\> + ## Parameters ### fn @@ -38,9 +43,13 @@ The Batcher collects items and processes them in batches based on configurable c `BatcherOptions`\<`TValue`\> = `{}` +### selector? + +(`state`) => `TSelected` + ## Returns -`Batcher`\<`TValue`\> +[`ReactBatcher`](../../interfaces/reactbatcher.md)\<`TValue`, `TSelected`\> ## Example diff --git a/docs/framework/react/reference/functions/usedebouncedcallback.md b/docs/framework/react/reference/functions/usedebouncedcallback.md index d9da81160..c87338365 100644 --- a/docs/framework/react/reference/functions/usedebouncedcallback.md +++ b/docs/framework/react/reference/functions/usedebouncedcallback.md @@ -8,10 +8,13 @@ title: useDebouncedCallback # Function: useDebouncedCallback() ```ts -function useDebouncedCallback(fn, options): (...args) => void +function useDebouncedCallback( + fn, + options, + selector): (...args) => void ``` -Defined in: [react-pacer/src/debouncer/useDebouncedCallback.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedCallback.ts#L42) +Defined in: [react-pacer/src/debouncer/useDebouncedCallback.ts:45](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedCallback.ts#L45) A React hook that creates a debounced version of a callback function. This hook is essentially a wrapper around the basic `debounce` function @@ -36,6 +39,8 @@ Consider using the `useDebouncer` hook instead. • **TFn** *extends* `AnyFunction` +• **TSelected** = \{\} + ## Parameters ### fn @@ -46,6 +51,10 @@ Consider using the `useDebouncer` hook instead. `DebouncerOptions`\<`TFn`\> +### selector + +(`state`) => `TSelected` + ## Returns `Function` diff --git a/docs/framework/react/reference/functions/usedebouncedstate.md b/docs/framework/react/reference/functions/usedebouncedstate.md index c2be1d8d6..b662beaa9 100644 --- a/docs/framework/react/reference/functions/usedebouncedstate.md +++ b/docs/framework/react/reference/functions/usedebouncedstate.md @@ -8,10 +8,13 @@ title: useDebouncedState # Function: useDebouncedState() ```ts -function useDebouncedState(value, options): [TValue, Dispatch>, Debouncer>>] +function useDebouncedState( + value, + options, + selector?): [TValue, Dispatch>, ReactDebouncer>, TSelected>] ``` -Defined in: [react-pacer/src/debouncer/useDebouncedState.ts:38](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedState.ts#L38) +Defined in: [react-pacer/src/debouncer/useDebouncedState.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedState.ts#L42) A React hook that creates a debounced state value, combining React's useState with debouncing functionality. This hook provides both the current debounced value and methods to update it. @@ -30,6 +33,8 @@ The hook returns a tuple containing: • **TValue** +• **TSelected** = `DebouncerState`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> + ## Parameters ### value @@ -40,9 +45,13 @@ The hook returns a tuple containing: `DebouncerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, `Debouncer`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] +\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, [`ReactDebouncer`](../../interfaces/reactdebouncer.md)\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, `TSelected`\>\] ## Example @@ -61,5 +70,5 @@ const handleChange = (e) => { const executionCount = debouncer.getExecutionCount(); // Get the pending state -const isPending = debouncer.getIsPending(); +const isPending = debouncer.getState().isPending; ``` diff --git a/docs/framework/react/reference/functions/usedebouncedvalue.md b/docs/framework/react/reference/functions/usedebouncedvalue.md index 6c7871ba2..07825a9ba 100644 --- a/docs/framework/react/reference/functions/usedebouncedvalue.md +++ b/docs/framework/react/reference/functions/usedebouncedvalue.md @@ -8,10 +8,13 @@ title: useDebouncedValue # Function: useDebouncedValue() ```ts -function useDebouncedValue(value, options): [TValue, Debouncer>>] +function useDebouncedValue( + value, + options, + selector?): [TValue, ReactDebouncer>, TSelected>] ``` -Defined in: [react-pacer/src/debouncer/useDebouncedValue.ts:41](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedValue.ts#L41) +Defined in: [react-pacer/src/debouncer/useDebouncedValue.ts:45](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedValue.ts#L45) A React hook that creates a debounced value that updates only after a specified delay. Unlike useDebouncedState, this hook automatically tracks changes to the input value @@ -33,6 +36,8 @@ and execution counts. • **TValue** +• **TSelected** = `DebouncerState`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> + ## Parameters ### value @@ -43,9 +48,13 @@ and execution counts. `DebouncerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`TValue`, `Debouncer`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] +\[`TValue`, [`ReactDebouncer`](../../interfaces/reactdebouncer.md)\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, `TSelected`\>\] ## Example diff --git a/docs/framework/react/reference/functions/usedebouncer.md b/docs/framework/react/reference/functions/usedebouncer.md index 22c162551..b1585af21 100644 --- a/docs/framework/react/reference/functions/usedebouncer.md +++ b/docs/framework/react/reference/functions/usedebouncer.md @@ -8,10 +8,13 @@ title: useDebouncer # Function: useDebouncer() ```ts -function useDebouncer(fn, options): Debouncer +function useDebouncer( + fn, + options, +selector?): ReactDebouncer ``` -Defined in: [react-pacer/src/debouncer/useDebouncer.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncer.ts#L42) +Defined in: [react-pacer/src/debouncer/useDebouncer.ts:57](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncer.ts#L57) A React hook that creates and manages a Debouncer instance. @@ -31,6 +34,8 @@ timer resets and starts waiting again. • **TFn** *extends* `AnyFunction` +• **TSelected** = `DebouncerState`\<`TFn`\> + ## Parameters ### fn @@ -41,9 +46,13 @@ timer resets and starts waiting again. `DebouncerOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -`Debouncer`\<`TFn`\> +[`ReactDebouncer`](../../interfaces/reactdebouncer.md)\<`TFn`, `TSelected`\> ## Example @@ -63,5 +72,5 @@ const handleChange = (e) => { const executionCount = searchDebouncer.getExecutionCount(); // Get the pending state -const isPending = searchDebouncer.getIsPending(); +const isPending = searchdebouncer.getState().isPending; ``` diff --git a/docs/framework/react/reference/functions/usequeuedstate.md b/docs/framework/react/reference/functions/usequeuedstate.md index e27b54337..e21595471 100644 --- a/docs/framework/react/reference/functions/usequeuedstate.md +++ b/docs/framework/react/reference/functions/usequeuedstate.md @@ -8,7 +8,10 @@ title: useQueuedState # Function: useQueuedState() ```ts -function useQueuedState(fn, options): [TValue[], (item, position?, runOnUpdate?) => boolean, Queuer] +function useQueuedState( + fn, + options, + selector?): [TValue[], (item, position?, runOnItemsChange?) => boolean, ReactQueuer] ``` Defined in: [react-pacer/src/queuer/useQueuedState.ts:54](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuedState.ts#L54) @@ -31,6 +34,8 @@ The hook returns a tuple containing: • **TValue** +• **TSelected** *extends* `Pick`\<`QueuerState`\<`TValue`\>, `"items"`\> = `QueuerState`\<`TValue`\> + ## Parameters ### fn @@ -41,9 +46,13 @@ The hook returns a tuple containing: `QueuerOptions`\<`TValue`\> = `{}` +### selector? + +(`state`) => `TSelected` + ## Returns -\[`TValue`[], (`item`, `position`?, `runOnUpdate`?) => `boolean`, `Queuer`\<`TValue`\>\] +\[`TValue`[], (`item`, `position`?, `runOnItemsChange`?) => `boolean`, [`ReactQueuer`](../../interfaces/reactqueuer.md)\<`TValue`, `TSelected`\>\] ## Example diff --git a/docs/framework/react/reference/functions/usequeuedvalue.md b/docs/framework/react/reference/functions/usequeuedvalue.md index b2b84c35c..6a69e4018 100644 --- a/docs/framework/react/reference/functions/usequeuedvalue.md +++ b/docs/framework/react/reference/functions/usequeuedvalue.md @@ -8,10 +8,13 @@ title: useQueuedValue # Function: useQueuedValue() ```ts -function useQueuedValue(initialValue, options): [TValue, Queuer] +function useQueuedValue( + initialValue, + options, + selector?): [TValue, ReactQueuer] ``` -Defined in: [react-pacer/src/queuer/useQueuedValue.ts:40](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuedValue.ts#L40) +Defined in: [react-pacer/src/queuer/useQueuedValue.ts:41](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuedValue.ts#L41) A React hook that creates a queued value that processes state changes in order with an optional delay. This hook uses useQueuer internally to manage a queue of state changes and apply them sequentially. @@ -28,6 +31,8 @@ The hook returns a tuple containing: • **TValue** +• **TSelected** *extends* `Pick`\<`QueuerState`\<`TValue`\>, `"items"`\> = `QueuerState`\<`TValue`\> + ## Parameters ### initialValue @@ -38,9 +43,13 @@ The hook returns a tuple containing: `QueuerOptions`\<`TValue`\> = `{}` +### selector? + +(`state`) => `TSelected` + ## Returns -\[`TValue`, `Queuer`\<`TValue`\>\] +\[`TValue`, [`ReactQueuer`](../../interfaces/reactqueuer.md)\<`TValue`, `TSelected`\>\] ## Example diff --git a/docs/framework/react/reference/functions/usequeuer.md b/docs/framework/react/reference/functions/usequeuer.md index b4a03ca32..2ce82d6e2 100644 --- a/docs/framework/react/reference/functions/usequeuer.md +++ b/docs/framework/react/reference/functions/usequeuer.md @@ -8,10 +8,13 @@ title: useQueuer # Function: useQueuer() ```ts -function useQueuer(fn, options): Queuer +function useQueuer( + fn, + options, +selector?): ReactQueuer ``` -Defined in: [react-pacer/src/queuer/useQueuer.ts:44](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuer.ts#L44) +Defined in: [react-pacer/src/queuer/useQueuer.ts:54](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuer.ts#L54) A React hook that creates and manages a Queuer instance. @@ -33,6 +36,8 @@ By default uses FIFO (First In First Out) behavior, but can be configured for LI • **TValue** +• **TSelected** = `QueuerState`\<`TValue`\> + ## Parameters ### fn @@ -43,9 +48,13 @@ By default uses FIFO (First In First Out) behavior, but can be configured for LI `QueuerOptions`\<`TValue`\> = `{}` +### selector? + +(`state`) => `TSelected` + ## Returns -`Queuer`\<`TValue`\> +[`ReactQueuer`](../../interfaces/reactqueuer.md)\<`TValue`, `TSelected`\> ## Example diff --git a/docs/framework/react/reference/functions/useratelimitedcallback.md b/docs/framework/react/reference/functions/useratelimitedcallback.md index 7b1287a3f..405669bce 100644 --- a/docs/framework/react/reference/functions/useratelimitedcallback.md +++ b/docs/framework/react/reference/functions/useratelimitedcallback.md @@ -8,10 +8,13 @@ title: useRateLimitedCallback # Function: useRateLimitedCallback() ```ts -function useRateLimitedCallback(fn, options): (...args) => boolean +function useRateLimitedCallback( + fn, + options, + selector): (...args) => boolean ``` -Defined in: [react-pacer/src/rate-limiter/useRateLimitedCallback.ts:59](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts#L59) +Defined in: [react-pacer/src/rate-limiter/useRateLimitedCallback.ts:62](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts#L62) A React hook that creates a rate-limited version of a callback function. This hook is essentially a wrapper around the basic `rateLimiter` function @@ -51,7 +54,7 @@ Consider using the `useRateLimiter` hook instead. • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `any`[] +• **TSelected** = \{\} ## Parameters @@ -63,6 +66,10 @@ Consider using the `useRateLimiter` hook instead. `RateLimiterOptions`\<`TFn`\> +### selector + +(`state`) => `TSelected` + ## Returns `Function` @@ -71,7 +78,7 @@ Consider using the `useRateLimiter` hook instead. #### args -...`TArgs` +...`Parameters`\<`TFn`\> ### Returns diff --git a/docs/framework/react/reference/functions/useratelimitedstate.md b/docs/framework/react/reference/functions/useratelimitedstate.md index 655e09aa6..9b044ec2d 100644 --- a/docs/framework/react/reference/functions/useratelimitedstate.md +++ b/docs/framework/react/reference/functions/useratelimitedstate.md @@ -8,10 +8,13 @@ title: useRateLimitedState # Function: useRateLimitedState() ```ts -function useRateLimitedState(value, options): [TValue, Dispatch>, RateLimiter>>] +function useRateLimitedState( + value, + options, + selector?): [TValue, Dispatch>, ReactRateLimiter>, TSelected>] ``` -Defined in: [react-pacer/src/rate-limiter/useRateLimitedState.ts:66](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts#L66) +Defined in: [react-pacer/src/rate-limiter/useRateLimitedState.ts:67](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts#L67) A React hook that creates a rate-limited state value that enforces a hard limit on state updates within a time window. This hook combines React's useState with rate limiting functionality to provide controlled state updates. @@ -44,6 +47,8 @@ consider using the lower-level useRateLimiter hook instead. • **TValue** +• **TSelected** = `RateLimiterState` + ## Parameters ### value @@ -54,9 +59,13 @@ consider using the lower-level useRateLimiter hook instead. `RateLimiterOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, `RateLimiter`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] +\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, [`ReactRateLimiter`](../../interfaces/reactratelimiter.md)\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, `TSelected`\>\] ## Example diff --git a/docs/framework/react/reference/functions/useratelimitedvalue.md b/docs/framework/react/reference/functions/useratelimitedvalue.md index 2194cd3f1..78f9dcc3b 100644 --- a/docs/framework/react/reference/functions/useratelimitedvalue.md +++ b/docs/framework/react/reference/functions/useratelimitedvalue.md @@ -8,10 +8,13 @@ title: useRateLimitedValue # Function: useRateLimitedValue() ```ts -function useRateLimitedValue(value, options): [TValue, RateLimiter>>] +function useRateLimitedValue( + value, + options, + selector?): [TValue, ReactRateLimiter>, TSelected>] ``` -Defined in: [react-pacer/src/rate-limiter/useRateLimitedValue.ts:55](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts#L55) +Defined in: [react-pacer/src/rate-limiter/useRateLimitedValue.ts:56](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts#L56) A high-level React hook that creates a rate-limited version of a value that updates at most a certain number of times within a time window. This hook uses React's useState internally to manage the rate-limited state. @@ -43,6 +46,8 @@ consider using the lower-level useRateLimiter hook instead. • **TValue** +• **TSelected** = `RateLimiterState` + ## Parameters ### value @@ -53,9 +58,13 @@ consider using the lower-level useRateLimiter hook instead. `RateLimiterOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`TValue`, `RateLimiter`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] +\[`TValue`, [`ReactRateLimiter`](../../interfaces/reactratelimiter.md)\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, `TSelected`\>\] ## Example diff --git a/docs/framework/react/reference/functions/useratelimiter.md b/docs/framework/react/reference/functions/useratelimiter.md index 1ca948af9..eb9d0080a 100644 --- a/docs/framework/react/reference/functions/useratelimiter.md +++ b/docs/framework/react/reference/functions/useratelimiter.md @@ -8,10 +8,13 @@ title: useRateLimiter # Function: useRateLimiter() ```ts -function useRateLimiter(fn, options): RateLimiter +function useRateLimiter( + fn, + options, +selector?): ReactRateLimiter ``` -Defined in: [react-pacer/src/rate-limiter/useRateLimiter.ts:55](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimiter.ts#L55) +Defined in: [react-pacer/src/rate-limiter/useRateLimiter.ts:70](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimiter.ts#L70) A low-level React hook that creates a `RateLimiter` instance to enforce rate limits on function execution. @@ -44,6 +47,8 @@ The hook returns an object containing: • **TFn** *extends* `AnyFunction` +• **TSelected** = `RateLimiterState` + ## Parameters ### fn @@ -54,9 +59,13 @@ The hook returns an object containing: `RateLimiterOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -`RateLimiter`\<`TFn`\> +[`ReactRateLimiter`](../../interfaces/reactratelimiter.md)\<`TFn`, `TSelected`\> ## Example diff --git a/docs/framework/react/reference/functions/usethrottledcallback.md b/docs/framework/react/reference/functions/usethrottledcallback.md index 429ede263..17871e5ca 100644 --- a/docs/framework/react/reference/functions/usethrottledcallback.md +++ b/docs/framework/react/reference/functions/usethrottledcallback.md @@ -8,10 +8,13 @@ title: useThrottledCallback # Function: useThrottledCallback() ```ts -function useThrottledCallback(fn, options): (...args) => void +function useThrottledCallback( + fn, + options, + selector): (...args) => void ``` -Defined in: [react-pacer/src/throttler/useThrottledCallback.ts:43](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledCallback.ts#L43) +Defined in: [react-pacer/src/throttler/useThrottledCallback.ts:46](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledCallback.ts#L46) A React hook that creates a throttled version of a callback function. This hook is essentially a wrapper around the basic `throttle` function @@ -37,7 +40,7 @@ Consider using the `useThrottler` hook instead. • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `any`[] +• **TSelected** = \{\} ## Parameters @@ -49,6 +52,10 @@ Consider using the `useThrottler` hook instead. `ThrottlerOptions`\<`TFn`\> +### selector + +(`state`) => `TSelected` + ## Returns `Function` @@ -57,7 +64,7 @@ Consider using the `useThrottler` hook instead. #### args -...`TArgs` +...`Parameters`\<`TFn`\> ### Returns diff --git a/docs/framework/react/reference/functions/usethrottledstate.md b/docs/framework/react/reference/functions/usethrottledstate.md index 1e4f7e845..5e30ccc07 100644 --- a/docs/framework/react/reference/functions/usethrottledstate.md +++ b/docs/framework/react/reference/functions/usethrottledstate.md @@ -8,10 +8,13 @@ title: useThrottledState # Function: useThrottledState() ```ts -function useThrottledState(value, options): [TValue, Dispatch>, Throttler>>] +function useThrottledState( + value, + options, + selector?): [TValue, Dispatch>, ReactThrottler>, TSelected>] ``` -Defined in: [react-pacer/src/throttler/useThrottledState.ts:40](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledState.ts#L40) +Defined in: [react-pacer/src/throttler/useThrottledState.ts:44](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledState.ts#L44) A React hook that creates a throttled state value that updates at most once within a specified time window. This hook combines React's useState with throttling functionality to provide controlled state updates. @@ -31,6 +34,8 @@ consider using the lower-level useThrottler hook instead. • **TValue** +• **TSelected** = `ThrottlerState`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> + ## Parameters ### value @@ -41,9 +46,13 @@ consider using the lower-level useThrottler hook instead. `ThrottlerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, `Throttler`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] +\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, [`ReactThrottler`](../../interfaces/reactthrottler.md)\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, `TSelected`\>\] ## Example diff --git a/docs/framework/react/reference/functions/usethrottledvalue.md b/docs/framework/react/reference/functions/usethrottledvalue.md index db3d90c3a..614dbf5be 100644 --- a/docs/framework/react/reference/functions/usethrottledvalue.md +++ b/docs/framework/react/reference/functions/usethrottledvalue.md @@ -8,10 +8,13 @@ title: useThrottledValue # Function: useThrottledValue() ```ts -function useThrottledValue(value, options): [TValue, Throttler>>] +function useThrottledValue( + value, + options, + selector?): [TValue, ReactThrottler>, TSelected>] ``` -Defined in: [react-pacer/src/throttler/useThrottledValue.ts:32](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledValue.ts#L32) +Defined in: [react-pacer/src/throttler/useThrottledValue.ts:36](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledValue.ts#L36) A high-level React hook that creates a throttled version of a value that updates at most once within a specified time window. This hook uses React's useState internally to manage the throttled state. @@ -30,6 +33,8 @@ consider using the lower-level useThrottler hook instead. • **TValue** +• **TSelected** = `ThrottlerState`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> + ## Parameters ### value @@ -40,9 +45,13 @@ consider using the lower-level useThrottler hook instead. `ThrottlerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`TValue`, `Throttler`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] +\[`TValue`, [`ReactThrottler`](../../interfaces/reactthrottler.md)\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, `TSelected`\>\] ## Example diff --git a/docs/framework/react/reference/functions/usethrottler.md b/docs/framework/react/reference/functions/usethrottler.md index 24ba9e71e..03622e13a 100644 --- a/docs/framework/react/reference/functions/usethrottler.md +++ b/docs/framework/react/reference/functions/usethrottler.md @@ -8,10 +8,13 @@ title: useThrottler # Function: useThrottler() ```ts -function useThrottler(fn, options): Throttler +function useThrottler( + fn, + options, +selector?): ReactThrottler ``` -Defined in: [react-pacer/src/throttler/useThrottler.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottler.ts#L42) +Defined in: [react-pacer/src/throttler/useThrottler.ts:50](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottler.ts#L50) A low-level React hook that creates a `Throttler` instance that limits how often the provided function can execute. @@ -27,6 +30,8 @@ expensive operations or UI updates. • **TFn** *extends* `AnyFunction` +• **TSelected** = `ThrottlerState`\<`TFn`\> + ## Parameters ### fn @@ -37,9 +42,13 @@ expensive operations or UI updates. `ThrottlerOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -`Throttler`\<`TFn`\> +[`ReactThrottler`](../../interfaces/reactthrottler.md)\<`TFn`, `TSelected`\> ## Example @@ -48,13 +57,6 @@ expensive operations or UI updates. const [value, setValue] = useState(0); const throttler = useThrottler(setValue, { wait: 1000 }); -// With Redux -const dispatch = useDispatch(); -const throttler = useThrottler( - (value) => dispatch(updateAction(value)), - { wait: 1000 } -); - // With any state manager const throttler = useThrottler( (value) => stateManager.setState(value), diff --git a/docs/framework/react/reference/index.md b/docs/framework/react/reference/index.md index b1c1e5192..cf797a6d7 100644 --- a/docs/framework/react/reference/index.md +++ b/docs/framework/react/reference/index.md @@ -7,12 +7,29 @@ title: "@tanstack/react-pacer" # @tanstack/react-pacer +## Interfaces + +- [ReactAsyncBatcher](../interfaces/reactasyncbatcher.md) +- [ReactAsyncDebouncer](../interfaces/reactasyncdebouncer.md) +- [ReactAsyncQueuer](../interfaces/reactasyncqueuer.md) +- [ReactAsyncRateLimiter](../interfaces/reactasyncratelimiter.md) +- [ReactAsyncThrottler](../interfaces/reactasyncthrottler.md) +- [ReactBatcher](../interfaces/reactbatcher.md) +- [ReactDebouncer](../interfaces/reactdebouncer.md) +- [ReactQueuer](../interfaces/reactqueuer.md) +- [ReactRateLimiter](../interfaces/reactratelimiter.md) +- [ReactThrottler](../interfaces/reactthrottler.md) + ## Functions +- [useAsyncBatcher](../functions/useasyncbatcher.md) +- [useAsyncDebouncedCallback](../functions/useasyncdebouncedcallback.md) - [useAsyncDebouncer](../functions/useasyncdebouncer.md) - [useAsyncQueuedState](../functions/useasyncqueuedstate.md) - [useAsyncQueuer](../functions/useasyncqueuer.md) +- [useAsyncRateLimitedCallback](../functions/useasyncratelimitedcallback.md) - [useAsyncRateLimiter](../functions/useasyncratelimiter.md) +- [useAsyncThrottledCallback](../functions/useasyncthrottledcallback.md) - [useAsyncThrottler](../functions/useasyncthrottler.md) - [useBatcher](../functions/usebatcher.md) - [useDebouncedCallback](../functions/usedebouncedcallback.md) diff --git a/docs/framework/react/reference/interfaces/reactasyncbatcher.md b/docs/framework/react/reference/interfaces/reactasyncbatcher.md new file mode 100644 index 000000000..368964972 --- /dev/null +++ b/docs/framework/react/reference/interfaces/reactasyncbatcher.md @@ -0,0 +1,34 @@ +--- +id: ReactAsyncBatcher +title: ReactAsyncBatcher +--- + + + +# Interface: ReactAsyncBatcher\ + +Defined in: [react-pacer/src/async-batcher/useAsyncBatcher.ts:9](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-batcher/useAsyncBatcher.ts#L9) + +## Extends + +- `Omit`\<`AsyncBatcher`\<`TValue`\>, `"store"`\> + +## Type Parameters + +• **TValue** + +• **TSelected** = `AsyncBatcherState`\<`TValue`\> + +## Properties + +### state + +```ts +readonly state: Readonly; +``` + +Defined in: [react-pacer/src/async-batcher/useAsyncBatcher.ts:18](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-batcher/useAsyncBatcher.ts#L18) + +Reactive state that will be updated and re-rendered when the batcher state changes + +Use this instead of `batcher.store.state` diff --git a/docs/framework/react/reference/interfaces/reactasyncdebouncer.md b/docs/framework/react/reference/interfaces/reactasyncdebouncer.md new file mode 100644 index 000000000..22e8589d1 --- /dev/null +++ b/docs/framework/react/reference/interfaces/reactasyncdebouncer.md @@ -0,0 +1,34 @@ +--- +id: ReactAsyncDebouncer +title: ReactAsyncDebouncer +--- + + + +# Interface: ReactAsyncDebouncer\ + +Defined in: [react-pacer/src/async-debouncer/useAsyncDebouncer.ts:10](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts#L10) + +## Extends + +- `Omit`\<`AsyncDebouncer`\<`TFn`\>, `"store"`\> + +## Type Parameters + +• **TFn** *extends* `AnyAsyncFunction` + +• **TSelected** = `AsyncDebouncerState`\<`TFn`\> + +## Properties + +### state + +```ts +readonly state: Readonly; +``` + +Defined in: [react-pacer/src/async-debouncer/useAsyncDebouncer.ts:19](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts#L19) + +Reactive state that will be updated and re-rendered when the debouncer state changes + +Use this instead of `debouncer.store.state` diff --git a/docs/framework/react/reference/interfaces/reactasyncqueuer.md b/docs/framework/react/reference/interfaces/reactasyncqueuer.md new file mode 100644 index 000000000..10de0ab7a --- /dev/null +++ b/docs/framework/react/reference/interfaces/reactasyncqueuer.md @@ -0,0 +1,34 @@ +--- +id: ReactAsyncQueuer +title: ReactAsyncQueuer +--- + + + +# Interface: ReactAsyncQueuer\ + +Defined in: [react-pacer/src/async-queuer/useAsyncQueuer.ts:9](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-queuer/useAsyncQueuer.ts#L9) + +## Extends + +- `Omit`\<`AsyncQueuer`\<`TValue`\>, `"store"`\> + +## Type Parameters + +• **TValue** + +• **TSelected** = `AsyncQueuerState`\<`TValue`\> + +## Properties + +### state + +```ts +readonly state: Readonly; +``` + +Defined in: [react-pacer/src/async-queuer/useAsyncQueuer.ts:16](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-queuer/useAsyncQueuer.ts#L16) + +Reactive state that will be updated and re-rendered when the queuer state changes + +Use this instead of `queuer.store.state` diff --git a/docs/framework/react/reference/interfaces/reactasyncratelimiter.md b/docs/framework/react/reference/interfaces/reactasyncratelimiter.md new file mode 100644 index 000000000..bf8c03b9a --- /dev/null +++ b/docs/framework/react/reference/interfaces/reactasyncratelimiter.md @@ -0,0 +1,34 @@ +--- +id: ReactAsyncRateLimiter +title: ReactAsyncRateLimiter +--- + + + +# Interface: ReactAsyncRateLimiter\ + +Defined in: [react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts:10](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts#L10) + +## Extends + +- `Omit`\<`AsyncRateLimiter`\<`TFn`\>, `"store"`\> + +## Type Parameters + +• **TFn** *extends* `AnyAsyncFunction` + +• **TSelected** = `AsyncRateLimiterState`\<`TFn`\> + +## Properties + +### state + +```ts +readonly state: Readonly; +``` + +Defined in: [react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts:19](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts#L19) + +Reactive state that will be updated and re-rendered when the rate limiter state changes + +Use this instead of `rateLimiter.store.state` diff --git a/docs/framework/react/reference/interfaces/reactasyncthrottler.md b/docs/framework/react/reference/interfaces/reactasyncthrottler.md new file mode 100644 index 000000000..ebc903f54 --- /dev/null +++ b/docs/framework/react/reference/interfaces/reactasyncthrottler.md @@ -0,0 +1,34 @@ +--- +id: ReactAsyncThrottler +title: ReactAsyncThrottler +--- + + + +# Interface: ReactAsyncThrottler\ + +Defined in: [react-pacer/src/async-throttler/useAsyncThrottler.ts:10](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts#L10) + +## Extends + +- `Omit`\<`AsyncThrottler`\<`TFn`\>, `"store"`\> + +## Type Parameters + +• **TFn** *extends* `AnyAsyncFunction` + +• **TSelected** = `AsyncThrottlerState`\<`TFn`\> + +## Properties + +### state + +```ts +readonly state: Readonly; +``` + +Defined in: [react-pacer/src/async-throttler/useAsyncThrottler.ts:19](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts#L19) + +Reactive state that will be updated and re-rendered when the debouncer state changes + +Use this instead of `debouncer.store.state` diff --git a/docs/framework/react/reference/interfaces/reactbatcher.md b/docs/framework/react/reference/interfaces/reactbatcher.md new file mode 100644 index 000000000..8d62d55d5 --- /dev/null +++ b/docs/framework/react/reference/interfaces/reactbatcher.md @@ -0,0 +1,34 @@ +--- +id: ReactBatcher +title: ReactBatcher +--- + + + +# Interface: ReactBatcher\ + +Defined in: [react-pacer/src/batcher/useBatcher.ts:6](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/batcher/useBatcher.ts#L6) + +## Extends + +- `Omit`\<`Batcher`\<`TValue`\>, `"store"`\> + +## Type Parameters + +• **TValue** + +• **TSelected** = `BatcherState`\<`TValue`\> + +## Properties + +### state + +```ts +readonly state: Readonly; +``` + +Defined in: [react-pacer/src/batcher/useBatcher.ts:13](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/batcher/useBatcher.ts#L13) + +Reactive state that will be updated and re-rendered when the batcher state changes + +Use this instead of `batcher.store.state` diff --git a/docs/framework/react/reference/interfaces/reactdebouncer.md b/docs/framework/react/reference/interfaces/reactdebouncer.md new file mode 100644 index 000000000..4a620a17a --- /dev/null +++ b/docs/framework/react/reference/interfaces/reactdebouncer.md @@ -0,0 +1,34 @@ +--- +id: ReactDebouncer +title: ReactDebouncer +--- + + + +# Interface: ReactDebouncer\ + +Defined in: [react-pacer/src/debouncer/useDebouncer.ts:10](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncer.ts#L10) + +## Extends + +- `Omit`\<`Debouncer`\<`TFn`\>, `"store"`\> + +## Type Parameters + +• **TFn** *extends* `AnyFunction` + +• **TSelected** = `DebouncerState`\<`TFn`\> + +## Properties + +### state + +```ts +readonly state: Readonly; +``` + +Defined in: [react-pacer/src/debouncer/useDebouncer.ts:19](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncer.ts#L19) + +Reactive state that will be updated and re-rendered when the debouncer state changes + +Use this instead of `debouncer.store.state` diff --git a/docs/framework/react/reference/interfaces/reactqueuer.md b/docs/framework/react/reference/interfaces/reactqueuer.md new file mode 100644 index 000000000..c5dede91e --- /dev/null +++ b/docs/framework/react/reference/interfaces/reactqueuer.md @@ -0,0 +1,34 @@ +--- +id: ReactQueuer +title: ReactQueuer +--- + + + +# Interface: ReactQueuer\ + +Defined in: [react-pacer/src/queuer/useQueuer.ts:6](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuer.ts#L6) + +## Extends + +- `Omit`\<`Queuer`\<`TValue`\>, `"store"`\> + +## Type Parameters + +• **TValue** + +• **TSelected** = `QueuerState`\<`TValue`\> + +## Properties + +### state + +```ts +readonly state: Readonly; +``` + +Defined in: [react-pacer/src/queuer/useQueuer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuer.ts#L13) + +Reactive state that will be updated and re-rendered when the queuer state changes + +Use this instead of `queuer.store.state` diff --git a/docs/framework/react/reference/interfaces/reactratelimiter.md b/docs/framework/react/reference/interfaces/reactratelimiter.md new file mode 100644 index 000000000..a670cadec --- /dev/null +++ b/docs/framework/react/reference/interfaces/reactratelimiter.md @@ -0,0 +1,34 @@ +--- +id: ReactRateLimiter +title: ReactRateLimiter +--- + + + +# Interface: ReactRateLimiter\ + +Defined in: [react-pacer/src/rate-limiter/useRateLimiter.ts:10](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimiter.ts#L10) + +## Extends + +- `Omit`\<`RateLimiter`\<`TFn`\>, `"store"`\> + +## Type Parameters + +• **TFn** *extends* `AnyFunction` + +• **TSelected** = `RateLimiterState` + +## Properties + +### state + +```ts +readonly state: Readonly; +``` + +Defined in: [react-pacer/src/rate-limiter/useRateLimiter.ts:19](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimiter.ts#L19) + +Reactive state that will be updated and re-rendered when the rate limiter state changes + +Use this instead of `rateLimiter.store.state` diff --git a/docs/framework/react/reference/interfaces/reactthrottler.md b/docs/framework/react/reference/interfaces/reactthrottler.md new file mode 100644 index 000000000..2d69cf164 --- /dev/null +++ b/docs/framework/react/reference/interfaces/reactthrottler.md @@ -0,0 +1,34 @@ +--- +id: ReactThrottler +title: ReactThrottler +--- + + + +# Interface: ReactThrottler\ + +Defined in: [react-pacer/src/throttler/useThrottler.ts:10](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottler.ts#L10) + +## Extends + +- `Omit`\<`Throttler`\<`TFn`\>, `"store"`\> + +## Type Parameters + +• **TFn** *extends* `AnyFunction` + +• **TSelected** = `ThrottlerState`\<`TFn`\> + +## Properties + +### state + +```ts +readonly state: Readonly; +``` + +Defined in: [react-pacer/src/throttler/useThrottler.ts:19](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottler.ts#L19) + +Reactive state that will be updated and re-rendered when the throttler state changes + +Use this instead of `throttler.store.state` diff --git a/docs/framework/solid/reference/functions/createasyncbatcher.md b/docs/framework/solid/reference/functions/createasyncbatcher.md new file mode 100644 index 000000000..847b4ea73 --- /dev/null +++ b/docs/framework/solid/reference/functions/createasyncbatcher.md @@ -0,0 +1,101 @@ +--- +id: createAsyncBatcher +title: createAsyncBatcher +--- + + + +# Function: createAsyncBatcher() + +```ts +function createAsyncBatcher( + fn, + initialOptions, +selector?): SolidAsyncBatcher +``` + +Defined in: [async-batcher/createAsyncBatcher.ts:82](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-batcher/createAsyncBatcher.ts#L82) + +Creates a Solid-compatible AsyncBatcher instance for managing asynchronous batches of items, exposing Solid signals for all stateful properties. + +This is the async version of the createBatcher hook. Unlike the sync version, this async batcher: +- Handles promises and returns results from batch executions +- Provides error handling with configurable error behavior +- Tracks success, error, and settle counts separately +- Has state tracking for when batches are executing +- Returns the result of the batch function execution + +Features: +- Configurable batch size and wait time +- Custom batch processing logic via getShouldExecute +- Event callbacks for monitoring batch operations +- Error handling for failed batch operations +- Automatic or manual batch processing +- All stateful properties (items, counts, etc.) are exposed as Solid signals for reactivity + +The batcher collects items and processes them in batches based on: +- Maximum batch size (number of items per batch) +- Time-based batching (process after X milliseconds) +- Custom batch processing logic via getShouldExecute + +Error Handling: +- If an `onError` handler is provided, it will be called with the error and batcher instance +- If `throwOnError` is true (default when no onError handler is provided), the error will be thrown +- If `throwOnError` is false (default when onError handler is provided), the error will be swallowed +- Both onError and throwOnError can be used together; the handler will be called before any error is thrown +- The error state can be checked using the underlying AsyncBatcher instance + +Example usage: +```tsx +// Basic async batcher for API requests +const asyncBatcher = createAsyncBatcher( + async (items) => { + const results = await Promise.all(items.map(item => processItem(item))); + return results; + }, + { + maxSize: 10, + wait: 2000, + onSuccess: (result) => { + console.log('Batch processed successfully:', result); + }, + onError: (error) => { + console.error('Batch processing failed:', error); + } + } +); + +// Add items to batch +asyncBatcher.addItem(newItem); + +// Manually execute batch +const result = await asyncBatcher.execute(); + +// Use Solid signals in your UI +const items = asyncBatcher.state().items; +const isExecuting = asyncBatcher.state().isExecuting; +``` + +## Type Parameters + +• **TValue** + +• **TSelected** = `AsyncBatcherState`\<`TValue`\> + +## Parameters + +### fn + +(`items`) => `Promise`\<`any`\> + +### initialOptions + +`AsyncBatcherOptions`\<`TValue`\> = `{}` + +### selector? + +(`state`) => `TSelected` + +## Returns + +[`SolidAsyncBatcher`](../../interfaces/solidasyncbatcher.md)\<`TValue`, `TSelected`\> diff --git a/docs/framework/solid/reference/functions/createasyncdebouncer.md b/docs/framework/solid/reference/functions/createasyncdebouncer.md index bcc35c571..4bf8a2972 100644 --- a/docs/framework/solid/reference/functions/createasyncdebouncer.md +++ b/docs/framework/solid/reference/functions/createasyncdebouncer.md @@ -8,10 +8,13 @@ title: createAsyncDebouncer # Function: createAsyncDebouncer() ```ts -function createAsyncDebouncer(fn, initialOptions): SolidAsyncDebouncer +function createAsyncDebouncer( + fn, + initialOptions, +selector?): SolidAsyncDebouncer ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:77](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L77) +Defined in: [async-debouncer/createAsyncDebouncer.ts:76](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L76) A low-level Solid hook that creates an `AsyncDebouncer` instance to delay execution of an async function. @@ -40,6 +43,8 @@ Error Handling: • **TFn** *extends* `AnyAsyncFunction` +• **TSelected** = `AsyncDebouncerState`\<`TFn`\> + ## Parameters ### fn @@ -50,9 +55,13 @@ Error Handling: `AsyncDebouncerOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -[`SolidAsyncDebouncer`](../../interfaces/solidasyncdebouncer.md)\<`TFn`\> +[`SolidAsyncDebouncer`](../../interfaces/solidasyncdebouncer.md)\<`TFn`, `TSelected`\> ## Example diff --git a/docs/framework/solid/reference/functions/createasyncqueuer.md b/docs/framework/solid/reference/functions/createasyncqueuer.md index 024061aa3..c4c3a05c3 100644 --- a/docs/framework/solid/reference/functions/createasyncqueuer.md +++ b/docs/framework/solid/reference/functions/createasyncqueuer.md @@ -8,10 +8,13 @@ title: createAsyncQueuer # Function: createAsyncQueuer() ```ts -function createAsyncQueuer(fn, initialOptions): SolidAsyncQueuer +function createAsyncQueuer( + fn, + initialOptions, +selector?): SolidAsyncQueuer ``` -Defined in: [async-queuer/createAsyncQueuer.ts:130](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L130) +Defined in: [async-queuer/createAsyncQueuer.ts:71](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L71) Creates a Solid-compatible AsyncQueuer instance for managing an asynchronous queue of items, exposing Solid signals for all stateful properties. @@ -68,6 +71,8 @@ const pending = asyncQueuer.pendingItems(); • **TValue** +• **TSelected** = `AsyncQueuerState`\<`TValue`\> + ## Parameters ### fn @@ -78,6 +83,10 @@ const pending = asyncQueuer.pendingItems(); `AsyncQueuerOptions`\<`TValue`\> = `{}` +### selector? + +(`state`) => `TSelected` + ## Returns -[`SolidAsyncQueuer`](../../interfaces/solidasyncqueuer.md)\<`TValue`\> +[`SolidAsyncQueuer`](../../interfaces/solidasyncqueuer.md)\<`TValue`, `TSelected`\> diff --git a/docs/framework/solid/reference/functions/createasyncratelimiter.md b/docs/framework/solid/reference/functions/createasyncratelimiter.md index 2b31347b7..0c5fb12f9 100644 --- a/docs/framework/solid/reference/functions/createasyncratelimiter.md +++ b/docs/framework/solid/reference/functions/createasyncratelimiter.md @@ -8,10 +8,13 @@ title: createAsyncRateLimiter # Function: createAsyncRateLimiter() ```ts -function createAsyncRateLimiter(fn, initialOptions): SolidAsyncRateLimiter +function createAsyncRateLimiter( + fn, + initialOptions, +selector?): SolidAsyncRateLimiter ``` -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:88](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L88) +Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:79](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L79) A low-level Solid hook that creates an `AsyncRateLimiter` instance to limit how many times an async function can execute within a time window. @@ -51,6 +54,8 @@ Error Handling: • **TFn** *extends* `AnyAsyncFunction` +• **TSelected** = `AsyncRateLimiterState`\<`TFn`\> + ## Parameters ### fn @@ -61,9 +66,13 @@ Error Handling: `AsyncRateLimiterOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -[`SolidAsyncRateLimiter`](../../interfaces/solidasyncratelimiter.md)\<`TFn`\> +[`SolidAsyncRateLimiter`](../../interfaces/solidasyncratelimiter.md)\<`TFn`, `TSelected`\> ## Example diff --git a/docs/framework/solid/reference/functions/createasyncthrottler.md b/docs/framework/solid/reference/functions/createasyncthrottler.md index e56536867..57e54807d 100644 --- a/docs/framework/solid/reference/functions/createasyncthrottler.md +++ b/docs/framework/solid/reference/functions/createasyncthrottler.md @@ -8,10 +8,13 @@ title: createAsyncThrottler # Function: createAsyncThrottler() ```ts -function createAsyncThrottler(fn, initialOptions): SolidAsyncThrottler +function createAsyncThrottler( + fn, + initialOptions, +selector?): SolidAsyncThrottler ``` -Defined in: [async-throttler/createAsyncThrottler.ts:80](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L80) +Defined in: [async-throttler/createAsyncThrottler.ts:72](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L72) A low-level Solid hook that creates an `AsyncThrottler` instance to limit how often an async function can execute. @@ -37,6 +40,8 @@ Error Handling: • **TFn** *extends* `AnyAsyncFunction` +• **TSelected** = `AsyncThrottlerState`\<`TFn`\> + ## Parameters ### fn @@ -47,9 +52,13 @@ Error Handling: `AsyncThrottlerOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -[`SolidAsyncThrottler`](../../interfaces/solidasyncthrottler.md)\<`TFn`\> +[`SolidAsyncThrottler`](../../interfaces/solidasyncthrottler.md)\<`TFn`, `TSelected`\> ## Example diff --git a/docs/framework/solid/reference/functions/createbatcher.md b/docs/framework/solid/reference/functions/createbatcher.md index 87c46ca5c..ca51efc8b 100644 --- a/docs/framework/solid/reference/functions/createbatcher.md +++ b/docs/framework/solid/reference/functions/createbatcher.md @@ -8,10 +8,13 @@ title: createBatcher # Function: createBatcher() ```ts -function createBatcher(fn, initialOptions): SolidBatcher +function createBatcher( + fn, + initialOptions, +selector?): SolidBatcher ``` -Defined in: [batcher/createBatcher.ts:90](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L90) +Defined in: [batcher/createBatcher.ts:63](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L63) Creates a Solid-compatible Batcher instance for managing batches of items, exposing Solid signals for all stateful properties. @@ -55,14 +58,16 @@ console.log('Items:', batcher.allItems()); console.log('Size:', batcher.size()); console.log('Is empty:', batcher.isEmpty()); console.log('Is running:', batcher.isRunning()); -console.log('Batch count:', batcher.batchExecutionCount()); -console.log('Item count:', batcher.itemExecutionCount()); +console.log('Batch count:', batcher.executionCount()); +console.log('Item count:', batcher.totalItemsProcessed()); ``` ## Type Parameters • **TValue** +• **TSelected** = `BatcherState`\<`TValue`\> + ## Parameters ### fn @@ -73,6 +78,10 @@ console.log('Item count:', batcher.itemExecutionCount()); `BatcherOptions`\<`TValue`\> = `{}` +### selector? + +(`state`) => `TSelected` + ## Returns -[`SolidBatcher`](../../interfaces/solidbatcher.md)\<`TValue`\> +[`SolidBatcher`](../../interfaces/solidbatcher.md)\<`TValue`, `TSelected`\> diff --git a/docs/framework/solid/reference/functions/createdebouncedsignal.md b/docs/framework/solid/reference/functions/createdebouncedsignal.md index 3f090f46b..5e9b55c27 100644 --- a/docs/framework/solid/reference/functions/createdebouncedsignal.md +++ b/docs/framework/solid/reference/functions/createdebouncedsignal.md @@ -8,10 +8,13 @@ title: createDebouncedSignal # Function: createDebouncedSignal() ```ts -function createDebouncedSignal(value, initialOptions): [Accessor, Setter, SolidDebouncer>] +function createDebouncedSignal( + value, + initialOptions, + selector?): [Accessor, Setter, SolidDebouncer, TSelected>] ``` -Defined in: [debouncer/createDebouncedSignal.ts:46](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts#L46) +Defined in: [debouncer/createDebouncedSignal.ts:49](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts#L49) A Solid hook that creates a debounced state value, combining Solid's createSignal with debouncing functionality. This hook provides both the current debounced value and methods to update it. @@ -30,6 +33,8 @@ The hook returns a tuple containing: • **TValue** +• **TSelected** = `DebouncerState`\<`Setter`\<`TValue`\>\> + ## Parameters ### value @@ -40,9 +45,13 @@ The hook returns a tuple containing: `DebouncerOptions`\<`Setter`\<`TValue`\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidDebouncer`](../../interfaces/soliddebouncer.md)\<`Setter`\<`TValue`\>\>\] +\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidDebouncer`](../../interfaces/soliddebouncer.md)\<`Setter`\<`TValue`\>, `TSelected`\>\] ## Example diff --git a/docs/framework/solid/reference/functions/createdebouncedvalue.md b/docs/framework/solid/reference/functions/createdebouncedvalue.md index 3316e6caa..910863176 100644 --- a/docs/framework/solid/reference/functions/createdebouncedvalue.md +++ b/docs/framework/solid/reference/functions/createdebouncedvalue.md @@ -8,10 +8,13 @@ title: createDebouncedValue # Function: createDebouncedValue() ```ts -function createDebouncedValue(value, initialOptions): [Accessor, SolidDebouncer>] +function createDebouncedValue( + value, + initialOptions, + selector?): [Accessor, SolidDebouncer, TSelected>] ``` -Defined in: [debouncer/createDebouncedValue.ts:41](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncedValue.ts#L41) +Defined in: [debouncer/createDebouncedValue.ts:44](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncedValue.ts#L44) A Solid hook that creates a debounced value that updates only after a specified delay. Unlike createDebouncedSignal, this hook automatically tracks changes to the input value @@ -33,6 +36,8 @@ The hook returns a tuple containing: • **TValue** +• **TSelected** = `DebouncerState`\<`Setter`\<`TValue`\>\> + ## Parameters ### value @@ -43,9 +48,13 @@ The hook returns a tuple containing: `DebouncerOptions`\<`Setter`\<`TValue`\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`Accessor`\<`TValue`\>, [`SolidDebouncer`](../../interfaces/soliddebouncer.md)\<`Setter`\<`TValue`\>\>\] +\[`Accessor`\<`TValue`\>, [`SolidDebouncer`](../../interfaces/soliddebouncer.md)\<`Setter`\<`TValue`\>, `TSelected`\>\] ## Example diff --git a/docs/framework/solid/reference/functions/createdebouncer.md b/docs/framework/solid/reference/functions/createdebouncer.md index 0c2c3b497..9404d6adc 100644 --- a/docs/framework/solid/reference/functions/createdebouncer.md +++ b/docs/framework/solid/reference/functions/createdebouncer.md @@ -8,10 +8,13 @@ title: createDebouncer # Function: createDebouncer() ```ts -function createDebouncer(fn, initialOptions): SolidDebouncer +function createDebouncer( + fn, + initialOptions, +selector?): SolidDebouncer ``` -Defined in: [debouncer/createDebouncer.ts:53](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L53) +Defined in: [debouncer/createDebouncer.ts:59](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L59) A Solid hook that creates and manages a Debouncer instance. @@ -31,6 +34,8 @@ timer resets and starts waiting again. • **TFn** *extends* `AnyFunction` +• **TSelected** = `DebouncerState`\<`TFn`\> + ## Parameters ### fn @@ -41,9 +46,13 @@ timer resets and starts waiting again. `DebouncerOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -[`SolidDebouncer`](../../interfaces/soliddebouncer.md)\<`TFn`\> +[`SolidDebouncer`](../../interfaces/soliddebouncer.md)\<`TFn`, `TSelected`\> ## Example diff --git a/docs/framework/solid/reference/functions/createqueuer.md b/docs/framework/solid/reference/functions/createqueuer.md index 886859c2c..7c3ea7645 100644 --- a/docs/framework/solid/reference/functions/createqueuer.md +++ b/docs/framework/solid/reference/functions/createqueuer.md @@ -8,10 +8,13 @@ title: createQueuer # Function: createQueuer() ```ts -function createQueuer(fn, initialOptions): SolidQueuer +function createQueuer( + fn, + initialOptions, +selector?): SolidQueuer ``` -Defined in: [queuer/createQueuer.ts:108](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L108) +Defined in: [queuer/createQueuer.ts:67](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L67) Creates a Solid-compatible Queuer instance for managing a synchronous queue of items, exposing Solid signals for all stateful properties. @@ -67,6 +70,8 @@ console.log('Next item:', queue.nextItem()); • **TValue** +• **TSelected** = `QueuerState`\<`TValue`\> + ## Parameters ### fn @@ -77,6 +82,10 @@ console.log('Next item:', queue.nextItem()); `QueuerOptions`\<`TValue`\> = `{}` +### selector? + +(`state`) => `TSelected` + ## Returns -[`SolidQueuer`](../../interfaces/solidqueuer.md)\<`TValue`\> +[`SolidQueuer`](../../interfaces/solidqueuer.md)\<`TValue`, `TSelected`\> diff --git a/docs/framework/solid/reference/functions/createratelimitedsignal.md b/docs/framework/solid/reference/functions/createratelimitedsignal.md index 4a5d29c22..68d9067ec 100644 --- a/docs/framework/solid/reference/functions/createratelimitedsignal.md +++ b/docs/framework/solid/reference/functions/createratelimitedsignal.md @@ -8,10 +8,13 @@ title: createRateLimitedSignal # Function: createRateLimitedSignal() ```ts -function createRateLimitedSignal(value, initialOptions): [Accessor, Setter, SolidRateLimiter>] +function createRateLimitedSignal( + value, + initialOptions, + selector?): [Accessor, Setter, SolidRateLimiter, TSelected>] ``` -Defined in: [rate-limiter/createRateLimitedSignal.ts:65](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts#L65) +Defined in: [rate-limiter/createRateLimitedSignal.ts:68](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts#L68) A Solid hook that creates a rate-limited state value that enforces a hard limit on state updates within a time window. This hook combines Solid's createSignal with rate limiting functionality to provide controlled state updates. @@ -44,6 +47,8 @@ consider using the lower-level createRateLimiter hook instead. • **TValue** +• **TSelected** = `RateLimiterState` + ## Parameters ### value @@ -54,9 +59,13 @@ consider using the lower-level createRateLimiter hook instead. `RateLimiterOptions`\<`Setter`\<`TValue`\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidRateLimiter`](../../interfaces/solidratelimiter.md)\<`Setter`\<`TValue`\>\>\] +\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidRateLimiter`](../../interfaces/solidratelimiter.md)\<`Setter`\<`TValue`\>, `TSelected`\>\] ## Example diff --git a/docs/framework/solid/reference/functions/createratelimitedvalue.md b/docs/framework/solid/reference/functions/createratelimitedvalue.md index c6f047f50..39251a641 100644 --- a/docs/framework/solid/reference/functions/createratelimitedvalue.md +++ b/docs/framework/solid/reference/functions/createratelimitedvalue.md @@ -8,10 +8,13 @@ title: createRateLimitedValue # Function: createRateLimitedValue() ```ts -function createRateLimitedValue(value, initialOptions): [Accessor, SolidRateLimiter>] +function createRateLimitedValue( + value, + initialOptions, + selector?): [Accessor, SolidRateLimiter, TSelected>] ``` -Defined in: [rate-limiter/createRateLimitedValue.ts:50](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts#L50) +Defined in: [rate-limiter/createRateLimitedValue.ts:53](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts#L53) A high-level Solid hook that creates a rate-limited version of a value that updates at most a certain number of times within a time window. This hook uses Solid's createSignal internally to manage the rate-limited state. @@ -43,6 +46,8 @@ consider using the lower-level createRateLimiter hook instead. • **TValue** +• **TSelected** = `RateLimiterState` + ## Parameters ### value @@ -53,9 +58,13 @@ consider using the lower-level createRateLimiter hook instead. `RateLimiterOptions`\<`Setter`\<`TValue`\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`Accessor`\<`TValue`\>, [`SolidRateLimiter`](../../interfaces/solidratelimiter.md)\<`Setter`\<`TValue`\>\>\] +\[`Accessor`\<`TValue`\>, [`SolidRateLimiter`](../../interfaces/solidratelimiter.md)\<`Setter`\<`TValue`\>, `TSelected`\>\] ## Example diff --git a/docs/framework/solid/reference/functions/createratelimiter.md b/docs/framework/solid/reference/functions/createratelimiter.md index 2c8a1b33d..6189902d7 100644 --- a/docs/framework/solid/reference/functions/createratelimiter.md +++ b/docs/framework/solid/reference/functions/createratelimiter.md @@ -8,7 +8,10 @@ title: createRateLimiter # Function: createRateLimiter() ```ts -function createRateLimiter(fn, initialOptions): SolidRateLimiter +function createRateLimiter( + fn, + initialOptions, +selector?): SolidRateLimiter ``` Defined in: [rate-limiter/createRateLimiter.ts:62](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L62) @@ -37,6 +40,8 @@ For smoother execution patterns: • **TFn** *extends* `AnyFunction` +• **TSelected** = `RateLimiterState` + ## Parameters ### fn @@ -47,9 +52,13 @@ For smoother execution patterns: `RateLimiterOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -[`SolidRateLimiter`](../../interfaces/solidratelimiter.md)\<`TFn`\> +[`SolidRateLimiter`](../../interfaces/solidratelimiter.md)\<`TFn`, `TSelected`\> ## Example diff --git a/docs/framework/solid/reference/functions/createthrottledsignal.md b/docs/framework/solid/reference/functions/createthrottledsignal.md index ce685ac54..57b6c46d5 100644 --- a/docs/framework/solid/reference/functions/createthrottledsignal.md +++ b/docs/framework/solid/reference/functions/createthrottledsignal.md @@ -8,10 +8,13 @@ title: createThrottledSignal # Function: createThrottledSignal() ```ts -function createThrottledSignal(value, initialOptions): [Accessor, Setter, SolidThrottler>] +function createThrottledSignal( + value, + initialOptions, + selector?): [Accessor, Setter, SolidThrottler, TSelected>] ``` -Defined in: [throttler/createThrottledSignal.ts:41](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottledSignal.ts#L41) +Defined in: [throttler/createThrottledSignal.ts:44](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottledSignal.ts#L44) A Solid hook that creates a throttled state value that updates at most once within a specified time window. This hook combines Solid's createSignal with throttling functionality to provide controlled state updates. @@ -31,6 +34,8 @@ consider using the lower-level createThrottler hook instead. • **TValue** +• **TSelected** = `ThrottlerState`\<`Setter`\<`TValue`\>\> + ## Parameters ### value @@ -41,9 +46,13 @@ consider using the lower-level createThrottler hook instead. `ThrottlerOptions`\<`Setter`\<`TValue`\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidThrottler`](../../interfaces/solidthrottler.md)\<`Setter`\<`TValue`\>\>\] +\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidThrottler`](../../interfaces/solidthrottler.md)\<`Setter`\<`TValue`\>, `TSelected`\>\] ## Example diff --git a/docs/framework/solid/reference/functions/createthrottledvalue.md b/docs/framework/solid/reference/functions/createthrottledvalue.md index 622ce2f9e..ae7bc4926 100644 --- a/docs/framework/solid/reference/functions/createthrottledvalue.md +++ b/docs/framework/solid/reference/functions/createthrottledvalue.md @@ -8,10 +8,13 @@ title: createThrottledValue # Function: createThrottledValue() ```ts -function createThrottledValue(value, initialOptions): [Accessor, SolidThrottler>] +function createThrottledValue( + value, + initialOptions, + selector?): [Accessor, SolidThrottler, TSelected>] ``` -Defined in: [throttler/createThrottledValue.ts:35](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottledValue.ts#L35) +Defined in: [throttler/createThrottledValue.ts:38](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottledValue.ts#L38) A high-level Solid hook that creates a throttled version of a value that updates at most once within a specified time window. This hook uses Solid's createSignal internally to manage the throttled state. @@ -32,6 +35,8 @@ consider using the lower-level createThrottler hook instead. • **TValue** +• **TSelected** = `ThrottlerState`\<`Setter`\<`TValue`\>\> + ## Parameters ### value @@ -42,9 +47,13 @@ consider using the lower-level createThrottler hook instead. `ThrottlerOptions`\<`Setter`\<`TValue`\>\> +### selector? + +(`state`) => `TSelected` + ## Returns -\[`Accessor`\<`TValue`\>, [`SolidThrottler`](../../interfaces/solidthrottler.md)\<`Setter`\<`TValue`\>\>\] +\[`Accessor`\<`TValue`\>, [`SolidThrottler`](../../interfaces/solidthrottler.md)\<`Setter`\<`TValue`\>, `TSelected`\>\] ## Example diff --git a/docs/framework/solid/reference/functions/createthrottler.md b/docs/framework/solid/reference/functions/createthrottler.md index 42be3f6bc..0e81f1177 100644 --- a/docs/framework/solid/reference/functions/createthrottler.md +++ b/docs/framework/solid/reference/functions/createthrottler.md @@ -8,10 +8,13 @@ title: createThrottler # Function: createThrottler() ```ts -function createThrottler(fn, initialOptions): SolidThrottler +function createThrottler( + fn, + initialOptions, +selector?): SolidThrottler ``` -Defined in: [throttler/createThrottler.ts:59](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L59) +Defined in: [throttler/createThrottler.ts:57](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L57) A low-level Solid hook that creates a `Throttler` instance that limits how often the provided function can execute. @@ -27,6 +30,8 @@ expensive operations or UI updates. • **TFn** *extends* `AnyFunction` +• **TSelected** = `ThrottlerState`\<`TFn`\> + ## Parameters ### fn @@ -37,9 +42,13 @@ expensive operations or UI updates. `ThrottlerOptions`\<`TFn`\> +### selector? + +(`state`) => `TSelected` + ## Returns -[`SolidThrottler`](../../interfaces/solidthrottler.md)\<`TFn`\> +[`SolidThrottler`](../../interfaces/solidthrottler.md)\<`TFn`, `TSelected`\> ## Example diff --git a/docs/framework/solid/reference/index.md b/docs/framework/solid/reference/index.md index 875ebdcfd..5ad4b11c6 100644 --- a/docs/framework/solid/reference/index.md +++ b/docs/framework/solid/reference/index.md @@ -9,6 +9,7 @@ title: "@tanstack/solid-pacer" ## Interfaces +- [SolidAsyncBatcher](../interfaces/solidasyncbatcher.md) - [SolidAsyncDebouncer](../interfaces/solidasyncdebouncer.md) - [SolidAsyncQueuer](../interfaces/solidasyncqueuer.md) - [SolidAsyncRateLimiter](../interfaces/solidasyncratelimiter.md) @@ -21,6 +22,7 @@ title: "@tanstack/solid-pacer" ## Functions +- [createAsyncBatcher](../functions/createasyncbatcher.md) - [createAsyncDebouncer](../functions/createasyncdebouncer.md) - [createAsyncQueuer](../functions/createasyncqueuer.md) - [createAsyncRateLimiter](../functions/createasyncratelimiter.md) diff --git a/docs/framework/solid/reference/interfaces/solidasyncbatcher.md b/docs/framework/solid/reference/interfaces/solidasyncbatcher.md new file mode 100644 index 000000000..7d7f3c091 --- /dev/null +++ b/docs/framework/solid/reference/interfaces/solidasyncbatcher.md @@ -0,0 +1,34 @@ +--- +id: SolidAsyncBatcher +title: SolidAsyncBatcher +--- + + + +# Interface: SolidAsyncBatcher\ + +Defined in: [async-batcher/createAsyncBatcher.ts:9](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-batcher/createAsyncBatcher.ts#L9) + +## Extends + +- `Omit`\<`AsyncBatcher`\<`TValue`\>, `"store"`\> + +## Type Parameters + +• **TValue** + +• **TSelected** = `AsyncBatcherState`\<`TValue`\> + +## Properties + +### state + +```ts +readonly state: Accessor>; +``` + +Defined in: [async-batcher/createAsyncBatcher.ts:18](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-batcher/createAsyncBatcher.ts#L18) + +Reactive state that will be updated when the batcher state changes + +Use this instead of `batcher.store.state` diff --git a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md index ee5169ed4..8d7131dca 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md +++ b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md @@ -5,69 +5,30 @@ title: SolidAsyncDebouncer -# Interface: SolidAsyncDebouncer\ +# Interface: SolidAsyncDebouncer\ -Defined in: [async-debouncer/createAsyncDebouncer.ts:8](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L8) +Defined in: [async-debouncer/createAsyncDebouncer.ts:11](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L11) ## Extends -- `Omit`\<`AsyncDebouncer`\<`TFn`\>, - \| `"getErrorCount"` - \| `"getIsPending"` - \| `"getLastResult"` - \| `"getSettleCount"` - \| `"getSuccessCount"`\> +- `Omit`\<`AsyncDebouncer`\<`TFn`\>, `"store"`\> ## Type Parameters • **TFn** *extends* `AnyAsyncFunction` -## Properties - -### errorCount - -```ts -errorCount: Accessor; -``` - -Defined in: [async-debouncer/createAsyncDebouncer.ts:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L17) - -*** - -### isPending - -```ts -isPending: Accessor; -``` - -Defined in: [async-debouncer/createAsyncDebouncer.ts:18](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L18) +• **TSelected** = `AsyncDebouncerState`\<`TFn`\> -*** - -### lastResult - -```ts -lastResult: Accessor>; -``` - -Defined in: [async-debouncer/createAsyncDebouncer.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L19) - -*** +## Properties -### settleCount +### state ```ts -settleCount: Accessor; +readonly state: Accessor>; ``` Defined in: [async-debouncer/createAsyncDebouncer.ts:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L20) -*** - -### successCount - -```ts -successCount: Accessor; -``` +Reactive state that will be updated when the debouncer state changes -Defined in: [async-debouncer/createAsyncDebouncer.ts:21](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L21) +Use this instead of `debouncer.store.state` diff --git a/docs/framework/solid/reference/interfaces/solidasyncqueuer.md b/docs/framework/solid/reference/interfaces/solidasyncqueuer.md index f7e816be8..3a1cf99b8 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncqueuer.md +++ b/docs/framework/solid/reference/interfaces/solidasyncqueuer.md @@ -5,183 +5,30 @@ title: SolidAsyncQueuer -# Interface: SolidAsyncQueuer\ +# Interface: SolidAsyncQueuer\ -Defined in: [async-queuer/createAsyncQueuer.ts:7](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L7) +Defined in: [async-queuer/createAsyncQueuer.ts:9](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L9) ## Extends -- `Omit`\<`AsyncQueuer`\<`TValue`\>, - \| `"getErrorCount"` - \| `"getIsEmpty"` - \| `"getIsFull"` - \| `"getIsIdle"` - \| `"getIsRunning"` - \| `"getRejectionCount"` - \| `"getSettledCount"` - \| `"getSize"` - \| `"getSuccessCount"` - \| `"peekActiveItems"` - \| `"peekAllItems"` - \| `"peekNextItem"` - \| `"peekPendingItems"`\> +- `Omit`\<`AsyncQueuer`\<`TValue`\>, `"store"`\> ## Type Parameters • **TValue** -## Properties - -### activeItems - -```ts -activeItems: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:27](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L27) - -Signal version of `peekActiveItems` - -*** - -### allItems - -```ts -allItems: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:31](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L31) - -Signal version of `peekAllItems` - -*** - -### errorCount - -```ts -errorCount: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:35](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L35) - -Signal version of `getErrorCount` - -*** - -### isEmpty - -```ts -isEmpty: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:39](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L39) - -Signal version of `getIsEmpty` - -*** - -### isFull - -```ts -isFull: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:43](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L43) - -Signal version of `getIsFull` - -*** - -### isIdle - -```ts -isIdle: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:47](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L47) - -Signal version of `getIsIdle` +• **TSelected** = `AsyncQueuerState`\<`TValue`\> -*** - -### isRunning - -```ts -isRunning: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:51](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L51) - -Signal version of `getIsRunning` - -*** - -### nextItem - -```ts -nextItem: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:55](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L55) - -Signal version of `peekNextItem` - -*** - -### pendingItems - -```ts -pendingItems: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:59](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L59) - -Signal version of `peekPendingItems` - -*** - -### rejectionCount - -```ts -rejectionCount: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:63](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L63) - -Signal version of `getRejectionCount` - -*** - -### settledCount - -```ts -settledCount: Accessor; -``` - -Defined in: [async-queuer/createAsyncQueuer.ts:67](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L67) - -Signal version of `getSettledCount` - -*** +## Properties -### size +### state ```ts -size: Accessor; +readonly state: Accessor>; ``` -Defined in: [async-queuer/createAsyncQueuer.ts:71](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L71) - -Signal version of `getSize` - -*** - -### successCount - -```ts -successCount: Accessor; -``` +Defined in: [async-queuer/createAsyncQueuer.ts:16](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L16) -Defined in: [async-queuer/createAsyncQueuer.ts:75](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L75) +Reactive state that will be updated and re-rendered when the queuer state changes -Signal version of `getSuccessCount` +Use this instead of `queuer.store.state` diff --git a/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md b/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md index 3cfca7680..9e7cc9845 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md +++ b/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md @@ -5,80 +5,26 @@ title: SolidAsyncRateLimiter -# Interface: SolidAsyncRateLimiter\ +# Interface: SolidAsyncRateLimiter\ -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:8](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L8) +Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:10](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L10) ## Extends -- `Omit`\<`AsyncRateLimiter`\<`TFn`\>, - \| `"getSuccessCount"` - \| `"getSettleCount"` - \| `"getErrorCount"` - \| `"getRejectionCount"` - \| `"getRemainingInWindow"` - \| `"getMsUntilNextWindow"`\> +- `Omit`\<`AsyncRateLimiter`\<`TFn`\>, `"store"`\> ## Type Parameters • **TFn** *extends* `AnyAsyncFunction` -## Properties - -### errorCount - -```ts -errorCount: Accessor; -``` - -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L20) - -*** - -### msUntilNextWindow +• **TSelected** = `AsyncRateLimiterState`\<`TFn`\> -```ts -msUntilNextWindow: Accessor; -``` - -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:23](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L23) - -*** - -### rejectionCount - -```ts -rejectionCount: Accessor; -``` - -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:21](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L21) - -*** - -### remainingInWindow - -```ts -remainingInWindow: Accessor; -``` - -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:22](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L22) - -*** - -### settleCount - -```ts -settleCount: Accessor; -``` - -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L19) - -*** +## Properties -### successCount +### state ```ts -successCount: Accessor; +readonly state: Accessor>; ``` -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:18](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L18) +Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:14](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L14) diff --git a/docs/framework/solid/reference/interfaces/solidasyncthrottler.md b/docs/framework/solid/reference/interfaces/solidasyncthrottler.md index 2e8462ca9..04ab12a0f 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncthrottler.md +++ b/docs/framework/solid/reference/interfaces/solidasyncthrottler.md @@ -5,102 +5,30 @@ title: SolidAsyncThrottler -# Interface: SolidAsyncThrottler\ +# Interface: SolidAsyncThrottler\ -Defined in: [async-throttler/createAsyncThrottler.ts:8](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L8) +Defined in: [async-throttler/createAsyncThrottler.ts:10](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L10) ## Extends -- `Omit`\<`AsyncThrottler`\<`TFn`\>, - \| `"getSuccessCount"` - \| `"getSettleCount"` - \| `"getErrorCount"` - \| `"getIsPending"` - \| `"getIsExecuting"` - \| `"getLastResult"` - \| `"getLastExecutionTime"` - \| `"getNextExecutionTime"`\> +- `Omit`\<`AsyncThrottler`\<`TFn`\>, `"store"`\> ## Type Parameters • **TFn** *extends* `AnyAsyncFunction` -## Properties - -### errorCount - -```ts -errorCount: Accessor; -``` - -Defined in: [async-throttler/createAsyncThrottler.ts:22](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L22) - -*** - -### isExecuting - -```ts -isExecuting: Accessor; -``` +• **TSelected** = `AsyncThrottlerState`\<`TFn`\> -Defined in: [async-throttler/createAsyncThrottler.ts:24](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L24) - -*** - -### isPending - -```ts -isPending: Accessor; -``` - -Defined in: [async-throttler/createAsyncThrottler.ts:23](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L23) - -*** - -### lastExecutionTime - -```ts -lastExecutionTime: Accessor; -``` - -Defined in: [async-throttler/createAsyncThrottler.ts:26](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L26) - -*** - -### lastResult - -```ts -lastResult: Accessor>; -``` - -Defined in: [async-throttler/createAsyncThrottler.ts:25](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L25) - -*** - -### nextExecutionTime - -```ts -nextExecutionTime: Accessor; -``` - -Defined in: [async-throttler/createAsyncThrottler.ts:27](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L27) - -*** +## Properties -### settleCount +### state ```ts -settleCount: Accessor; +readonly state: Accessor>; ``` -Defined in: [async-throttler/createAsyncThrottler.ts:21](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L21) - -*** - -### successCount +Defined in: [async-throttler/createAsyncThrottler.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L19) -```ts -successCount: Accessor; -``` +Reactive state that will be updated and re-rendered when the throttler state changes -Defined in: [async-throttler/createAsyncThrottler.ts:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L20) +Use this instead of `throttler.store.state` diff --git a/docs/framework/solid/reference/interfaces/solidbatcher.md b/docs/framework/solid/reference/interfaces/solidbatcher.md index 063f79b7c..3f117625a 100644 --- a/docs/framework/solid/reference/interfaces/solidbatcher.md +++ b/docs/framework/solid/reference/interfaces/solidbatcher.md @@ -5,92 +5,30 @@ title: SolidBatcher -# Interface: SolidBatcher\ +# Interface: SolidBatcher\ -Defined in: [batcher/createBatcher.ts:7](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L7) +Defined in: [batcher/createBatcher.ts:6](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L6) ## Extends -- `Omit`\<`Batcher`\<`TValue`\>, - \| `"peekAllItems"` - \| `"getBatchExecutionCount"` - \| `"getIsEmpty"` - \| `"getIsRunning"` - \| `"getItemExecutionCount"` - \| `"getSize"`\> +- `Omit`\<`Batcher`\<`TValue`\>, `"store"`\> ## Type Parameters • **TValue** -## Properties - -### allItems - -```ts -allItems: Accessor; -``` - -Defined in: [batcher/createBatcher.ts:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L20) - -Signal version of `peekAllItems` - -*** - -### batchExecutionCount +• **TSelected** = `BatcherState`\<`TValue`\> -```ts -batchExecutionCount: Accessor; -``` - -Defined in: [batcher/createBatcher.ts:24](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L24) - -Signal version of `getBatchExecutionCount` - -*** - -### isEmpty - -```ts -isEmpty: Accessor; -``` - -Defined in: [batcher/createBatcher.ts:28](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L28) - -Signal version of `getIsEmpty` - -*** - -### isRunning - -```ts -isRunning: Accessor; -``` - -Defined in: [batcher/createBatcher.ts:32](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L32) - -Signal version of `getIsRunning` - -*** +## Properties -### itemExecutionCount +### state ```ts -itemExecutionCount: Accessor; +readonly state: Accessor>; ``` -Defined in: [batcher/createBatcher.ts:36](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L36) - -Signal version of `getItemExecutionCount` - -*** - -### size - -```ts -size: Accessor; -``` +Defined in: [batcher/createBatcher.ts:13](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L13) -Defined in: [batcher/createBatcher.ts:40](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/batcher/createBatcher.ts#L40) +Reactive state that will be updated when the batcher state changes -Signal version of `getSize` +Use this instead of `batcher.store.state` diff --git a/docs/framework/solid/reference/interfaces/soliddebouncer.md b/docs/framework/solid/reference/interfaces/soliddebouncer.md index fa3285f86..b9183f1f9 100644 --- a/docs/framework/solid/reference/interfaces/soliddebouncer.md +++ b/docs/framework/solid/reference/interfaces/soliddebouncer.md @@ -5,36 +5,30 @@ title: SolidDebouncer -# Interface: SolidDebouncer\ +# Interface: SolidDebouncer\ Defined in: [debouncer/createDebouncer.ts:11](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L11) -An extension of the Debouncer class that adds Solid signals to access the internal state of the debouncer - ## Extends -- `Omit`\<`Debouncer`\<`TFn`\>, `"getExecutionCount"` \| `"getIsPending"`\> +- `Omit`\<`Debouncer`\<`TFn`\>, `"store"`\> ## Type Parameters • **TFn** *extends* `AnyFunction` +• **TSelected** = `DebouncerState`\<`TFn`\> + ## Properties -### executionCount +### state ```ts -executionCount: Accessor; +readonly state: Accessor>; ``` -Defined in: [debouncer/createDebouncer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L13) - -*** +Defined in: [debouncer/createDebouncer.ts:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L20) -### isPending - -```ts -isPending: Accessor; -``` +Reactive state that will be updated when the debouncer state changes -Defined in: [debouncer/createDebouncer.ts:14](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L14) +Use this instead of `debouncer.store.state` diff --git a/docs/framework/solid/reference/interfaces/solidqueuer.md b/docs/framework/solid/reference/interfaces/solidqueuer.md index 30a564cf1..b045a5445 100644 --- a/docs/framework/solid/reference/interfaces/solidqueuer.md +++ b/docs/framework/solid/reference/interfaces/solidqueuer.md @@ -5,130 +5,30 @@ title: SolidQueuer -# Interface: SolidQueuer\ +# Interface: SolidQueuer\ -Defined in: [queuer/createQueuer.ts:7](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L7) +Defined in: [queuer/createQueuer.ts:6](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L6) ## Extends -- `Omit`\<`Queuer`\<`TValue`\>, - \| `"getExecutionCount"` - \| `"getIsEmpty"` - \| `"getIsFull"` - \| `"getIsIdle"` - \| `"getIsRunning"` - \| `"getSize"` - \| `"peekAllItems"` - \| `"peekNextItem"`\> +- `Omit`\<`Queuer`\<`TValue`\>, `"store"`\> ## Type Parameters • **TValue** -## Properties - -### allItems - -```ts -allItems: Accessor; -``` - -Defined in: [queuer/createQueuer.ts:22](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L22) - -Signal version of `peekAllItems` - -*** - -### executionCount - -```ts -executionCount: Accessor; -``` - -Defined in: [queuer/createQueuer.ts:26](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L26) - -Signal version of `getExecutionCount` - -*** - -### isEmpty - -```ts -isEmpty: Accessor; -``` - -Defined in: [queuer/createQueuer.ts:30](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L30) - -Signal version of `getIsEmpty` - -*** - -### isFull - -```ts -isFull: Accessor; -``` - -Defined in: [queuer/createQueuer.ts:34](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L34) - -Signal version of `getIsFull` +• **TSelected** = `QueuerState`\<`TValue`\> -*** - -### isIdle - -```ts -isIdle: Accessor; -``` - -Defined in: [queuer/createQueuer.ts:38](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L38) - -Signal version of `getIsIdle` - -*** - -### isRunning - -```ts -isRunning: Accessor; -``` - -Defined in: [queuer/createQueuer.ts:42](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L42) - -Signal version of `getIsRunning` - -*** - -### nextItem - -```ts -nextItem: Accessor; -``` - -Defined in: [queuer/createQueuer.ts:46](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L46) - -Signal version of `peekNextItem` - -*** +## Properties -### rejectionCount +### state ```ts -rejectionCount: Accessor; +readonly state: Accessor>; ``` -Defined in: [queuer/createQueuer.ts:50](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L50) - -Signal version of `getRejectionCount` - -*** - -### size - -```ts -size: Accessor; -``` +Defined in: [queuer/createQueuer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L13) -Defined in: [queuer/createQueuer.ts:54](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L54) +Reactive state that will be updated when the queuer state changes -Signal version of `getSize` +Use this instead of `queuer.store.state` diff --git a/docs/framework/solid/reference/interfaces/solidratelimiter.md b/docs/framework/solid/reference/interfaces/solidratelimiter.md index f8cbc71b6..b7c1fe322 100644 --- a/docs/framework/solid/reference/interfaces/solidratelimiter.md +++ b/docs/framework/solid/reference/interfaces/solidratelimiter.md @@ -5,58 +5,30 @@ title: SolidRateLimiter -# Interface: SolidRateLimiter\ +# Interface: SolidRateLimiter\ -Defined in: [rate-limiter/createRateLimiter.ts:8](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L8) +Defined in: [rate-limiter/createRateLimiter.ts:10](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L10) ## Extends -- `Omit`\<`RateLimiter`\<`TFn`\>, - \| `"getExecutionCount"` - \| `"getMsUntilNextWindow"` - \| `"getRejectionCount"` - \| `"getRemainingInWindow"`\> +- `Omit`\<`RateLimiter`\<`TFn`\>, `"store"`\> ## Type Parameters • **TFn** *extends* `AnyFunction` -## Properties - -### executionCount - -```ts -executionCount: Accessor; -``` +• **TSelected** = `RateLimiterState` -Defined in: [rate-limiter/createRateLimiter.ts:16](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L16) - -*** - -### msUntilNextWindow - -```ts -msUntilNextWindow: Accessor; -``` - -Defined in: [rate-limiter/createRateLimiter.ts:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L17) - -*** +## Properties -### rejectionCount +### state ```ts -rejectionCount: Accessor; +readonly state: Accessor>; ``` -Defined in: [rate-limiter/createRateLimiter.ts:18](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L18) - -*** - -### remainingInWindow +Defined in: [rate-limiter/createRateLimiter.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L19) -```ts -remainingInWindow: Accessor; -``` +Reactive state that will be updated when the rate limiter state changes -Defined in: [rate-limiter/createRateLimiter.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L19) +Use this instead of `rateLimiter.store.state` diff --git a/docs/framework/solid/reference/interfaces/solidthrottler.md b/docs/framework/solid/reference/interfaces/solidthrottler.md index e7b17ad12..8993403a5 100644 --- a/docs/framework/solid/reference/interfaces/solidthrottler.md +++ b/docs/framework/solid/reference/interfaces/solidthrottler.md @@ -5,60 +5,30 @@ title: SolidThrottler -# Interface: SolidThrottler\ +# Interface: SolidThrottler\ Defined in: [throttler/createThrottler.ts:11](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L11) -An extension of the Throttler class that adds Solid signals to access the internal state of the throttler - ## Extends -- `Omit`\<`Throttler`\<`TFn`\>, - \| `"getExecutionCount"` - \| `"getIsPending"` - \| `"getLastExecutionTime"` - \| `"getNextExecutionTime"`\> +- `Omit`\<`Throttler`\<`TFn`\>, `"store"`\> ## Type Parameters • **TFn** *extends* `AnyFunction` -## Properties - -### executionCount +• **TSelected** = `ThrottlerState`\<`TFn`\> -```ts -executionCount: Accessor; -``` - -Defined in: [throttler/createThrottler.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L19) - -*** +## Properties -### isPending +### state ```ts -isPending: Accessor; +readonly state: Accessor>; ``` Defined in: [throttler/createThrottler.ts:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L20) -*** - -### lastExecutionTime - -```ts -lastExecutionTime: Accessor; -``` - -Defined in: [throttler/createThrottler.ts:21](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L21) - -*** - -### nextExecutionTime - -```ts -nextExecutionTime: Accessor; -``` +Reactive state that will be updated when the throttler state changes -Defined in: [throttler/createThrottler.ts:22](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L22) +Use this instead of `throttler.store.state` diff --git a/docs/guides/async-batching.md b/docs/guides/async-batching.md new file mode 100644 index 000000000..88a80e304 --- /dev/null +++ b/docs/guides/async-batching.md @@ -0,0 +1,365 @@ +--- +title: Async Batching Guide +id: async-batching +--- + +All core concepts from the [Batching Guide](../batching.md) apply to async batching as well. + +## When to Use Async Batching + +While the synchronous batcher works well for many use cases, async batching provides additional capabilities that are particularly useful when: + +- You need to capture and use the return value from batch executions +- Your batch processing involves asynchronous operations (API calls, database operations, file I/O) +- You require advanced error handling with configurable error behavior +- You want to track success/error statistics separately +- You need to monitor when batches are actively executing + +## Async Batching in TanStack Pacer + +TanStack Pacer provides async batching through the `AsyncBatcher` class and the `asyncBatch` function. Unlike the synchronous version, the async batcher handles Promises and provides robust error handling capabilities. + +### Basic Usage with `asyncBatch` + +The `asyncBatch` function provides a simple way to create an async batching function: + +```ts +import { asyncBatch } from '@tanstack/pacer' + +const processAsyncBatch = asyncBatch( + async (items) => { + // Process the batch asynchronously + const results = await Promise.all( + items.map(item => processApiCall(item)) + ) + return results + }, + { + maxSize: 3, + wait: 2000, + onSuccess: (results, batcher) => { + console.log('Batch completed successfully:', results) + console.log('Total successes:', batcher.store.state.successCount) + }, + onError: (error, failedItems, batcher) => { + console.error('Batch failed:', error) + console.log('Failed items:', failedItems) + console.log('Total errors:', batcher.store.state.errorCount) + } + } +) + +// Add items to be batched +processAsyncBatch(1) +processAsyncBatch(2) +processAsyncBatch(3) // Triggers batch processing +``` + +### Advanced Usage with `AsyncBatcher` Class + +For more control over async batch behavior, use the `AsyncBatcher` class directly: + +```ts +import { AsyncBatcher } from '@tanstack/pacer' + +const batcher = new AsyncBatcher( + async (items) => { + // Process the batch asynchronously + const results = await Promise.all( + items.map(item => processApiCall(item)) + ) + return results + }, + { + maxSize: 5, + wait: 3000, + onSuccess: (results, batcher) => { + console.log('Batch succeeded:', results) + }, + onError: (error, failedItems, batcher) => { + console.error('Batch failed:', error) + console.log('Failed items:', failedItems) + } + } +) + +// Access current state via TanStack Store +console.log(batcher.store.state.successCount) // Number of successful batch executions +console.log(batcher.store.state.errorCount) // Number of failed batch executions +console.log(batcher.store.state.isExecuting) // Whether a batch is currently executing +console.log(batcher.store.state.lastResult) // Result from most recent batch + +// Add items to the batch +batcher.addItem(1) +batcher.addItem(2) + +// Control batch execution +batcher.stop() // Stop processing +batcher.start() // Resume processing +``` + +## Key Differences from Synchronous Batching + +### 1. Return Value Handling + +Unlike the synchronous batcher which returns void, the async version allows you to capture and use the return value from your batch function: + +```ts +const batcher = new AsyncBatcher( + async (items) => { + const results = await processBatch(items) + return results + }, + { + maxSize: 5, + onSuccess: (results, batcher) => { + // Handle the returned results + console.log('Batch results:', results) + } + } +) +``` + +### 2. Error Handling + +The async batcher provides comprehensive error handling capabilities: + +```ts +const batcher = new AsyncBatcher( + async (items) => { + // This might throw an error + const results = await riskyBatchOperation(items) + return results + }, + { + maxSize: 3, + onError: (error, failedItems, batcher) => { + // Handle batch errors + console.error('Batch processing failed:', error) + console.log('Items that failed:', failedItems) + console.log('Total error count:', batcher.store.state.errorCount) + }, + throwOnError: false, // Don't throw errors, just handle them + onSuccess: (results, batcher) => { + console.log('Batch succeeded:', results) + console.log('Total success count:', batcher.store.state.successCount) + }, + onSettled: (batcher) => { + // Called after every batch (success or failure) + console.log('Batch settled. Total batches:', batcher.store.state.settleCount) + } + } +) +``` + +### 3. Execution State Tracking + +The async batcher tracks when batches are actively executing: + +```ts +const batcher = new AsyncBatcher( + async (items) => { + console.log('Starting batch execution...') + const results = await longRunningBatchOperation(items) + console.log('Batch execution completed') + return results + }, + { + maxSize: 5, + onItemsChange: (batcher) => { + console.log('Is executing:', batcher.store.state.isExecuting) + console.log('Items in queue:', batcher.store.state.size) + } + } +) +``` + +### 4. Different Callbacks + +The `AsyncBatcher` supports these async-specific callbacks: + +- `onSuccess`: Called after each successful batch execution, providing the result and batcher instance +- `onError`: Called when a batch execution fails, providing the error, failed items, and batcher instance +- `onSettled`: Called after each batch execution (success or failure), providing the batcher instance +- `onExecute`: Called after each batch execution (same as synchronous batcher) +- `onItemsChange`: Called when items are added or the batch is processed + +## Error Handling Options + +The async batcher provides flexible error handling through the `throwOnError` option: + +```ts +const batcher = new AsyncBatcher( + async (items) => { + // This might throw an error + throw new Error('Batch processing failed') + }, + { + maxSize: 3, + onError: (error, failedItems, batcher) => { + console.error('Handling error:', error) + }, + throwOnError: true, // Will throw errors even with onError handler + // throwOnError: false, // Will swallow errors (default if onError is provided) + // throwOnError: undefined, // Uses default behavior based on onError presence + } +) +``` + +- **Default behavior**: `throwOnError` is `true` if no `onError` handler is provided, `false` if an `onError` handler is provided +- **With `onError` handler**: The handler is called first, then the error is thrown if `throwOnError` is `true` +- **Error state**: Failed items are tracked in `failedItems` array and can be accessed via `peekFailedItems()` + +## Dynamic Options + +Like the synchronous batcher, the async batcher supports dynamic options: + +```ts +const batcher = new AsyncBatcher( + async (items) => { + return await processBatch(items) + }, + { + // Dynamic batch size based on success rate + maxSize: (batcher) => { + const successRate = batcher.store.state.successCount / Math.max(1, batcher.store.state.settleCount) + return successRate > 0.8 ? 10 : 5 // Larger batches if success rate is high + }, + // Dynamic wait time based on error count + wait: (batcher) => { + return batcher.store.state.errorCount > 5 ? 5000 : 2000 // Wait longer if errors are frequent + } + } +) +``` + +## State Management + +The `AsyncBatcher` class uses TanStack Store for reactive state management, providing real-time access to batch execution state, error tracking, and processing statistics. + +### Accessing State + +When using the `AsyncBatcher` class directly, access state via the `store.state` property: + +```ts +const batcher = new AsyncBatcher(asyncBatchFn, { maxSize: 5, wait: 1000 }) + +// Access current state +console.log(batcher.store.state.totalItemsProcessed) +``` + +### Framework Adapters + +When using framework adapters like React or Solid, the state is exposed directly as a reactive property: + +```ts +// React example +const batcher = useAsyncBatcher(asyncBatchFn, { maxSize: 5, wait: 1000 }) + +// Access state directly (reactive) +console.log(batcher.state.successCount) // Reactive value +console.log(batcher.state.isExecuting) // Reactive value +``` + +### Initial State + +You can provide initial state values when creating an async batcher: + +```ts +const batcher = new AsyncBatcher(asyncBatchFn, { + maxSize: 5, + wait: 1000, + initialState: { + successCount: 10, // Start with 10 successful batches + errorCount: 2, // Start with 2 failed batches + totalItemsProcessed: 50, // Start with 50 items processed + lastResult: 'initial-result', // Start with initial result + } +}) +``` + +### Subscribing to State Changes + +The store is reactive and supports subscriptions: + +```ts +const batcher = new AsyncBatcher(asyncBatchFn, { maxSize: 5, wait: 1000 }) + +// Subscribe to state changes +const unsubscribe = batcher.store.subscribe((state) => { + console.log('Success count:', state.successCount) + console.log('Error count:', state.errorCount) + console.log('Currently executing:', state.isExecuting) + console.log('Items processed:', state.totalItemsProcessed) +}) + +// Unsubscribe when done +unsubscribe() +``` + +### Available State Properties + +The `AsyncBatcherState` includes: + +- `successCount`: Number of successful batch executions +- `errorCount`: Number of failed batch executions +- `settleCount`: Total batch executions completed (success + error) +- `isExecuting`: Whether a batch is currently being processed +- `isPending`: Whether the batcher is waiting for timeout to trigger batch processing +- `isRunning`: Whether the batcher is active and will process items automatically +- `isEmpty`: Whether the batcher has no items to process +- `size`: Number of items currently in the batch queue +- `status`: Current processing status ('idle' | 'pending' | 'executing' | 'populated') +- `items`: Array of items currently queued for batch processing +- `lastResult`: Result from the most recent successful batch execution +- `failedItems`: Array of items that failed during batch processing +- `totalItemsProcessed`: Total number of items processed successfully across all batches +- `totalItemsFailed`: Total number of items that failed processing across all batches + +### Flushing Pending Batches + +The async batcher supports flushing pending batches to trigger processing immediately: + +```ts +const batcher = new AsyncBatcher(asyncBatchFn, { maxSize: 10, wait: 5000 }) + +batcher.addItem('item1') +batcher.addItem('item2') +console.log(batcher.store.state.isPending) // true + +// Flush immediately instead of waiting +const result = await batcher.flush() +console.log('Flush result:', result) +console.log(batcher.store.state.isEmpty) // true (batch was processed) +``` + +### Monitoring Failed Items + +The async batcher tracks items that failed during batch processing: + +```ts +const batcher = new AsyncBatcher( + async (items) => { + // This might fail for some items + if (items.some(item => item < 0)) { + throw new Error('Negative numbers not allowed') + } + return await processBatch(items) + }, + { + maxSize: 3, + onError: (error, failedItems, batcher) => { + console.log('Failed items:', failedItems) + console.log('All failed items:', batcher.peekFailedItems()) + } + } +) +``` + +## Framework Adapters + +Each framework adapter provides hooks that build on top of the core async batching functionality to integrate with the framework's state management system. Hooks like `useAsyncBatcher` or similar are available for each framework. + +--- + +For core batching concepts and synchronous batching, see the [Batching Guide](../batching.md). \ No newline at end of file diff --git a/docs/guides/async-debouncing.md b/docs/guides/async-debouncing.md index a1ac4abca..182fb143c 100644 --- a/docs/guides/async-debouncing.md +++ b/docs/guides/async-debouncing.md @@ -59,7 +59,7 @@ The async debouncer provides robust error handling capabilities: - When true (default if no onError handler), errors will be thrown - When false (default if onError handler provided), errors will be swallowed - Can be explicitly set to override these defaults -- You can track error counts using `getErrorCount()` and check execution state with `getIsExecuting()` +- You can track error counts using `debouncer.store.state.errorCount` and check execution state with `debouncer.store.state.isExecuting` - The debouncer maintains its state and can continue to be used after an error occurs ### 3. Different Callbacks @@ -78,11 +78,11 @@ const asyncDebouncer = new AsyncDebouncer(async (value) => { wait: 500, onSuccess: (result, debouncer) => { // Called after each successful execution - console.log('Async function executed', debouncer.getSuccessCount()) + console.log('Async function executed', debouncer.store.state.successCount) }, onSettled: (debouncer) => { // Called after each execution attempt - console.log('Async function settled', debouncer.getSettledCount()) + console.log('Async function settled', debouncer.store.state.settleCount) }, onError: (error) => { // Called if the async function throws an error @@ -105,6 +105,96 @@ Just like the synchronous debouncer, the async debouncer supports dynamic option Each framework adapter provides hooks that build on top of the core async debouncing functionality to integrate with the framework's state management system. Hooks like `createAsyncDebouncer`, `useAsyncDebouncedCallback`, or similar are available for each framework. +## State Management + +The `AsyncDebouncer` class uses TanStack Store for reactive state management, providing real-time access to execution state, error tracking, and execution statistics. + +### Accessing State + +When using the `AsyncDebouncer` class directly, access state via the `store.state` property: + +```ts +const asyncDebouncer = new AsyncDebouncer(asyncFn, { wait: 500 }) + +// Access current state +console.log(asyncDebouncer.store.state.isPending) // Number of successful executions +``` + +### Framework Adapters + +When using framework adapters like React or Solid, the state is exposed directly as a reactive property: + +```ts +// React example +const asyncDebouncer = useAsyncDebouncer(asyncFn, { wait: 500 }) + +// Access state directly (reactive) +console.log(asyncDebouncer.state.successCount) // Reactive value +console.log(asyncDebouncer.state.isExecuting) // Reactive value +``` + +### Initial State + +You can provide initial state values when creating an async debouncer: + +```ts +const asyncDebouncer = new AsyncDebouncer(asyncFn, { + wait: 500, + initialState: { + successCount: 3, // Start with 3 successful executions + errorCount: 1, // Start with 1 error + lastResult: 'initial-result', // Start with initial result + } +}) +``` + +### Subscribing to State Changes + +The store is reactive and supports subscriptions: + +```ts +const asyncDebouncer = new AsyncDebouncer(asyncFn, { wait: 500 }) + +// Subscribe to state changes +const unsubscribe = asyncDebouncer.store.subscribe((state) => { + console.log('Success count:', state.successCount) + console.log('Error count:', state.errorCount) + console.log('Currently executing:', state.isExecuting) +}) + +// Unsubscribe when done +unsubscribe() +``` + +### Available State Properties + +The `AsyncDebouncerState` includes: + +- `successCount`: Number of successful function executions +- `errorCount`: Number of failed function executions +- `settleCount`: Total number of completed executions (success + error) +- `isExecuting`: Whether the async function is currently executing +- `isPending`: Whether the debouncer is waiting for timeout to trigger execution +- `status`: Current execution status ('idle' | 'pending' | 'executing' | 'settled') +- `canLeadingExecute`: Whether leading edge execution is allowed +- `lastResult`: Result from the most recent successful execution +- `lastArgs`: Arguments from the most recent call to `maybeExecute` + +### Flushing Pending Executions + +The async debouncer supports flushing pending executions to trigger them immediately: + +```ts +const asyncDebouncer = new AsyncDebouncer(asyncFn, { wait: 1000 }) + +asyncDebouncer.maybeExecute('some-arg') +console.log(asyncDebouncer.store.state.isPending) // true + +// Flush immediately instead of waiting +asyncDebouncer.flush() +console.log(asyncDebouncer.store.state.isPending) // false +``` + --- For core debouncing concepts and synchronous debouncing, see the [Debouncing Guide](../debouncing.md). \ No newline at end of file diff --git a/docs/guides/async-queuing.md b/docs/guides/async-queuing.md index 39887da41..4b14a7ee0 100644 --- a/docs/guides/async-queuing.md +++ b/docs/guides/async-queuing.md @@ -105,16 +105,16 @@ queue.setOptions({ onError: (error, queuer) => { console.error('Task failed:', error) // You can access queue state here - console.log('Error count:', queuer.getErrorCount()) + console.log('Error count:', queuer.store.state.errorCount) }, onSuccess: (result, queuer) => { console.log('Task completed:', result) // You can access queue state here - console.log('Success count:', queuer.getSuccessCount()) + console.log('Success count:', queuer.store.state.successCount) }, onSettled: (queuer) => { // Called after each execution (success or failure) - console.log('Total settled:', queuer.getSettledCount()) + console.log('Total settled:', queuer.store.state.settledCount) } }) @@ -164,17 +164,17 @@ const queue = new AsyncQueuer( onError: (error, queuer) => { console.error('Task failed:', error) // You can access queue state here - console.log('Error count:', queuer.getErrorCount()) + console.log('Error count:', queuer.store.state.errorCount) }, throwOnError: true, // Will throw errors even with onError handler onSuccess: (result, queuer) => { console.log('Task succeeded:', result) // You can access queue state here - console.log('Success count:', queuer.getSuccessCount()) + console.log('Success count:', queuer.store.state.successCount) }, onSettled: (queuer) => { // Called after each execution (success or failure) - console.log('Total settled:', queuer.getSettledCount()) + console.log('Total settled:', queuer.store.state.settledCount) } } ) @@ -194,11 +194,11 @@ const queue = new AsyncQueuer( { // Dynamic concurrency based on system load concurrency: (queuer) => { - return Math.max(1, 4 - queuer.peekActiveItems().length) + return Math.max(1, 4 - queuer.store.state.activeItems.length) }, // Dynamic wait time based on queue size wait: (queuer) => { - return queuer.getSize() > 10 ? 2000 : 1000 + return queuer.store.state.size > 10 ? 2000 : 1000 } } ) @@ -209,8 +209,10 @@ const queue = new AsyncQueuer( AsyncQueuer provides all the queue management and monitoring methods from the core queuing guide, plus async-specific ones: - `peekActiveItems()` — Items currently being processed - `peekPendingItems()` — Items waiting to be processed -- `getSuccessCount()`, `getErrorCount()`, `getSettledCount()` — Execution statistics -- `start()`, `stop()`, `clear()`, `reset()`, etc. +- `queuer.store.state.successCount`, `queuer.store.state.errorCount`, `queuer.store.state.settledCount` — Execution statistics +- `queuer.store.state.activeItems` — Array of items currently being processed +- `queuer.store.state.size` — Current queue size +- `start()`, `stop()`, `clear()`, `reset()`, `flush()`, etc. See the [Queuing Guide](../queuing) for more on queue management concepts. @@ -222,6 +224,114 @@ AsyncQueuer supports expiration and rejection just like the core queuer: See the [Queuing Guide](../queuing.md) for details and examples. +## State Management + +The `AsyncQueuer` class uses TanStack Store for reactive state management, providing real-time access to queue state, processing statistics, and concurrent task tracking. + +### Accessing State + +When using the `AsyncQueuer` class directly, access state via the `store.state` property: + +```ts +const queue = new AsyncQueuer(processFn, { concurrency: 2, wait: 1000 }) + +// Access current state +console.log(queue.store.state.isIdle) +``` + +### Framework Adapters + +When using framework adapters like React or Solid, the state is exposed directly as a reactive property: + +```ts +// React example +const queue = useAsyncQueuer(processFn, { concurrency: 2, wait: 1000 }) + +// Access state directly (reactive) +console.log(queue.state.successCount) // Reactive value +console.log(queue.state.activeItems.length) // Reactive value +``` + +### Initial State + +You can provide initial state values when creating an async queuer: + +```ts +const queue = new AsyncQueuer(processFn, { + concurrency: 2, + wait: 1000, + initialState: { + successCount: 10, // Start with 10 successful tasks + errorCount: 2, // Start with 2 failed tasks + isRunning: false, // Start paused + lastResult: 'initial-result', // Start with initial result + } +}) +``` + +### Subscribing to State Changes + +The store is reactive and supports subscriptions: + +```ts +const queue = new AsyncQueuer(processFn, { concurrency: 2, wait: 1000 }) + +// Subscribe to state changes +const unsubscribe = queue.store.subscribe((state) => { + console.log('Active tasks:', state.activeItems.length) + console.log('Success count:', state.successCount) + console.log('Error count:', state.errorCount) + console.log('Queue size:', state.size) + console.log('Is running:', state.isRunning) +}) + +// Unsubscribe when done +unsubscribe() +``` + +### Available State Properties + +The `AsyncQueuerState` includes all properties from the core queuing guide plus: + +- `successCount`: Number of successful task executions +- `errorCount`: Number of failed task executions +- `settledCount`: Total number of completed tasks (success + error) +- `activeItems`: Array of items currently being processed +- `lastResult`: Result from the most recent successful task execution +- `rejectionCount`: Number of tasks rejected due to maxSize +- `expirationCount`: Number of tasks expired +- `size`: Current number of items in the queue +- `isEmpty`: Whether the queue has no items +- `isFull`: Whether the queue has reached maxSize +- `isIdle`: Whether the queue has no active tasks +- `isRunning`: Whether the queue is active and processing tasks +- `status`: Current processing status ('idle' | 'running' | 'stopped') +- `items`: Array of items waiting to be processed +- `itemTimestamps`: Array of timestamps when items were added +- `pendingTick`: Whether the queue has a pending timeout for processing + +### Flushing Queue Items + +The async queuer supports flushing items to process them immediately: + +```ts +const queue = new AsyncQueuer(processFn, { concurrency: 2, wait: 5000 }) + +queue.addItem('item1') +queue.addItem('item2') +queue.addItem('item3') +console.log(queue.store.state.size) // 3 + +// Flush all items immediately instead of waiting +queue.flush() +console.log(queue.store.state.activeItems.length) // 2 (processing concurrently) +console.log(queue.store.state.size) // 1 (one remaining) + +// Or flush a specific number of items +queue.flush(1) // Process 1 more item +console.log(queue.store.state.activeItems.length) // 3 (all processing concurrently) +``` + ### Framework Adapters Each framework adapter builds convenient hooks and functions around the async queuer classes. Hooks like `useAsyncQueuer` or `useAsyncQueuedState` are small wrappers that can cut down on the boilerplate needed in your own code for some common use cases. diff --git a/docs/guides/async-rate-limiting.md b/docs/guides/async-rate-limiting.md index 5e966195a..ac8464ddc 100644 --- a/docs/guides/async-rate-limiting.md +++ b/docs/guides/async-rate-limiting.md @@ -27,7 +27,7 @@ const rateLimitedApi = asyncRateLimit( limit: 5, window: 1000, onExecute: (limiter) => { - console.log('API call succeeded:', limiter.getExecutionCount()) + console.log('API call succeeded:', limiter.store.state.successCount) }, onReject: (limiter) => { console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`) @@ -63,7 +63,7 @@ The async rate limiter provides robust error handling capabilities: - When true (default if no onError handler), errors will be thrown - When false (default if onError handler provided), errors will be swallowed - Can be explicitly set to override these defaults -- You can track error counts using `getErrorCount()` and check execution state with `getIsExecuting()` +- You can track error counts using `limiter.store.state.errorCount` and check execution state with `limiter.store.state.isExecuting` - The rate limiter maintains its state and can continue to be used after an error occurs - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler @@ -86,7 +86,7 @@ const asyncLimiter = new AsyncRateLimiter(async (id) => { window: 1000, onExecute: (rateLimiter) => { // Called after each successful execution - console.log('Async function executed', rateLimiter.getExecutionCount()) + console.log('Async function executed', rateLimiter.store.state.successCount) }, onReject: (rateLimiter) => { // Called when an execution is rejected @@ -113,6 +113,97 @@ Just like the synchronous rate limiter, the async rate limiter supports dynamic Each framework adapter provides hooks that build on top of the core async rate limiting functionality to integrate with the framework's state management system. Hooks like `createAsyncRateLimiter`, `useAsyncRateLimitedCallback`, or similar are available for each framework. +## State Management + +The `AsyncRateLimiter` class uses TanStack Store for reactive state management, providing real-time access to execution state, error tracking, and rejection statistics. + +### Accessing State + +When using the `AsyncRateLimiter` class directly, access state via the `store.state` property: + +```ts +const asyncLimiter = new AsyncRateLimiter(asyncFn, { limit: 5, window: 1000 }) + +// Access current state +console.log(asyncLimiter.store.state.isExecuting) +``` + +### Framework Adapters + +When using framework adapters like React or Solid, the state is exposed directly as a reactive property: + +```ts +// React example +const asyncLimiter = useAsyncRateLimiter(asyncFn, { limit: 5, window: 1000 }) + +// Access state directly (reactive) +console.log(asyncLimiter.state.successCount) // Reactive value +console.log(asyncLimiter.state.isExecuting) // Reactive value +``` + +### Initial State + +You can provide initial state values when creating an async rate limiter: + +```ts +const asyncLimiter = new AsyncRateLimiter(asyncFn, { + limit: 5, + window: 1000, + initialState: { + successCount: 3, // Start with 3 successful executions + errorCount: 1, // Start with 1 error + rejectionCount: 2, // Start with 2 rejections + lastResult: 'initial-result', // Start with initial result + executionTimes: [Date.now() - 500], // Start with one execution timestamp + } +}) +``` + +### Subscribing to State Changes + +The store is reactive and supports subscriptions: + +```ts +const asyncLimiter = new AsyncRateLimiter(asyncFn, { limit: 5, window: 1000 }) + +// Subscribe to state changes +const unsubscribe = asyncLimiter.store.subscribe((state) => { + console.log('Success count:', state.successCount) + console.log('Error count:', state.errorCount) + console.log('Rejection count:', state.rejectionCount) + console.log('Currently executing:', state.isExecuting) +}) + +// Unsubscribe when done +unsubscribe() +``` + +### Available State Properties + +The `AsyncRateLimiterState` includes: + +- `successCount`: Number of successful function executions +- `errorCount`: Number of failed function executions +- `settleCount`: Total number of completed executions (success + error) +- `rejectionCount`: Number of rejected executions due to rate limiting +- `isExecuting`: Whether the async function is currently executing +- `lastResult`: Result from the most recent successful execution +- `executionTimes`: Array of timestamps when executions occurred (used for rate limiting calculations) + +### Helper Methods + +The async rate limiter provides helper methods that compute values based on the current state: + +```ts +const asyncLimiter = new AsyncRateLimiter(asyncFn, { limit: 5, window: 1000 }) + +// These methods use the current state to compute values +console.log(asyncLimiter.getRemainingInWindow()) // Number of calls remaining in current window +console.log(asyncLimiter.getMsUntilNextWindow()) // Milliseconds until next window +``` + +These methods are computed values that use the current state and don't need to be accessed through the store. + --- For core rate limiting concepts and synchronous rate limiting, see the [Rate Limiting Guide](../rate-limiting.md). \ No newline at end of file diff --git a/docs/guides/async-throttling.md b/docs/guides/async-throttling.md index 1973e095e..7132af341 100644 --- a/docs/guides/async-throttling.md +++ b/docs/guides/async-throttling.md @@ -59,7 +59,7 @@ The async throttler provides robust error handling capabilities: - When true (default if no onError handler), errors will be thrown - When false (default if onError handler provided), errors will be swallowed - Can be explicitly set to override these defaults -- You can track error counts using `getErrorCount()` and check execution state with `getIsExecuting()` +- You can track error counts using `throttler.store.state.errorCount` and check execution state with `throttler.store.state.isExecuting` - The throttler maintains its state and can continue to be used after an error occurs ### 3. Different Callbacks @@ -78,11 +78,11 @@ const asyncThrottler = new AsyncThrottler(async (value) => { wait: 500, onSuccess: (result, throttler) => { // Called after each successful execution - console.log('Async function executed', throttler.getSuccessCount()) + console.log('Async function executed', throttler.store.state.successCount) }, onSettled: (throttler) => { // Called after each execution attempt - console.log('Async function settled', throttler.getSettledCount()) + console.log('Async function settled', throttler.store.state.settleCount) }, onError: (error) => { // Called if the async function throws an error @@ -105,6 +105,99 @@ Just like the synchronous throttler, the async throttler supports dynamic option Each framework adapter provides hooks that build on top of the core async throttling functionality to integrate with the framework's state management system. Hooks like `createAsyncThrottler`, `useAsyncThrottledCallback`, or similar are available for each framework. +## State Management + +The `AsyncThrottler` class uses TanStack Store for reactive state management, providing real-time access to execution state, error tracking, and timing information. + +### Accessing State + +When using the `AsyncThrottler` class directly, access state via the `store.state` property: + +```ts +const asyncThrottler = new AsyncThrottler(asyncFn, { wait: 500 }) + +// Access current state +console.log(asyncThrottler.store.state.successCount) +``` + +### Framework Adapters + +When using framework adapters like React or Solid, the state is exposed directly as a reactive property: + +```ts +// React example +const asyncThrottler = useAsyncThrottler(asyncFn, { wait: 500 }) + +// Access state directly (reactive) +console.log(asyncThrottler.state.successCount) // Reactive value +console.log(asyncThrottler.state.isExecuting) // Reactive value +``` + +### Initial State + +You can provide initial state values when creating an async throttler: + +```ts +const asyncThrottler = new AsyncThrottler(asyncFn, { + wait: 500, + initialState: { + successCount: 3, // Start with 3 successful executions + errorCount: 1, // Start with 1 error + lastResult: 'initial-result', // Start with initial result + lastExecutionTime: Date.now() - 1000, // Set last execution to 1 second ago + } +}) +``` + +### Subscribing to State Changes + +The store is reactive and supports subscriptions: + +```ts +const asyncThrottler = new AsyncThrottler(asyncFn, { wait: 500 }) + +// Subscribe to state changes +const unsubscribe = asyncThrottler.store.subscribe((state) => { + console.log('Success count:', state.successCount) + console.log('Error count:', state.errorCount) + console.log('Currently executing:', state.isExecuting) + console.log('Last execution time:', state.lastExecutionTime) +}) + +// Unsubscribe when done +unsubscribe() +``` + +### Available State Properties + +The `AsyncThrottlerState` includes: + +- `successCount`: Number of successful function executions +- `errorCount`: Number of failed function executions +- `settleCount`: Total number of completed executions (success + error) +- `isExecuting`: Whether the async function is currently executing +- `isPending`: Whether the throttler is waiting for timeout to trigger execution +- `status`: Current execution status ('idle' | 'pending' | 'executing' | 'settled') +- `lastExecutionTime`: Timestamp of the last function execution (in milliseconds) +- `nextExecutionTime`: Timestamp when the next execution can occur (in milliseconds) +- `lastResult`: Result from the most recent successful execution +- `lastArgs`: Arguments from the most recent call to `maybeExecute` + +### Flushing Pending Executions + +The async throttler supports flushing pending executions to trigger them immediately: + +```ts +const asyncThrottler = new AsyncThrottler(asyncFn, { wait: 1000 }) + +asyncThrottler.maybeExecute('some-arg') +console.log(asyncThrottler.store.state.isPending) // true + +// Flush immediately instead of waiting +asyncThrottler.flush() +console.log(asyncThrottler.store.state.isPending) // false +``` + --- For core throttling concepts and synchronous throttling, see the [Throttling Guide](../throttling.md). \ No newline at end of file diff --git a/docs/guides/batching.md b/docs/guides/batching.md index c7b3a8a36..a4a033b95 100644 --- a/docs/guides/batching.md +++ b/docs/guides/batching.md @@ -138,14 +138,14 @@ batcher.addItem(item) // Add an item to the batch batcher.execute() // Manually process the current batch batcher.stop() // Pause batching batcher.start() // Resume batching -batcher.getSize() // Get current batch size -batcher.getIsEmpty() // Check if batch is empty -batcher.getIsRunning() // Check if batcher is running +batcher.store.state.size // Get current batch size +batcher.store.state.isEmpty // Check if batch is empty +batcher.store.state.isRunning // Check if batcher is running batcher.peekAllItems() // Get all items in the current batch -batcher.getBatchExecutionCount()// Number of batches processed -batcher.getItemExecutionCount() // Number of items processed +batcher.store.state.executionCount // Number of batches processed +batcher.store.state.totalItemsProcessed // Number of items processed batcher.setOptions(opts) // Update batcher options -batcher.getOptions() // Get current options +batcher.flush() // Flush pending batch immediately ``` ## Custom Batch Triggers @@ -180,11 +180,101 @@ console.log(options.maxSize) // 10 ## Performance Monitoring -Batcher provides methods to monitor its performance: +Batcher provides state properties to monitor its performance: ```ts -console.log(batcher.getBatchExecutionCount()) // Number of batches processed -console.log(batcher.getItemExecutionCount()) // Number of items processed +console.log(batcher.store.state.totalBatchesProcessed) +``` + +## State Management + +The `Batcher` class uses TanStack Store for reactive state management, providing real-time access to batch state, execution counts, and processing status. + +### Accessing State + +When using the `Batcher` class directly, access state via the `store.state` property: + +```ts +const batcher = new Batcher(processFn, { maxSize: 5, wait: 1000 }) + +// Access current state +console.log(batcher.store.state.executionCount) +``` + +### Framework Adapters + +When using framework adapters like React or Solid, the state is exposed directly as a reactive property: + +```ts +// React example +const batcher = useBatcher(processFn, { maxSize: 5, wait: 1000 }) + +// Access state directly (reactive) +console.log(batcher.state.executionCount) // Reactive value +console.log(batcher.state.size) // Reactive value +``` + +### Initial State + +You can provide initial state values when creating a batcher: + +```ts +const batcher = new Batcher(processFn, { + maxSize: 5, + wait: 1000, + initialState: { + executionCount: 2, // Start with 2 batches processed + totalItemsProcessed: 10, // Start with 10 items processed + isRunning: false, // Start paused + } +}) +``` + +### Subscribing to State Changes + +The store is reactive and supports subscriptions: + +```ts +const batcher = new Batcher(processFn, { maxSize: 5, wait: 1000 }) + +// Subscribe to state changes +const unsubscribe = batcher.store.subscribe((state) => { + console.log('Batch size:', state.size) + console.log('Items processed:', state.totalItemsProcessed) + console.log('Is running:', state.isRunning) +}) + +// Unsubscribe when done +unsubscribe() +``` + +### Available State Properties + +The `BatcherState` includes: + +- `executionCount`: Number of batch executions completed +- `totalItemsProcessed`: Total number of items processed across all batches +- `size`: Number of items currently in the batch queue +- `isEmpty`: Whether the batch has no items (items array is empty) +- `isPending`: Whether the batcher is waiting for timeout to trigger batch processing +- `isRunning`: Whether the batcher is active and will process items automatically +- `status`: Current processing status ('idle' | 'pending') +- `items`: Array of items currently queued for batch processing + +### Flushing Pending Batches + +The batcher supports flushing pending batches to trigger processing immediately: + +```ts +const batcher = new Batcher(processFn, { maxSize: 10, wait: 5000 }) + +batcher.addItem('item1') +batcher.addItem('item2') +console.log(batcher.store.state.isPending) // true + +// Flush immediately instead of waiting +batcher.flush() +console.log(batcher.store.state.isEmpty) // true (batch was processed) ``` ## Framework Adapters diff --git a/docs/guides/debouncing.md b/docs/guides/debouncing.md index 096b0d123..4ec6137d5 100644 --- a/docs/guides/debouncing.md +++ b/docs/guides/debouncing.md @@ -70,15 +70,19 @@ const searchDebouncer = new Debouncer( { wait: 500 } ) -// Get information about current state -console.log(searchDebouncer.getExecutionCount()) // Number of successful executions -console.log(searchDebouncer.getIsPending()) // Whether a call is pending +// Access current state via TanStack Store +console.log(searchDebouncer.store.state.executionCount) // Number of successful executions +console.log(searchDebouncer.store.state.isPending) // Whether a call is pending +console.log(searchDebouncer.store.state.status) // Current execution status // Update options dynamically searchDebouncer.setOptions({ wait: 1000 }) // Increase wait time // Cancel pending execution searchDebouncer.cancel() + +// Flush pending execution immediately +searchDebouncer.flush() ``` ### Leading and Trailing Executions @@ -122,7 +126,7 @@ The `enabled` option can also be a function that returns a boolean, allowing for const debouncer = new Debouncer(fn, { wait: 500, enabled: (debouncer) => { - return debouncer.getExecutionCount() < 10 // Disable after 10 executions + return debouncer.store.state.executionCount < 10 // Disable after 10 executions } }) ``` @@ -145,11 +149,11 @@ Several options in the Debouncer support dynamic values through callback functio const debouncer = new Debouncer(fn, { // Dynamic wait time based on execution count wait: (debouncer) => { - return debouncer.getExecutionCount() * 100 // Increase wait time with each execution + return debouncer.store.state.executionCount * 100 // Increase wait time with each execution }, // Dynamic enabled state based on execution count enabled: (debouncer) => { - return debouncer.getExecutionCount() < 10 // Disable after 10 executions + return debouncer.store.state.executionCount < 10 // Disable after 10 executions } }) ``` @@ -169,13 +173,97 @@ const debouncer = new Debouncer(fn, { wait: 500, onExecute: (debouncer) => { // Called after each successful execution - console.log('Function executed', debouncer.getExecutionCount()) + console.log('Function executed', debouncer.store.state.executionCount) } }) ``` The `onExecute` callback is called after each successful execution of the debounced function, making it useful for tracking executions, updating UI state, or performing cleanup operations. +## State Management + +The `Debouncer` class uses TanStack Store for reactive state management, providing real-time access to execution state and statistics. + +### Accessing State + +When using the `Debouncer` class directly, access state via the `store.state` property: + +```ts +const debouncer = new Debouncer(fn, { wait: 500 }) + +// Access current state +console.log(debouncer.store.state.isPending) +``` + +### Framework Adapters + +When using framework adapters like React or Solid, the state is exposed directly as a reactive property: + +```ts +// React example +const debouncer = useDebouncer(fn, { wait: 500 }) + +// Access state directly (reactive) +console.log(debouncer.state.executionCount) // Reactive value +console.log(debouncer.state.isPending) // Reactive value +``` + +### Initial State + +You can provide initial state values when creating a debouncer: + +```ts +const debouncer = new Debouncer(fn, { + wait: 500, + initialState: { + executionCount: 5, // Start with 5 executions + canLeadingExecute: false, // Start with leading execution disabled + } +}) +``` + +### Subscribing to State Changes + +The store is reactive and supports subscriptions: + +```ts +const debouncer = new Debouncer(fn, { wait: 500 }) + +// Subscribe to state changes +const unsubscribe = debouncer.store.subscribe((state) => { + console.log('Execution count:', state.executionCount) + console.log('Is pending:', state.isPending) +}) + +// Unsubscribe when done +unsubscribe() +``` + +### Available State Properties + +The `DebouncerState` includes: + +- `executionCount`: Number of completed function executions +- `isPending`: Whether the debouncer is waiting for timeout to trigger execution +- `status`: Current execution status ('idle' | 'pending') +- `canLeadingExecute`: Whether leading edge execution is allowed +- `lastArgs`: Arguments from the most recent call to `maybeExecute` + +### Flushing Pending Executions + +The debouncer supports flushing pending executions to trigger them immediately: + +```ts +const debouncer = new Debouncer(fn, { wait: 1000 }) + +debouncer.maybeExecute('some-arg') +console.log(debouncer.store.state.isPending) // true + +// Flush immediately instead of waiting +debouncer.flush() +console.log(debouncer.store.state.isPending) // false +``` + --- For asynchronous debouncing (e.g., API calls, async operations), see the [Async Debouncing Guide](../async-debouncing.md). \ No newline at end of file diff --git a/docs/guides/queuing.md b/docs/guides/queuing.md index a91dd6443..342ed6414 100644 --- a/docs/guides/queuing.md +++ b/docs/guides/queuing.md @@ -210,15 +210,16 @@ The Queuer provides several helpful methods for queue management: ```ts // Queue inspection queue.peekNextItem() // View next item without removing it -queue.getSize() // Get current queue size -queue.getIsEmpty() // Check if queue is empty -queue.getIsFull() // Check if queue has reached maxSize -queue.peekAllItems() // Get copy of all queued items +queue.store.state.size // Get current queue size +queue.store.state.isEmpty // Check if queue is empty +queue.store.state.isFull // Check if queue has reached maxSize +queue.peekAllItems() // Get copy of all queued items // Queue manipulation -queue.clear() // Remove all items -queue.reset() // Reset to initial state -queue.getExecutionCount() // Get number of processed items +queue.clear() // Remove all items +queue.reset() // Reset to initial state +queue.store.state.executionCount // Get number of processed items +queue.flush() // Flush all pending items immediately // Event handling (use the onItemsChange option, not a method) // Example: @@ -271,7 +272,7 @@ const queue = new Queuer( ) // Check expiration statistics -console.log(queue.getExpirationCount()) // Number of items that have expired +console.log(queue.store.state.expirationCount) // Number of items that have expired ``` Expiration features are particularly useful for: @@ -302,7 +303,7 @@ queue.addItem(1) // Accepted queue.addItem(2) // Accepted queue.addItem(3) // Rejected, triggers onReject callback -console.log(queue.getRejectionCount()) // 1 +console.log(queue.store.state.rejectionCount) // 1 ``` ### Initial Items @@ -326,7 +327,7 @@ const queue = new Queuer( ### Dynamic Configuration -The Queuer's options can be modified after creation using `setOptions()` and retrieved using `getOptions()`. Additionally, several options support dynamic values through callback functions: +The Queuer's options can be modified after creation using `setOptions()`. Additionally, several options support dynamic values through callback functions: ```ts const queue = new Queuer( @@ -346,9 +347,9 @@ queue.setOptions({ started: true // Start processing }) -// Get current configuration -const options = queue.getOptions() -console.log(options.wait) // 500 +// Access current state +console.log(queue.store.state.size) // Current queue size +console.log(queue.store.state.isRunning) // Whether queue is running ``` ### Dynamic Options @@ -364,7 +365,7 @@ const queue = new Queuer( { // Dynamic wait time based on queue size wait: (queuer) => { - return queuer.getSize() > 10 ? 2000 : 1000 + return queuer.store.state.size > 10 ? 2000 : 1000 } } ) @@ -377,7 +378,7 @@ This allows for sophisticated queue behavior that adapts to runtime conditions. ### Performance Monitoring -The Queuer provides methods to monitor its performance: +The Queuer provides state properties to monitor its performance: ```ts const queue = new Queuer( @@ -392,8 +393,110 @@ queue.addItem(1) queue.addItem(2) queue.addItem(3) -console.log(queue.getExecutionCount()) // Number of items processed -console.log(queue.getRejectionCount()) // Number of items rejected +console.log(queue.store.state.rejectionCount) // Number of items processed +``` + +## State Management + +The `Queuer` class uses TanStack Store for reactive state management, providing real-time access to queue state, processing statistics, and item tracking. + +### Accessing State + +When using the `Queuer` class directly, access state via the `store.state` property: + +```ts +const queue = new Queuer(processFn, { wait: 1000, maxSize: 10 }) + +// Access current state +console.log(queue.store.state.isFull) +``` + +### Framework Adapters + +When using framework adapters like React or Solid, the state is exposed directly as a reactive property: + +```ts +// React example +const queue = useQueuer(processFn, { wait: 1000, maxSize: 10 }) + +// Access state directly (reactive) +console.log(queue.state.executionCount) // Reactive value +console.log(queue.state.size) // Reactive value +``` + +### Initial State + +You can provide initial state values when creating a queuer: + +```ts +const queue = new Queuer(processFn, { + wait: 1000, + maxSize: 10, + initialState: { + executionCount: 5, // Start with 5 items processed + rejectionCount: 2, // Start with 2 rejections + isRunning: false, // Start paused + } +}) +``` + +### Subscribing to State Changes + +The store is reactive and supports subscriptions: + +```ts +const queue = new Queuer(processFn, { wait: 1000, maxSize: 10 }) + +// Subscribe to state changes +const unsubscribe = queue.store.subscribe((state) => { + console.log('Queue size:', state.size) + console.log('Items processed:', state.executionCount) + console.log('Is running:', state.isRunning) + console.log('Is empty:', state.isEmpty) +}) + +// Unsubscribe when done +unsubscribe() +``` + +### Available State Properties + +The `QueuerState` includes: + +- `executionCount`: Number of items processed +- `rejectionCount`: Number of items rejected due to maxSize +- `expirationCount`: Number of items expired +- `size`: Current number of items in the queue +- `isEmpty`: Whether the queue has no items +- `isFull`: Whether the queue has reached maxSize +- `isIdle`: Whether the queue is not processing any items +- `isRunning`: Whether the queue is active and processing items +- `status`: Current processing status ('idle' | 'running' | 'stopped') +- `items`: Array of items currently in the queue +- `itemTimestamps`: Array of timestamps when items were added +- `pendingTick`: Whether the queue has a pending timeout for processing + +### Flushing Queue Items + +The queuer supports flushing items to process them immediately: + +```ts +const queue = new Queuer(processFn, { wait: 5000 }) + +queue.addItem('item1') +queue.addItem('item2') +console.log(queue.store.state.size) // 2 + +// Flush all items immediately instead of waiting +queue.flush() +console.log(queue.store.state.size) // 0 (items were processed) + +// Or flush a specific number of items +queue.addItem('item3') +queue.addItem('item4') +queue.addItem('item5') +queue.flush(2) // Process only 2 items +console.log(queue.store.state.size) // 1 (one item remaining) ``` ### Asynchronous Queuing diff --git a/docs/guides/rate-limiting.md b/docs/guides/rate-limiting.md index e6673189b..3e7dd41c9 100644 --- a/docs/guides/rate-limiting.md +++ b/docs/guides/rate-limiting.md @@ -112,7 +112,7 @@ const limiter = new RateLimiter( limit: 5, window: 60 * 1000, onExecute: (rateLimiter) => { - console.log('Function executed', rateLimiter.getExecutionCount()) + console.log('Function executed', rateLimiter.store.state.executionCount) }, onReject: (rateLimiter) => { console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`) @@ -120,10 +120,10 @@ const limiter = new RateLimiter( } ) -// Get information about current state +// Access current state via TanStack Store console.log(limiter.getRemainingInWindow()) // Number of calls remaining in current window -console.log(limiter.getExecutionCount()) // Total number of successful executions -console.log(limiter.getRejectionCount()) // Total number of rejected executions +console.log(limiter.store.state.executionCount) // Total number of successful executions +console.log(limiter.store.state.rejectionCount) // Total number of rejected executions // Attempt to execute (returns boolean indicating success) limiter.maybeExecute('user-1') @@ -158,7 +158,7 @@ const limiter = new RateLimiter(fn, { limit: 5, window: 1000, enabled: (limiter) => { - return limiter.getExecutionCount() < 100 // Disable after 100 executions + return limiter.store.state.executionCount < 100 // Disable after 100 executions } }) ``` @@ -173,15 +173,15 @@ Several options in the RateLimiter support dynamic values through callback funct const limiter = new RateLimiter(fn, { // Dynamic limit based on execution count limit: (limiter) => { - return Math.max(1, 10 - limiter.getExecutionCount()) // Decrease limit with each execution + return Math.max(1, 10 - limiter.store.state.executionCount) // Decrease limit with each execution }, // Dynamic window based on execution count window: (limiter) => { - return limiter.getExecutionCount() * 1000 // Increase window with each execution + return limiter.store.state.executionCount * 1000 // Increase window with each execution }, // Dynamic enabled state based on execution count enabled: (limiter) => { - return limiter.getExecutionCount() < 100 // Disable after 100 executions + return limiter.store.state.executionCount < 100 // Disable after 100 executions } }) ``` @@ -203,7 +203,7 @@ const limiter = new RateLimiter(fn, { window: 1000, onExecute: (rateLimiter) => { // Called after each successful execution - console.log('Function executed', rateLimiter.getExecutionCount()) + console.log('Function executed', rateLimiter.store.state.executionCount) }, onReject: (rateLimiter) => { // Called when an execution is rejected @@ -214,6 +214,90 @@ const limiter = new RateLimiter(fn, { The `onExecute` callback is called after each successful execution of the rate-limited function, while the `onReject` callback is called when an execution is rejected due to rate limiting. These callbacks are useful for tracking executions, updating UI state, or providing feedback to users. +## State Management + +The `RateLimiter` class uses TanStack Store for reactive state management, providing real-time access to execution counts and rejection statistics. + +### Accessing State + +When using the `RateLimiter` class directly, access state via the `store.state` property: + +```ts +const limiter = new RateLimiter(fn, { limit: 5, window: 1000 }) + +// Access current state +console.log(limiter.store.state.rejectionCount) +``` + +### Framework Adapters + +When using framework adapters like React or Solid, the state is exposed directly as a reactive property: + +```ts +// React example +const limiter = useRateLimiter(fn, { limit: 5, window: 1000 }) + +// Access state directly (reactive) +console.log(limiter.state.executionCount) // Reactive value +console.log(limiter.state.rejectionCount) // Reactive value +``` + +### Initial State + +You can provide initial state values when creating a rate limiter: + +```ts +const limiter = new RateLimiter(fn, { + limit: 5, + window: 1000, + initialState: { + executionCount: 2, // Start with 2 executions + rejectionCount: 1, // Start with 1 rejection + executionTimes: [Date.now() - 500], // Start with one execution timestamp + } +}) +``` + +### Subscribing to State Changes + +The store is reactive and supports subscriptions: + +```ts +const limiter = new RateLimiter(fn, { limit: 5, window: 1000 }) + +// Subscribe to state changes +const unsubscribe = limiter.store.subscribe((state) => { + console.log('Execution count:', state.executionCount) + console.log('Rejection count:', state.rejectionCount) + console.log('Execution times:', state.executionTimes) +}) + +// Unsubscribe when done +unsubscribe() +``` + +### Available State Properties + +The `RateLimiterState` includes: + +- `executionCount`: Number of successful function executions +- `rejectionCount`: Number of rejected executions due to rate limiting +- `executionTimes`: Array of timestamps when executions occurred (used for rate limiting calculations) + +### Helper Methods + +The rate limiter provides helper methods that compute values based on the current state: + +```ts +const limiter = new RateLimiter(fn, { limit: 5, window: 1000 }) + +// These methods use the current state to compute values +console.log(limiter.getRemainingInWindow()) // Number of calls remaining in current window +console.log(limiter.getMsUntilNextWindow()) // Milliseconds until next window +``` + +These methods are computed values that use the current state and don't need to be accessed through the store. + --- For asynchronous rate limiting (e.g., API calls, async operations), see the [Async Rate Limiting Guide](../async-rate-limiting.md). diff --git a/docs/guides/throttling.md b/docs/guides/throttling.md index 506926551..1d9ea2648 100644 --- a/docs/guides/throttling.md +++ b/docs/guides/throttling.md @@ -75,12 +75,17 @@ const updateThrottler = new Throttler( { wait: 200 } ) -// Get information about execution state -console.log(updateThrottler.getExecutionCount()) // Number of successful executions -console.log(updateThrottler.getLastExecutionTime()) // Timestamp of last execution +// Access current state via TanStack Store +console.log(updateThrottler.store.state.executionCount) // Number of successful executions +console.log(updateThrottler.store.state.lastExecutionTime) // Timestamp of last execution +console.log(updateThrottler.store.state.isPending) // Whether execution is pending +console.log(updateThrottler.store.state.status) // Current execution status // Cancel any pending execution updateThrottler.cancel() + +// Flush pending execution immediately +updateThrottler.flush() ``` ### Leading and Trailing Executions @@ -120,7 +125,7 @@ The `enabled` option can also be a function that returns a boolean, allowing for const throttler = new Throttler(fn, { wait: 200, enabled: (throttler) => { - return throttler.getExecutionCount() < 50 // Disable after 50 executions + return throttler.store.state.executionCount < 50 // Disable after 50 executions } }) ``` @@ -135,11 +140,11 @@ Several options in the Throttler support dynamic values through callback functio const throttler = new Throttler(fn, { // Dynamic wait time based on execution count wait: (throttler) => { - return throttler.getExecutionCount() * 100 // Increase wait time with each execution + return throttler.store.state.executionCount * 100 // Increase wait time with each execution }, // Dynamic enabled state based on execution count enabled: (throttler) => { - return throttler.getExecutionCount() < 50 // Disable after 50 executions + return throttler.store.state.executionCount < 50 // Disable after 50 executions } }) ``` @@ -159,13 +164,99 @@ const throttler = new Throttler(fn, { wait: 200, onExecute: (throttler) => { // Called after each successful execution - console.log('Function executed', throttler.getExecutionCount()) + console.log('Function executed', throttler.store.state.executionCount) } }) ``` The `onExecute` callback is called after each successful execution of the throttled function, making it useful for tracking executions, updating UI state, or performing cleanup operations. +## State Management + +The `Throttler` class uses TanStack Store for reactive state management, providing real-time access to execution state and timing information. + +### Accessing State + +When using the `Throttler` class directly, access state via the `store.state` property: + +```ts +const throttler = new Throttler(fn, { wait: 200 }) + +// Access current state +console.log(throttler.store.state.isPending) +``` + +### Framework Adapters + +When using framework adapters like React or Solid, the state is exposed directly as a reactive property: + +```ts +// React example +const throttler = useThrottler(fn, { wait: 200 }) + +// Access state directly (reactive) +console.log(throttler.state.executionCount) // Reactive value +console.log(throttler.state.isPending) // Reactive value +``` + +### Initial State + +You can provide initial state values when creating a throttler: + +```ts +const throttler = new Throttler(fn, { + wait: 200, + initialState: { + executionCount: 10, // Start with 10 executions + lastExecutionTime: Date.now() - 1000, // Set last execution to 1 second ago + } +}) +``` + +### Subscribing to State Changes + +The store is reactive and supports subscriptions: + +```ts +const throttler = new Throttler(fn, { wait: 200 }) + +// Subscribe to state changes +const unsubscribe = throttler.store.subscribe((state) => { + console.log('Execution count:', state.executionCount) + console.log('Last execution time:', state.lastExecutionTime) + console.log('Is pending:', state.isPending) +}) + +// Unsubscribe when done +unsubscribe() +``` + +### Available State Properties + +The `ThrottlerState` includes: + +- `executionCount`: Number of completed function executions +- `lastExecutionTime`: Timestamp of the last function execution (in milliseconds) +- `nextExecutionTime`: Timestamp when the next execution can occur (in milliseconds) +- `isPending`: Whether the throttler is waiting for timeout to trigger execution +- `status`: Current execution status ('idle' | 'pending') +- `lastArgs`: Arguments from the most recent call to `maybeExecute` + +### Flushing Pending Executions + +The throttler supports flushing pending executions to trigger them immediately: + +```ts +const throttler = new Throttler(fn, { wait: 1000 }) + +throttler.maybeExecute('some-arg') +console.log(throttler.store.state.isPending) // true + +// Flush immediately instead of waiting +throttler.flush() +console.log(throttler.store.state.isPending) // false +``` + --- For asynchronous throttling (e.g., API calls, async operations), see the [Async Throttling Guide](../async-throttling.md). \ No newline at end of file diff --git a/docs/overview.md b/docs/overview.md index 42ce8925c..aef9ea0e1 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -17,7 +17,7 @@ Many of the ideas (and code) for TanStack Pacer are not new. In fact, many of th ## Features > [!NOTE] -> TanStack Pacer is currently a client-side only library +> TanStack Pacer is currently mostly a client-side only library, but it is being designed to be able to potentially be used on the server-side as well. - **Debouncing** - Delay execution until after a period of inactivity for when you only care about the last execution in a sequence. @@ -43,9 +43,10 @@ Many of the ideas (and code) for TanStack Pacer are not new. In fact, many of th - **Async or Sync Variations** - Choose between synchronous and asynchronous versions of each utility - Optional error, success, and settled handling for async variations -- **Comparison Utilities** - - Perform deep equality checks between values - - Create custom comparison logic for specific needs +- **State Management** + - Uses TanStack Store under the hood for state management with fine-grained reactivity + - Easily integrate with your own state management library of choice + - Persist state to local or session storage for some utilities like rate limiting and queuing - **Convenient Hooks** - Reduce boilerplate code with pre-built hooks like `useDebouncedCallback`, `useThrottledValue`, and `useQueuedState`, and more. - Multiple layers of abstraction to choose from depending on your use case. diff --git a/docs/reference/classes/asyncbatcher.md b/docs/reference/classes/asyncbatcher.md new file mode 100644 index 000000000..0f6ef9d81 --- /dev/null +++ b/docs/reference/classes/asyncbatcher.md @@ -0,0 +1,274 @@ +--- +id: AsyncBatcher +title: AsyncBatcher +--- + + + +# Class: AsyncBatcher\ + +Defined in: [async-batcher.ts:229](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L229) + +A class that collects items and processes them in batches asynchronously. + +This is the async version of the Batcher class. Unlike the sync version, this async batcher: +- Handles promises and returns results from batch executions +- Provides error handling with configurable error behavior +- Tracks success, error, and settle counts separately +- Has state tracking for when batches are executing +- Returns the result of the batch function execution + +Batching is a technique for grouping multiple operations together to be processed as a single unit. + +The AsyncBatcher provides a flexible way to implement async batching with configurable: +- Maximum batch size (number of items per batch) +- Time-based batching (process after X milliseconds) +- Custom batch processing logic via getShouldExecute +- Event callbacks for monitoring batch operations +- Error handling for failed batch operations + +Error Handling: +- If an `onError` handler is provided, it will be called with the error and batcher instance +- If `throwOnError` is true (default when no onError handler is provided), the error will be thrown +- If `throwOnError` is false (default when onError handler is provided), the error will be swallowed +- Both onError and throwOnError can be used together - the handler will be called before any error is thrown +- The error state can be checked using the AsyncBatcher instance + +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the async batcher +- Use `onSuccess` callback to react to successful batch execution and implement custom logic +- Use `onError` callback to react to batch execution errors and implement custom error handling +- Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic +- Use `onExecute` callback to react to batch execution and implement custom logic +- Use `onItemsChange` callback to react to items being added or removed from the batcher +- The state includes total items processed, success/error counts, and execution status +- State can be accessed via `asyncBatcher.store.state` when using the class directly +- When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state` + +## Example + +```ts +const batcher = new AsyncBatcher( + async (items) => { + const result = await processItems(items); + console.log('Processing batch:', items); + return result; + }, + { + maxSize: 5, + wait: 2000, + onSuccess: (result) => console.log('Batch succeeded:', result), + onError: (error) => console.error('Batch failed:', error) + } +); + +batcher.addItem(1); +batcher.addItem(2); +// After 2 seconds or when 5 items are added, whichever comes first, +// the batch will be processed and the result will be available +// batcher.execute() // manually trigger a batch +``` + +## Type Parameters + +• **TValue** + +## Constructors + +### new AsyncBatcher() + +```ts +new AsyncBatcher(fn, initialOptions): AsyncBatcher +``` + +Defined in: [async-batcher.ts:236](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L236) + +#### Parameters + +##### fn + +(`items`) => `Promise`\<`any`\> + +##### initialOptions + +[`AsyncBatcherOptions`](../../interfaces/asyncbatcheroptions.md)\<`TValue`\> + +#### Returns + +[`AsyncBatcher`](../asyncbatcher.md)\<`TValue`\> + +## Properties + +### options + +```ts +options: AsyncBatcherOptionsWithOptionalCallbacks; +``` + +Defined in: [async-batcher.ts:233](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L233) + +*** + +### store + +```ts +readonly store: Store>>; +``` + +Defined in: [async-batcher.ts:230](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L230) + +## Methods + +### addItem() + +```ts +addItem(item): void +``` + +Defined in: [async-batcher.ts:287](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L287) + +Adds an item to the async batcher +If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed + +#### Parameters + +##### item + +`TValue` + +#### Returns + +`void` + +*** + +### clear() + +```ts +clear(): void +``` + +Defined in: [async-batcher.ts:407](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L407) + +Removes all items from the async batcher + +#### Returns + +`void` + +*** + +### flush() + +```ts +flush(): Promise +``` + +Defined in: [async-batcher.ts:363](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L363) + +Processes the current batch of items immediately + +#### Returns + +`Promise`\<`any`\> + +*** + +### peekAllItems() + +```ts +peekAllItems(): TValue[] +``` + +Defined in: [async-batcher.ts:389](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L389) + +Returns a copy of all items in the async batcher + +#### Returns + +`TValue`[] + +*** + +### peekFailedItems() + +```ts +peekFailedItems(): TValue[] +``` + +Defined in: [async-batcher.ts:393](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L393) + +#### Returns + +`TValue`[] + +*** + +### reset() + +```ts +reset(): void +``` + +Defined in: [async-batcher.ts:414](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L414) + +Resets the async batcher state to its default values + +#### Returns + +`void` + +*** + +### setOptions() + +```ts +setOptions(newOptions): void +``` + +Defined in: [async-batcher.ts:251](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L251) + +Updates the async batcher options + +#### Parameters + +##### newOptions + +`Partial`\<[`AsyncBatcherOptions`](../../interfaces/asyncbatcheroptions.md)\<`TValue`\>\> + +#### Returns + +`void` + +*** + +### start() + +```ts +start(): void +``` + +Defined in: [async-batcher.ts:379](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L379) + +Starts the async batcher and processes any pending items + +#### Returns + +`void` + +*** + +### stop() + +```ts +stop(): void +``` + +Defined in: [async-batcher.ts:371](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L371) + +Stops the async batcher from processing batches + +#### Returns + +`void` diff --git a/docs/reference/classes/asyncdebouncer.md b/docs/reference/classes/asyncdebouncer.md index c69fa50d5..7cf399806 100644 --- a/docs/reference/classes/asyncdebouncer.md +++ b/docs/reference/classes/asyncdebouncer.md @@ -7,7 +7,7 @@ title: AsyncDebouncer # Class: AsyncDebouncer\ -Defined in: [async-debouncer.ts:101](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L101) +Defined in: [async-debouncer.ts:169](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L169) A class that creates an async debounced function. @@ -23,10 +23,18 @@ making it ideal for API calls and other async operations where you want the resu instead of setting the result on a state variable from within the debounced function. Error Handling: -- If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller. -- If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown. -- The error count can be tracked using `getErrorCount()`. -- The debouncer maintains its state and can continue to be used after an error occurs. +- If an `onError` handler is provided, it will be called with the error and debouncer instance +- If `throwOnError` is true (default when no onError handler is provided), the error will be thrown +- If `throwOnError` is false (default when onError handler is provided), the error will be swallowed +- Both onError and throwOnError can be used together - the handler will be called before any error is thrown +- The error state can be checked using the underlying store + +State Management: +- The debouncer uses a reactive store for state management +- Use `initialState` to provide initial state values when creating the async debouncer +- The state includes canLeadingExecute, error count, execution status, and success/settle counts +- State can be accessed via the `store` property and its `state` getter +- The store is reactive and will notify subscribers of state changes ## Example @@ -58,7 +66,7 @@ const results = await asyncDebouncer.maybeExecute(inputElement.value); new AsyncDebouncer(fn, initialOptions): AsyncDebouncer ``` -Defined in: [async-debouncer.ts:117](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L117) +Defined in: [async-debouncer.ts:180](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L180) #### Parameters @@ -74,165 +82,57 @@ Defined in: [async-debouncer.ts:117](https://github.com/TanStack/pacer/blob/main [`AsyncDebouncer`](../asyncdebouncer.md)\<`TFn`\> -## Methods - -### cancel() - -```ts -cancel(): void -``` - -Defined in: [async-debouncer.ts:259](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L259) - -Cancels any pending execution or aborts any execution in progress - -#### Returns - -`void` - -*** - -### getEnabled() - -```ts -getEnabled(): boolean -``` - -Defined in: [async-debouncer.ts:150](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L150) - -Returns the current debouncer enabled state - -#### Returns - -`boolean` +## Properties -*** - -### getErrorCount() +### options ```ts -getErrorCount(): number +options: AsyncDebouncerOptions; ``` -Defined in: [async-debouncer.ts:288](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L288) - -Returns the number of times the function has errored - -#### Returns - -`number` +Defined in: [async-debouncer.ts:173](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L173) *** -### getIsExecuting() +### store ```ts -getIsExecuting(): boolean +readonly store: Store>>; ``` -Defined in: [async-debouncer.ts:302](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L302) - -Returns `true` if there is currently an execution in progress - -#### Returns - -`boolean` - -*** - -### getIsPending() +Defined in: [async-debouncer.ts:170](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L170) -```ts -getIsPending(): boolean -``` - -Defined in: [async-debouncer.ts:295](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L295) - -Returns `true` if there is a pending execution queued up for trailing execution - -#### Returns - -`boolean` - -*** - -### getLastResult() - -```ts -getLastResult(): undefined | ReturnType -``` - -Defined in: [async-debouncer.ts:267](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L267) - -Returns the last result of the debounced function - -#### Returns - -`undefined` \| `ReturnType`\<`TFn`\> - -*** - -### getOptions() - -```ts -getOptions(): AsyncDebouncerOptions -``` - -Defined in: [async-debouncer.ts:143](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L143) - -Returns the current debouncer options - -#### Returns - -[`AsyncDebouncerOptions`](../../interfaces/asyncdebounceroptions.md)\<`TFn`\> - -*** - -### getSettleCount() - -```ts -getSettleCount(): number -``` - -Defined in: [async-debouncer.ts:281](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L281) - -Returns the number of times the function has settled (completed or errored) - -#### Returns - -`number` - -*** +## Methods -### getSuccessCount() +### cancel() ```ts -getSuccessCount(): number +cancel(): void ``` -Defined in: [async-debouncer.ts:274](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L274) +Defined in: [async-debouncer.ts:363](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L363) -Returns the number of times the function has been executed successfully +Cancels any pending execution or aborts any execution in progress #### Returns -`number` +`void` *** -### getWait() +### flush() ```ts -getWait(): number +flush(): void ``` -Defined in: [async-debouncer.ts:157](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L157) +Defined in: [async-debouncer.ts:325](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L325) -Returns the current debouncer wait state +Processes the current pending execution immediately #### Returns -`number` +`void` *** @@ -242,7 +142,7 @@ Returns the current debouncer wait state maybeExecute(...args): Promise> ``` -Defined in: [async-debouncer.ts:175](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L175) +Defined in: [async-debouncer.ts:254](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L254) Attempts to execute the debounced function. If a call is already in progress, it will be queued. @@ -272,15 +172,31 @@ The error from the debounced function if no onError handler is configured *** +### reset() + +```ts +reset(): void +``` + +Defined in: [async-debouncer.ts:372](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L372) + +Resets the debouncer state to its default values + +#### Returns + +`void` + +*** + ### setOptions() ```ts setOptions(newOptions): void ``` -Defined in: [async-debouncer.ts:131](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L131) +Defined in: [async-debouncer.ts:195](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L195) -Updates the debouncer options +Updates the async debouncer options #### Parameters diff --git a/docs/reference/classes/asyncqueuer.md b/docs/reference/classes/asyncqueuer.md index b1170cdeb..91dffb45a 100644 --- a/docs/reference/classes/asyncqueuer.md +++ b/docs/reference/classes/asyncqueuer.md @@ -7,7 +7,7 @@ title: AsyncQueuer # Class: AsyncQueuer\ -Defined in: [async-queuer.ts:157](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L157) +Defined in: [async-queuer.ts:259](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L259) A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration. @@ -30,6 +30,19 @@ Error Handling: - Both onError and throwOnError can be used together; the handler will be called before any error is thrown - The error state can be checked using the AsyncQueuer instance +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the async queuer +- Use `onSuccess` callback to react to successful task execution and implement custom logic +- Use `onError` callback to react to task execution errors and implement custom error handling +- Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic +- Use `onItemsChange` callback to react to items being added or removed from the queue +- Use `onExpire` callback to react to items expiring and implement custom logic +- Use `onReject` callback to react to items being rejected when the queue is full +- The state includes error count, expiration count, rejection count, running status, and success/settle counts +- State can be accessed via `asyncQueuer.store.state` when using the class directly +- When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state` + Example usage: ```ts const asyncQueuer = new AsyncQueuer(async (item) => { @@ -58,22 +71,42 @@ asyncQueuer.start(); new AsyncQueuer(fn, initialOptions): AsyncQueuer ``` -Defined in: [async-queuer.ts:171](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L171) +Defined in: [async-queuer.ts:266](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L266) #### Parameters ##### fn -(`value`) => `Promise`\<`any`\> +(`item`) => `Promise`\<`any`\> ##### initialOptions -[`AsyncQueuerOptions`](../../interfaces/asyncqueueroptions.md)\<`TValue`\> +[`AsyncQueuerOptions`](../../interfaces/asyncqueueroptions.md)\<`TValue`\> = `{}` #### Returns [`AsyncQueuer`](../asyncqueuer.md)\<`TValue`\> +## Properties + +### options + +```ts +options: AsyncQueuerOptions; +``` + +Defined in: [async-queuer.ts:263](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L263) + +*** + +### store + +```ts +readonly store: Store>>; +``` + +Defined in: [async-queuer.ts:260](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L260) + ## Methods ### addItem() @@ -82,10 +115,10 @@ Defined in: [async-queuer.ts:171](https://github.com/TanStack/pacer/blob/main/pa addItem( item, position, - runOnItemsChange): void + runOnItemsChange): boolean ``` -Defined in: [async-queuer.ts:312](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L312) +Defined in: [async-queuer.ts:400](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L400) 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. @@ -94,7 +127,7 @@ Items can be inserted based on priority or at the front/back depending on config ##### item -`TValue` & `object` +`TValue` ##### position @@ -106,7 +139,7 @@ Items can be inserted based on priority or at the front/back depending on config #### Returns -`void` +`boolean` #### Example @@ -123,7 +156,7 @@ queuer.addItem('task2', 'front'); clear(): void ``` -Defined in: [async-queuer.ts:282](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L282) +Defined in: [async-queuer.ts:686](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L686) Removes all pending items from the queue. Does not affect active tasks. @@ -139,7 +172,7 @@ Removes all pending items from the queue. Does not affect active tasks. execute(position?): Promise ``` -Defined in: [async-queuer.ts:410](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L410) +Defined in: [async-queuer.ts:520](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L520) Removes and returns the next item from the queue and executes the task function with it. @@ -163,116 +196,30 @@ queuer.execute('back'); *** -### getConcurrency() - -```ts -getConcurrency(): number -``` - -Defined in: [async-queuer.ts:215](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L215) - -Returns the current concurrency limit for processing items. -If a function is provided, it is called with the queuer instance. - -#### Returns - -`number` - -*** - -### getErrorCount() - -```ts -getErrorCount(): number -``` - -Defined in: [async-queuer.ts:552](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L552) - -Returns the number of items that have failed processing. - -#### Returns - -`number` - -*** - -### getExpirationCount() - -```ts -getExpirationCount(): number -``` - -Defined in: [async-queuer.ts:587](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L587) - -Returns the number of items that have expired and been removed from the queue. - -#### Returns - -`number` - -*** - -### getIsEmpty() +### flush() ```ts -getIsEmpty(): boolean +flush(numberOfItems, position?): void ``` -Defined in: [async-queuer.ts:503](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L503) +Defined in: [async-queuer.ts:555](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L555) -Returns true if the queue is empty (no pending items). - -#### Returns - -`boolean` - -*** - -### getIsFull() - -```ts -getIsFull(): boolean -``` +Processes a specified number of items to execute immediately with no wait time +If no numberOfItems is provided, all items will be processed -Defined in: [async-queuer.ts:510](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L510) - -Returns true if the queue is full (reached maxSize). - -#### Returns - -`boolean` - -*** - -### getIsIdle() - -```ts -getIsIdle(): boolean -``` - -Defined in: [async-queuer.ts:580](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L580) - -Returns true if the queuer is running but has no items to process and no active tasks. - -#### Returns - -`boolean` - -*** +#### Parameters -### getIsRunning() +##### numberOfItems -```ts -getIsRunning(): boolean -``` +`number` = `...` -Defined in: [async-queuer.ts:573](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L573) +##### position? -Returns true if the queuer is currently running (processing items). +[`QueuePosition`](../../type-aliases/queueposition.md) #### Returns -`boolean` +`void` *** @@ -282,7 +229,7 @@ Returns true if the queuer is currently running (processing items). getNextItem(position): undefined | TValue ``` -Defined in: [async-queuer.ts:380](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L380) +Defined in: [async-queuer.ts:479](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L479) 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. @@ -308,110 +255,13 @@ queuer.getNextItem('back'); *** -### getOptions() - -```ts -getOptions(): AsyncQueuerOptions -``` - -Defined in: [async-queuer.ts:199](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L199) - -Returns the current queuer options, including defaults and any overrides. - -#### Returns - -[`AsyncQueuerOptions`](../../interfaces/asyncqueueroptions.md)\<`TValue`\> - -*** - -### getRejectionCount() - -```ts -getRejectionCount(): number -``` - -Defined in: [async-queuer.ts:566](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L566) - -Returns the number of items that have been rejected from being added to the queue. - -#### Returns - -`number` - -*** - -### getSettledCount() - -```ts -getSettledCount(): number -``` - -Defined in: [async-queuer.ts:559](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L559) - -Returns the number of items that have completed processing (success or error). - -#### Returns - -`number` - -*** - -### getSize() - -```ts -getSize(): number -``` - -Defined in: [async-queuer.ts:517](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L517) - -Returns the number of pending items in the queue. - -#### Returns - -`number` - -*** - -### getSuccessCount() - -```ts -getSuccessCount(): number -``` - -Defined in: [async-queuer.ts:545](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L545) - -Returns the number of items that have been successfully processed. - -#### Returns - -`number` - -*** - -### getWait() - -```ts -getWait(): number -``` - -Defined in: [async-queuer.ts:207](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L207) - -Returns the current wait time (in milliseconds) between processing items. -If a function is provided, it is called with the queuer instance. - -#### Returns - -`number` - -*** - ### peekActiveItems() ```ts peekActiveItems(): TValue[] ``` -Defined in: [async-queuer.ts:531](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L531) +Defined in: [async-queuer.ts:649](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L649) Returns the items currently being processed (active tasks). @@ -427,7 +277,7 @@ Returns the items currently being processed (active tasks). peekAllItems(): TValue[] ``` -Defined in: [async-queuer.ts:524](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L524) +Defined in: [async-queuer.ts:642](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L642) Returns a copy of all items in the queue, including active and pending items. @@ -443,7 +293,7 @@ Returns a copy of all items in the queue, including active and pending items. peekNextItem(position): undefined | TValue ``` -Defined in: [async-queuer.ts:493](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L493) +Defined in: [async-queuer.ts:632](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L632) Returns the next item in the queue without removing it. @@ -472,7 +322,7 @@ queuer.peekNextItem('back'); // back peekPendingItems(): TValue[] ``` -Defined in: [async-queuer.ts:538](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L538) +Defined in: [async-queuer.ts:656](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L656) Returns the items waiting to be processed (pending tasks). @@ -485,19 +335,12 @@ Returns the items waiting to be processed (pending tasks). ### reset() ```ts -reset(withInitialItems?): void +reset(): void ``` -Defined in: [async-queuer.ts:291](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L291) - -Resets the queuer to its initial state. Optionally repopulates with initial items. -Does not affect callbacks or options. +Defined in: [async-queuer.ts:694](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L694) -#### Parameters - -##### withInitialItems? - -`boolean` +Resets the queuer state to its default values #### Returns @@ -511,7 +354,7 @@ Does not affect callbacks or options. setOptions(newOptions): void ``` -Defined in: [async-queuer.ts:192](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L192) +Defined in: [async-queuer.ts:298](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L298) Updates the queuer options. New options are merged with existing options. @@ -533,7 +376,7 @@ Updates the queuer options. New options are merged with existing options. start(): void ``` -Defined in: [async-queuer.ts:261](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L261) +Defined in: [async-queuer.ts:663](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L663) Starts processing items in the queue. If already running, does nothing. @@ -549,7 +392,7 @@ Starts processing items in the queue. If already running, does nothing. stop(): void ``` -Defined in: [async-queuer.ts:273](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L273) +Defined in: [async-queuer.ts:673](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L673) Stops processing items in the queue. Does not clear the queue. diff --git a/docs/reference/classes/asyncratelimiter.md b/docs/reference/classes/asyncratelimiter.md index 33dd724ac..6fee8953f 100644 --- a/docs/reference/classes/asyncratelimiter.md +++ b/docs/reference/classes/asyncratelimiter.md @@ -7,7 +7,7 @@ title: AsyncRateLimiter # Class: AsyncRateLimiter\ -Defined in: [async-rate-limiter.ts:127](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L127) +Defined in: [async-rate-limiter.ts:187](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L187) A class that creates an async rate-limited function. @@ -32,6 +32,18 @@ For smoother execution patterns, consider using: Rate limiting is best used for hard API limits or resource constraints. For UI updates or smoothing out frequent events, throttling or debouncing usually provide better user experience. +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the rate limiter +- `initialState` can be a partial state object +- Use `onSuccess` callback to react to successful function execution and implement custom logic +- Use `onError` callback to react to function execution errors and implement custom error handling +- Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic +- Use `onReject` callback to react to executions being rejected when rate limit is exceeded +- The state includes execution times, success/error counts, and current execution status +- State can be accessed via `asyncRateLimiter.store.state` when using the class directly +- When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state` + Error Handling: - If an `onError` handler is provided, it will be called with the error and rate limiter instance - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown @@ -75,7 +87,7 @@ const data = await rateLimiter.maybeExecute('123'); new AsyncRateLimiter(fn, initialOptions): AsyncRateLimiter ``` -Defined in: [async-rate-limiter.ts:137](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L137) +Defined in: [async-rate-limiter.ts:193](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L193) #### Parameters @@ -91,71 +103,27 @@ Defined in: [async-rate-limiter.ts:137](https://github.com/TanStack/pacer/blob/m [`AsyncRateLimiter`](../asyncratelimiter.md)\<`TFn`\> -## Methods - -### getEnabled() - -```ts -getEnabled(): boolean -``` - -Defined in: [async-rate-limiter.ts:165](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L165) - -Returns the current enabled state of the rate limiter - -#### Returns - -`boolean` - -*** - -### getErrorCount() - -```ts -getErrorCount(): number -``` - -Defined in: [async-rate-limiter.ts:324](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L324) - -Returns the number of times the function has errored - -#### Returns - -`number` - -*** +## Properties -### getIsExecuting() +### options ```ts -getIsExecuting(): boolean +options: AsyncRateLimiterOptions; ``` -Defined in: [async-rate-limiter.ts:338](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L338) - -Returns whether the function is currently executing - -#### Returns - -`boolean` +Defined in: [async-rate-limiter.ts:191](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L191) *** -### getLimit() +### store ```ts -getLimit(): number +readonly store: Store>>; ``` -Defined in: [async-rate-limiter.ts:172](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L172) - -Returns the current limit of executions allowed within the time window +Defined in: [async-rate-limiter.ts:188](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L188) -#### Returns - -`number` - -*** +## Methods ### getMsUntilNextWindow() @@ -163,7 +131,7 @@ Returns the current limit of executions allowed within the time window getMsUntilNextWindow(): number ``` -Defined in: [async-rate-limiter.ts:299](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L299) +Defined in: [async-rate-limiter.ts:366](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L366) Returns the number of milliseconds until the next execution will be possible For fixed windows, this is the time until the current window resets @@ -175,45 +143,13 @@ For sliding windows, this is the time until the oldest execution expires *** -### getOptions() - -```ts -getOptions(): AsyncRateLimiterOptions -``` - -Defined in: [async-rate-limiter.ts:158](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L158) - -Returns the current rate limiter options - -#### Returns - -[`AsyncRateLimiterOptions`](../../interfaces/asyncratelimiteroptions.md)\<`TFn`\> - -*** - -### getRejectionCount() - -```ts -getRejectionCount(): number -``` - -Defined in: [async-rate-limiter.ts:331](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L331) - -Returns the number of times the function has been rejected - -#### Returns - -`number` - -*** - ### getRemainingInWindow() ```ts getRemainingInWindow(): number ``` -Defined in: [async-rate-limiter.ts:289](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L289) +Defined in: [async-rate-limiter.ts:356](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L356) Returns the number of remaining executions allowed in the current window @@ -223,75 +159,23 @@ Returns the number of remaining executions allowed in the current window *** -### getSettleCount() - -```ts -getSettleCount(): number -``` - -Defined in: [async-rate-limiter.ts:317](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L317) - -Returns the number of times the function has been settled - -#### Returns - -`number` - -*** - -### getSuccessCount() - -```ts -getSuccessCount(): number -``` - -Defined in: [async-rate-limiter.ts:310](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L310) - -Returns the number of times the function has been executed - -#### Returns - -`number` - -*** - -### getWindow() - -```ts -getWindow(): number -``` - -Defined in: [async-rate-limiter.ts:179](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L179) - -Returns the current time window in milliseconds - -#### Returns - -`number` - -*** - ### maybeExecute() ```ts maybeExecute(...args): Promise> ``` -Defined in: [async-rate-limiter.ts:212](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L212) +Defined in: [async-rate-limiter.ts:268](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L268) Attempts to execute the rate-limited function if within the configured limits. Will reject execution if the number of calls in the current window exceeds the limit. -If execution is allowed, waits for any previous execution to complete before proceeding. Error Handling: - If the rate-limited function throws and no `onError` handler is configured, the error will be thrown from this method. - If an `onError` handler is configured, errors will be caught and passed to the handler, and this method will return undefined. -- If the rate limit is exceeded, the execution will be rejected and the `onReject` handler - will be called if configured. - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. -- Rate limit rejections can be tracked using `getRejectionCount()`. #### Parameters @@ -314,11 +198,11 @@ The error from the rate-limited function if no onError handler is configured ```ts const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 }); -// First 5 calls will execute -await rateLimiter.maybeExecute('arg1', 'arg2'); +// First 5 calls will return a promise that resolves with the result +const result = await rateLimiter.maybeExecute('arg1', 'arg2'); -// Additional calls within the window will be rejected -await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected +// Additional calls within the window will return undefined +const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined ``` *** @@ -329,7 +213,7 @@ await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected reset(): void ``` -Defined in: [async-rate-limiter.ts:345](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L345) +Defined in: [async-rate-limiter.ts:377](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L377) Resets the rate limiter state @@ -345,9 +229,9 @@ Resets the rate limiter state setOptions(newOptions): void ``` -Defined in: [async-rate-limiter.ts:151](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L151) +Defined in: [async-rate-limiter.ts:208](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L208) -Updates the rate limiter options +Updates the async rate limiter options #### Parameters diff --git a/docs/reference/classes/asyncthrottler.md b/docs/reference/classes/asyncthrottler.md index d5bef03ff..a72ace0e9 100644 --- a/docs/reference/classes/asyncthrottler.md +++ b/docs/reference/classes/asyncthrottler.md @@ -7,7 +7,7 @@ title: AsyncThrottler # Class: AsyncThrottler\ -Defined in: [async-throttler.ts:105](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L105) +Defined in: [async-throttler.ts:180](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L180) A class that creates an async throttled function. @@ -29,6 +29,16 @@ Error Handling: - Both onError and throwOnError can be used together - the handler will be called before any error is thrown - The error state can be checked using the underlying AsyncThrottler instance +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the async throttler +- Use `onSuccess` callback to react to successful function execution and implement custom logic +- Use `onError` callback to react to function execution errors and implement custom error handling +- Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic +- The state includes error count, execution status, last execution time, and success/settle counts +- State can be accessed via `asyncThrottler.store.state` when using the class directly +- When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state` + ## Example ```ts @@ -59,7 +69,7 @@ const result = await throttler.maybeExecute(inputElement.value); new AsyncThrottler(fn, initialOptions): AsyncThrottler ``` -Defined in: [async-throttler.ts:121](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L121) +Defined in: [async-throttler.ts:191](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L191) #### Parameters @@ -75,233 +85,115 @@ Defined in: [async-throttler.ts:121](https://github.com/TanStack/pacer/blob/main [`AsyncThrottler`](../asyncthrottler.md)\<`TFn`\> -## Methods - -### cancel() - -```ts -cancel(): void -``` - -Defined in: [async-throttler.ts:260](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L260) - -Cancels any pending execution or aborts any execution in progress - -#### Returns - -`void` - -*** +## Properties -### getEnabled() +### options ```ts -getEnabled(): boolean +options: AsyncThrottlerOptions; ``` -Defined in: [async-throttler.ts:154](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L154) - -Returns the current enabled state of the throttler - -#### Returns - -`boolean` +Defined in: [async-throttler.ts:184](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L184) *** -### getErrorCount() +### store ```ts -getErrorCount(): number +readonly store: Store>>; ``` -Defined in: [async-throttler.ts:311](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L311) - -Returns the number of times the function has errored - -#### Returns - -`number` - -*** - -### getIsExecuting() - -```ts -getIsExecuting(): boolean -``` - -Defined in: [async-throttler.ts:325](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L325) - -Returns the current executing state - -#### Returns - -`boolean` - -*** - -### getIsPending() - -```ts -getIsPending(): boolean -``` - -Defined in: [async-throttler.ts:318](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L318) - -Returns the current pending state - -#### Returns - -`boolean` - -*** - -### getLastExecutionTime() - -```ts -getLastExecutionTime(): number -``` - -Defined in: [async-throttler.ts:276](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L276) - -Returns the last execution time +Defined in: [async-throttler.ts:181](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L181) -#### Returns - -`number` - -*** +## Methods -### getLastResult() +### cancel() ```ts -getLastResult(): undefined | ReturnType +cancel(): void ``` -Defined in: [async-throttler.ts:290](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L290) +Defined in: [async-throttler.ts:401](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L401) -Returns the last result of the debounced function +Cancels any pending execution or aborts any execution in progress #### Returns -`undefined` \| `ReturnType`\<`TFn`\> +`void` *** -### getNextExecutionTime() +### flush() ```ts -getNextExecutionTime(): number +flush(): void ``` -Defined in: [async-throttler.ts:283](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L283) +Defined in: [async-throttler.ts:356](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L356) -Returns the next execution time +Processes the current pending execution immediately #### Returns -`number` +`void` *** -### getOptions() +### maybeExecute() ```ts -getOptions(): AsyncThrottlerOptions +maybeExecute(...args): Promise> ``` -Defined in: [async-throttler.ts:147](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L147) +Defined in: [async-throttler.ts:273](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L273) -Returns the current options +Attempts to execute the throttled function. The execution behavior depends on the throttler options: -#### Returns +- If enough time has passed since the last execution (>= wait period): + - With leading=true: Executes immediately + - With leading=false: Waits for the next trailing execution -[`AsyncThrottlerOptions`](../../interfaces/asyncthrottleroptions.md)\<`TFn`\> +- If within the wait period: + - With trailing=true: Schedules execution for end of wait period + - With trailing=false: Drops the execution -*** - -### getSettleCount() - -```ts -getSettleCount(): number -``` +#### Parameters -Defined in: [async-throttler.ts:304](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L304) +##### args -Returns the number of times the function has settled (completed or errored) +...`Parameters`\<`TFn`\> #### Returns -`number` - -*** +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> -### getSuccessCount() +#### Example ```ts -getSuccessCount(): number -``` - -Defined in: [async-throttler.ts:297](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L297) +const throttled = new AsyncThrottler(fn, { wait: 1000 }); -Returns the number of times the function has been executed successfully +// First call executes immediately +await throttled.maybeExecute('a', 'b'); -#### Returns - -`number` - -*** - -### getWait() - -```ts -getWait(): number +// Call during wait period - gets throttled +await throttled.maybeExecute('c', 'd'); ``` -Defined in: [async-throttler.ts:161](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L161) - -Returns the current wait time in milliseconds - -#### Returns - -`number` - *** -### maybeExecute() +### reset() ```ts -maybeExecute(...args): Promise> +reset(): void ``` -Defined in: [async-throttler.ts:179](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L179) +Defined in: [async-throttler.ts:409](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L409) -Attempts to execute the throttled function. -If a call is already in progress, it may be blocked or queued depending on the `wait` option. - -Error Handling: -- If the throttled function throws and no `onError` handler is configured, - the error will be thrown from this method. -- If an `onError` handler is configured, errors will be caught and passed to the handler, - and this method will return undefined. -- The error state can be checked using `getErrorCount()` and `getIsExecuting()`. - -#### Parameters - -##### args - -...`Parameters`\<`TFn`\> +Resets the debouncer state to its default values #### Returns -`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> - -A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError - -#### Throws - -The error from the throttled function if no onError handler is configured +`void` *** @@ -311,9 +203,9 @@ The error from the throttled function if no onError handler is configured setOptions(newOptions): void ``` -Defined in: [async-throttler.ts:135](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L135) +Defined in: [async-throttler.ts:206](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L206) -Updates the throttler options +Updates the async throttler options #### Parameters diff --git a/docs/reference/classes/batcher.md b/docs/reference/classes/batcher.md index 97d8432b0..edbf23e0e 100644 --- a/docs/reference/classes/batcher.md +++ b/docs/reference/classes/batcher.md @@ -7,7 +7,7 @@ title: Batcher # Class: Batcher\ -Defined in: [batcher.ts:84](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L84) +Defined in: [batcher.ts:143](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L143) A class that collects items and processes them in batches. @@ -19,6 +19,15 @@ The Batcher provides a flexible way to implement batching with configurable: - Custom batch processing logic via getShouldExecute - Event callbacks for monitoring batch operations +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the batcher +- Use `onExecute` callback to react to batch execution and implement custom logic +- Use `onItemsChange` callback to react to items being added or removed from the batcher +- The state includes batch execution count, total items processed, items, and running status +- State can be accessed via `batcher.store.state` when using the class directly +- When using framework adapters (React/Solid), state is accessed from `batcher.state` + ## Example ```ts @@ -27,7 +36,7 @@ const batcher = new Batcher( { maxSize: 5, wait: 2000, - onExecuteBatch: (items) => console.log('Batch executed:', items) + onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems()) } ); @@ -35,7 +44,7 @@ batcher.addItem(1); batcher.addItem(2); // After 2 seconds or when 5 items are added, whichever comes first, // the batch will be processed -// batcher.execute() // manually trigger a batch +// batcher.flush() // manually trigger a batch ``` ## Type Parameters @@ -50,7 +59,7 @@ batcher.addItem(2); new Batcher(fn, initialOptions): Batcher ``` -Defined in: [batcher.ts:92](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L92) +Defined in: [batcher.ts:150](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L150) #### Parameters @@ -66,162 +75,112 @@ Defined in: [batcher.ts:92](https://github.com/TanStack/pacer/blob/main/packages [`Batcher`](../batcher.md)\<`TValue`\> -## Methods +## Properties -### addItem() +### options ```ts -addItem(item): void +options: BatcherOptionsWithOptionalCallbacks; ``` -Defined in: [batcher.ts:118](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L118) - -Adds an item to the batcher -If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed - -#### Parameters - -##### item - -`TValue` - -#### Returns - -`void` +Defined in: [batcher.ts:147](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L147) *** -### execute() +### store ```ts -execute(): void +readonly store: Store>>; ``` -Defined in: [batcher.ts:146](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L146) - -Processes the current batch of items. -This method will automatically be triggered if the batcher is running and any of these conditions are met: -- The number of items reaches batchSize -- The wait duration has elapsed -- The getShouldExecute function returns true upon adding an item - -You can also call this method manually to process the current batch at any time. - -#### Returns - -`void` - -*** - -### getBatchExecutionCount() - -```ts -getBatchExecutionCount(): number -``` - -Defined in: [batcher.ts:220](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L220) - -Returns the number of times batches have been processed +Defined in: [batcher.ts:144](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L144) -#### Returns - -`number` - -*** +## Methods -### getIsEmpty() +### addItem() ```ts -getIsEmpty(): boolean +addItem(item): void ``` -Defined in: [batcher.ts:199](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L199) - -Returns true if the batcher is empty +Defined in: [batcher.ts:194](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L194) -#### Returns - -`boolean` - -*** - -### getIsRunning() +Adds an item to the batcher +If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed -```ts -getIsRunning(): boolean -``` +#### Parameters -Defined in: [batcher.ts:206](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L206) +##### item -Returns true if the batcher is running +`TValue` #### Returns -`boolean` +`void` *** -### getItemExecutionCount() +### clear() ```ts -getItemExecutionCount(): number +clear(): void ``` -Defined in: [batcher.ts:227](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L227) +Defined in: [batcher.ts:282](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L282) -Returns the total number of individual items that have been processed +Removes all items from the batcher #### Returns -`number` +`void` *** -### getOptions() +### flush() ```ts -getOptions(): BatcherOptions +flush(): void ``` -Defined in: [batcher.ts:110](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L110) +Defined in: [batcher.ts:242](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L242) -Returns the current batcher options +Processes the current batch of items immediately #### Returns -[`BatcherOptions`](../../interfaces/batcheroptions.md)\<`TValue`\> +`void` *** -### getSize() +### peekAllItems() ```ts -getSize(): number +peekAllItems(): TValue[] ``` -Defined in: [batcher.ts:192](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L192) +Defined in: [batcher.ts:268](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L268) -Returns the current number of items in the batcher +Returns a copy of all items in the batcher #### Returns -`number` +`TValue`[] *** -### peekAllItems() +### reset() ```ts -peekAllItems(): TValue[] +reset(): void ``` -Defined in: [batcher.ts:213](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L213) +Defined in: [batcher.ts:289](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L289) -Returns a copy of all items currently in the batcher +Resets the batcher state to its default values #### Returns -`TValue`[] +`void` *** @@ -231,7 +190,7 @@ Returns a copy of all items currently in the batcher setOptions(newOptions): void ``` -Defined in: [batcher.ts:103](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L103) +Defined in: [batcher.ts:164](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L164) Updates the batcher options @@ -253,7 +212,7 @@ Updates the batcher options start(): void ``` -Defined in: [batcher.ts:181](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L181) +Defined in: [batcher.ts:258](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L258) Starts the batcher and processes any pending items @@ -269,7 +228,7 @@ Starts the batcher and processes any pending items stop(): void ``` -Defined in: [batcher.ts:169](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L169) +Defined in: [batcher.ts:250](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L250) Stops the batcher from processing batches diff --git a/docs/reference/classes/debouncer.md b/docs/reference/classes/debouncer.md index 51d2c77e1..ba5eb24a6 100644 --- a/docs/reference/classes/debouncer.md +++ b/docs/reference/classes/debouncer.md @@ -7,7 +7,7 @@ title: Debouncer # Class: Debouncer\ -Defined in: [debouncer.ts:68](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L68) +Defined in: [debouncer.ts:118](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L118) A class that creates a debounced function. @@ -19,6 +19,14 @@ The debounced function can be configured to execute either at the start of the d (leading edge) or at the end (trailing edge, default). Each new call during the wait period will reset the timer. +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the debouncer +- Use `onExecute` callback to react to function execution and implement custom logic +- The state includes canLeadingExecute, execution count, and isPending status +- State can be accessed via `debouncer.store.state` when using the class directly +- When using framework adapters (React/Solid), state is accessed from `debouncer.state` + ## Example ```ts @@ -44,7 +52,7 @@ inputElement.addEventListener('input', () => { new Debouncer(fn, initialOptions): Debouncer ``` -Defined in: [debouncer.ts:75](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L75) +Defined in: [debouncer.ts:125](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L125) #### Parameters @@ -60,120 +68,92 @@ Defined in: [debouncer.ts:75](https://github.com/TanStack/pacer/blob/main/packag [`Debouncer`](../debouncer.md)\<`TFn`\> -## Methods +## Properties -### cancel() +### options ```ts -cancel(): void +options: DebouncerOptions; ``` -Defined in: [debouncer.ts:160](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L160) - -Cancels any pending execution - -#### Returns - -`void` +Defined in: [debouncer.ts:122](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L122) *** -### getEnabled() +### store ```ts -getEnabled(): boolean +readonly store: Store>>; ``` -Defined in: [debouncer.ts:107](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L107) - -Returns the current enabled state of the debouncer +Defined in: [debouncer.ts:119](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L119) -#### Returns - -`boolean` - -*** +## Methods -### getExecutionCount() +### cancel() ```ts -getExecutionCount(): number +cancel(): void ``` -Defined in: [debouncer.ts:171](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L171) +Defined in: [debouncer.ts:242](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L242) -Returns the number of times the function has been executed +Cancels any pending execution #### Returns -`number` +`void` *** -### getIsPending() +### flush() ```ts -getIsPending(): boolean +flush(): void ``` -Defined in: [debouncer.ts:178](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L178) +Defined in: [debouncer.ts:225](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L225) -Returns `true` if debouncing +Processes the current pending execution immediately #### Returns -`boolean` +`void` *** -### getOptions() +### maybeExecute() ```ts -getOptions(): Required> +maybeExecute(...args): void ``` -Defined in: [debouncer.ts:100](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L100) - -Returns the current debouncer options +Defined in: [debouncer.ts:184](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L184) -#### Returns - -`Required`\<[`DebouncerOptions`](../../interfaces/debounceroptions.md)\<`TFn`\>\> - -*** - -### getWait() +Attempts to execute the debounced function +If a call is already in progress, it will be queued -```ts -getWait(): number -``` +#### Parameters -Defined in: [debouncer.ts:114](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L114) +##### args -Returns the current wait time in milliseconds +...`Parameters`\<`TFn`\> #### Returns -`number` +`void` *** -### maybeExecute() +### reset() ```ts -maybeExecute(...args): void +reset(): void ``` -Defined in: [debouncer.ts:122](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L122) - -Attempts to execute the debounced function -If a call is already in progress, it will be queued - -#### Parameters - -##### args +Defined in: [debouncer.ts:253](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L253) -...`Parameters`\<`TFn`\> +Resets the debouncer state to its default values #### Returns @@ -187,7 +167,7 @@ If a call is already in progress, it will be queued setOptions(newOptions): void ``` -Defined in: [debouncer.ts:88](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L88) +Defined in: [debouncer.ts:139](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L139) Updates the debouncer options diff --git a/docs/reference/classes/queuer.md b/docs/reference/classes/queuer.md index a076a53dc..1e1458fde 100644 --- a/docs/reference/classes/queuer.md +++ b/docs/reference/classes/queuer.md @@ -7,7 +7,7 @@ title: Queuer # Class: Queuer\ -Defined in: [queuer.ts:160](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L160) +Defined in: [queuer.ts:243](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L243) A flexible queue that processes items with configurable wait times, expiration, and priority. @@ -19,7 +19,7 @@ Features: - Callbacks for queue state changes, execution, rejection, and expiration Running behavior: -- `start()`: Begins automatically processing items in the queue (defaults to running) +- `start()`: Begins automatically processing items in the queue (defaults to isRunning) - `stop()`: Pauses processing but maintains queue state - `wait`: Configurable delay between processing items - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state @@ -48,6 +48,17 @@ Item expiration: - `getIsExpired`: Function to override default expiration - `onExpire`: Callback for expired items +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the queuer +- Use `onExecute` callback to react to item execution and implement custom logic +- Use `onItemsChange` callback to react to items being added or removed from the queue +- Use `onExpire` callback to react to items expiring and implement custom logic +- Use `onReject` callback to react to items being rejected when the queue is full +- The state includes execution count, expiration count, rejection count, and isRunning status +- State can be accessed via `queuer.store.state` when using the class directly +- When using framework adapters (React/Solid), state is accessed from `queuer.state` + Example usage: ```ts // Auto-processing queue with wait time @@ -81,7 +92,7 @@ manualQueue.getNextItem(); // returns 2, queue is empty new Queuer(fn, initialOptions): Queuer ``` -Defined in: [queuer.ts:171](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L171) +Defined in: [queuer.ts:250](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L250) #### Parameters @@ -97,6 +108,26 @@ Defined in: [queuer.ts:171](https://github.com/TanStack/pacer/blob/main/packages [`Queuer`](../queuer.md)\<`TValue`\> +## Properties + +### options + +```ts +options: QueuerOptions; +``` + +Defined in: [queuer.ts:247](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L247) + +*** + +### store + +```ts +readonly store: Store>>; +``` + +Defined in: [queuer.ts:244](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L244) + ## Methods ### addItem() @@ -105,10 +136,10 @@ Defined in: [queuer.ts:171](https://github.com/TanStack/pacer/blob/main/packages addItem( item, position, - runOnUpdate): boolean + runOnItemsChange): boolean ``` -Defined in: [queuer.ts:343](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L343) +Defined in: [queuer.ts:364](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L364) 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. @@ -131,7 +162,7 @@ queuer.addItem('task2', 'front'); [`QueuePosition`](../../type-aliases/queueposition.md) = `...` -##### runOnUpdate +##### runOnItemsChange `boolean` = `true` @@ -147,7 +178,7 @@ queuer.addItem('task2', 'front'); clear(): void ``` -Defined in: [queuer.ts:313](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L313) +Defined in: [queuer.ts:620](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L620) Removes all pending items from the queue. Does not affect items being processed. @@ -163,7 +194,7 @@ Removes all pending items from the queue. Does not affect items being processed. execute(position?): undefined | TValue ``` -Defined in: [queuer.ts:431](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L431) +Defined in: [queuer.ts:485](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L485) Removes and returns the next item from the queue and processes it using the provided function. @@ -186,99 +217,30 @@ queuer.execute('back'); *** -### getExecutionCount() +### flush() ```ts -getExecutionCount(): number +flush(numberOfItems, position?): void ``` -Defined in: [queuer.ts:490](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L490) - -Returns the number of items that have been processed and removed from the queue. - -#### Returns - -`number` - -*** - -### getExpirationCount() - -```ts -getExpirationCount(): number -``` - -Defined in: [queuer.ts:504](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L504) - -Returns the number of items that have expired and been removed from the queue. - -#### Returns - -`number` - -*** - -### getIsEmpty() - -```ts -getIsEmpty(): boolean -``` - -Defined in: [queuer.ts:462](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L462) - -Returns true if the queue is empty (no pending items). - -#### Returns - -`boolean` - -*** - -### getIsFull() - -```ts -getIsFull(): boolean -``` - -Defined in: [queuer.ts:469](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L469) - -Returns true if the queue is full (reached maxSize). - -#### Returns - -`boolean` - -*** - -### getIsIdle() - -```ts -getIsIdle(): boolean -``` - -Defined in: [queuer.ts:518](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L518) - -Returns true if the queuer is running but has no items to process. +Defined in: [queuer.ts:501](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L501) -#### Returns - -`boolean` +Processes a specified number of items to execute immediately with no wait time +If no numberOfItems is provided, all items will be processed -*** +#### Parameters -### getIsRunning() +##### numberOfItems -```ts -getIsRunning(): boolean -``` +`number` = `...` -Defined in: [queuer.ts:511](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L511) +##### position? -Returns true if the queuer is currently running (processing items). +[`QueuePosition`](../../type-aliases/queueposition.md) #### Returns -`boolean` +`void` *** @@ -288,7 +250,7 @@ Returns true if the queuer is currently running (processing items). getNextItem(position): undefined | TValue ``` -Defined in: [queuer.ts:401](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L401) +Defined in: [queuer.ts:444](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L444) Removes and returns the next item from the queue without executing the function. Use for manual queue management. Normally, use execute() to process items. @@ -313,78 +275,13 @@ queuer.getNextItem('back'); *** -### getOptions() - -```ts -getOptions(): Required> -``` - -Defined in: [queuer.ts:195](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L195) - -Returns the current queuer options, including defaults and any overrides. - -#### Returns - -`Required`\<[`QueuerOptions`](../../interfaces/queueroptions.md)\<`TValue`\>\> - -*** - -### getRejectionCount() - -```ts -getRejectionCount(): number -``` - -Defined in: [queuer.ts:497](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L497) - -Returns the number of items that have been rejected from being added to the queue. - -#### Returns - -`number` - -*** - -### getSize() - -```ts -getSize(): number -``` - -Defined in: [queuer.ts:476](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L476) - -Returns the number of pending items in the queue. - -#### Returns - -`number` - -*** - -### getWait() - -```ts -getWait(): number -``` - -Defined in: [queuer.ts:203](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L203) - -Returns the current wait time (in milliseconds) between processing items. -If a function is provided, it is called with the queuer instance. - -#### Returns - -`number` - -*** - ### peekAllItems() ```ts peekAllItems(): TValue[] ``` -Defined in: [queuer.ts:483](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L483) +Defined in: [queuer.ts:588](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L588) Returns a copy of all items in the queue. @@ -400,7 +297,7 @@ Returns a copy of all items in the queue. peekNextItem(position): undefined | TValue ``` -Defined in: [queuer.ts:450](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L450) +Defined in: [queuer.ts:578](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L578) Returns the next item in the queue without removing it. @@ -414,7 +311,7 @@ queuer.peekNextItem('back'); // back ##### position -[`QueuePosition`](../../type-aliases/queueposition.md) = `...` +[`QueuePosition`](../../type-aliases/queueposition.md) = `'front'` #### Returns @@ -425,19 +322,12 @@ queuer.peekNextItem('back'); // back ### reset() ```ts -reset(withInitialItems?): void +reset(): void ``` -Defined in: [queuer.ts:322](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L322) +Defined in: [queuer.ts:628](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L628) -Resets the queuer to its initial state. Optionally repopulates with initial items. -Does not affect callbacks or options. - -#### Parameters - -##### withInitialItems? - -`boolean` +Resets the queuer state to its default values #### Returns @@ -451,7 +341,7 @@ Does not affect callbacks or options. setOptions(newOptions): void ``` -Defined in: [queuer.ts:188](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L188) +Defined in: [queuer.ts:281](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L281) Updates the queuer options. New options are merged with existing options. @@ -473,9 +363,9 @@ Updates the queuer options. New options are merged with existing options. start(): void ``` -Defined in: [queuer.ts:301](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L301) +Defined in: [queuer.ts:595](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L595) -Starts processing items in the queue. If already running, does nothing. +Starts processing items in the queue. If already isRunning, does nothing. #### Returns @@ -489,7 +379,7 @@ Starts processing items in the queue. If already running, does nothing. stop(): void ``` -Defined in: [queuer.ts:292](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L292) +Defined in: [queuer.ts:605](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L605) Stops processing items in the queue. Does not clear the queue. diff --git a/docs/reference/classes/ratelimiter.md b/docs/reference/classes/ratelimiter.md index 751ebe509..fe2fecb19 100644 --- a/docs/reference/classes/ratelimiter.md +++ b/docs/reference/classes/ratelimiter.md @@ -7,7 +7,7 @@ title: RateLimiter # Class: RateLimiter\ -Defined in: [rate-limiter.ts:80](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L80) +Defined in: [rate-limiter.ts:122](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L122) A class that creates a rate-limited function. @@ -28,12 +28,25 @@ For smoother execution patterns, consider using: Rate limiting is best used for hard API limits or resource constraints. For UI updates or smoothing out frequent events, throttling or debouncing usually provide better user experience. +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the rate limiter +- Use `onExecute` callback to react to function execution and implement custom logic +- Use `onReject` callback to react to executions being rejected when rate limit is exceeded +- The state includes execution count, execution times, and rejection count +- State can be accessed via `rateLimiter.store.state` when using the class directly +- When using framework adapters (React/Solid), state is accessed from `rateLimiter.state` + ## Example ```ts const rateLimiter = new RateLimiter( (id: string) => api.getData(id), - { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window + { + limit: 5, + window: 1000, + windowType: 'sliding', + } ); // Will execute immediately until limit reached, then block @@ -52,7 +65,7 @@ rateLimiter.maybeExecute('123'); new RateLimiter(fn, initialOptions): RateLimiter ``` -Defined in: [rate-limiter.ts:86](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L86) +Defined in: [rate-limiter.ts:127](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L127) #### Parameters @@ -68,55 +81,27 @@ Defined in: [rate-limiter.ts:86](https://github.com/TanStack/pacer/blob/main/pac [`RateLimiter`](../ratelimiter.md)\<`TFn`\> -## Methods - -### getEnabled() - -```ts -getEnabled(): boolean -``` - -Defined in: [rate-limiter.ts:113](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L113) +## Properties -Returns the current enabled state of the rate limiter - -#### Returns - -`boolean` - -*** - -### getExecutionCount() +### options ```ts -getExecutionCount(): number +options: RateLimiterOptions; ``` -Defined in: [rate-limiter.ts:198](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L198) - -Returns the number of times the function has been executed - -#### Returns - -`number` +Defined in: [rate-limiter.ts:125](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L125) *** -### getLimit() +### store ```ts -getLimit(): number +readonly store: Store>; ``` -Defined in: [rate-limiter.ts:120](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L120) - -Returns the current limit of executions allowed within the time window - -#### Returns - -`number` +Defined in: [rate-limiter.ts:123](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L123) -*** +## Methods ### getMsUntilNextWindow() @@ -124,7 +109,7 @@ Returns the current limit of executions allowed within the time window getMsUntilNextWindow(): number ``` -Defined in: [rate-limiter.ts:220](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L220) +Defined in: [rate-limiter.ts:258](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L258) Returns the number of milliseconds until the next execution will be possible @@ -134,45 +119,13 @@ Returns the number of milliseconds until the next execution will be possible *** -### getOptions() - -```ts -getOptions(): Required> -``` - -Defined in: [rate-limiter.ts:106](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L106) - -Returns the current rate limiter options - -#### Returns - -`Required`\<[`RateLimiterOptions`](../../interfaces/ratelimiteroptions.md)\<`TFn`\>\> - -*** - -### getRejectionCount() - -```ts -getRejectionCount(): number -``` - -Defined in: [rate-limiter.ts:205](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L205) - -Returns the number of times the function has been rejected - -#### Returns - -`number` - -*** - ### getRemainingInWindow() ```ts getRemainingInWindow(): number ``` -Defined in: [rate-limiter.ts:212](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L212) +Defined in: [rate-limiter.ts:250](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L250) Returns the number of remaining executions allowed in the current window @@ -182,29 +135,13 @@ Returns the number of remaining executions allowed in the current window *** -### getWindow() - -```ts -getWindow(): number -``` - -Defined in: [rate-limiter.ts:127](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L127) - -Returns the current time window in milliseconds - -#### Returns - -`number` - -*** - ### maybeExecute() ```ts maybeExecute(...args): boolean ``` -Defined in: [rate-limiter.ts:146](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L146) +Defined in: [rate-limiter.ts:191](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L191) Attempts to execute the rate-limited function if within the configured limits. Will reject execution if the number of calls in the current window exceeds the limit. @@ -239,7 +176,7 @@ rateLimiter.maybeExecute('arg1', 'arg2'); // false reset(): void ``` -Defined in: [rate-limiter.ts:231](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L231) +Defined in: [rate-limiter.ts:269](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L269) Resets the rate limiter state @@ -255,7 +192,7 @@ Resets the rate limiter state setOptions(newOptions): void ``` -Defined in: [rate-limiter.ts:99](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L99) +Defined in: [rate-limiter.ts:141](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L141) Updates the rate limiter options diff --git a/docs/reference/classes/throttler.md b/docs/reference/classes/throttler.md index c4126eb9c..9c9b63873 100644 --- a/docs/reference/classes/throttler.md +++ b/docs/reference/classes/throttler.md @@ -7,7 +7,7 @@ title: Throttler # Class: Throttler\ -Defined in: [throttler.ts:71](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L71) +Defined in: [throttler.ts:126](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L126) A class that creates a throttled function. @@ -21,6 +21,14 @@ Supports both leading and trailing edge execution: For collapsing rapid-fire events where you only care about the last call, consider using Debouncer. +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the throttler +- Use `onExecute` callback to react to function execution and implement custom logic +- The state includes execution count, last execution time, pending status, and more +- State can be accessed via `throttler.store.state` when using the class directly +- When using framework adapters (React/Solid), state is accessed from `throttler.state` + ## Example ```ts @@ -48,7 +56,7 @@ throttler.maybeExecute('123'); // Throttled new Throttler(fn, initialOptions): Throttler ``` -Defined in: [throttler.ts:78](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L78) +Defined in: [throttler.ts:133](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L133) #### Parameters @@ -64,139 +72,63 @@ Defined in: [throttler.ts:78](https://github.com/TanStack/pacer/blob/main/packag [`Throttler`](../throttler.md)\<`TFn`\> -## Methods +## Properties -### cancel() +### options ```ts -cancel(): void +options: ThrottlerOptions; ``` -Defined in: [throttler.ts:189](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L189) - -Cancels any pending trailing execution and clears internal state. - -If a trailing execution is scheduled (due to throttling with trailing=true), -this will prevent that execution from occurring. The internal timeout and -stored arguments will be cleared. - -Has no effect if there is no pending execution. - -#### Returns - -`void` +Defined in: [throttler.ts:130](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L130) *** -### getEnabled() +### store ```ts -getEnabled(): boolean +readonly store: Store>>; ``` -Defined in: [throttler.ts:110](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L110) - -Returns the current enabled state of the throttler - -#### Returns - -`boolean` - -*** +Defined in: [throttler.ts:127](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L127) -### getExecutionCount() - -```ts -getExecutionCount(): number -``` - -Defined in: [throttler.ts:214](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L214) - -Returns the number of times the function has been executed - -#### Returns - -`number` - -*** - -### getIsPending() - -```ts -getIsPending(): boolean -``` - -Defined in: [throttler.ts:221](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L221) - -Returns `true` if there is a pending execution - -#### Returns - -`boolean` - -*** - -### getLastExecutionTime() - -```ts -getLastExecutionTime(): number -``` - -Defined in: [throttler.ts:200](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L200) - -Returns the last execution time - -#### Returns - -`number` - -*** +## Methods -### getNextExecutionTime() +### cancel() ```ts -getNextExecutionTime(): number +cancel(): void ``` -Defined in: [throttler.ts:207](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L207) - -Returns the next execution time - -#### Returns - -`number` - -*** - -### getOptions() +Defined in: [throttler.ts:276](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L276) -```ts -getOptions(): Required> -``` +Cancels any pending trailing execution and clears internal state. -Defined in: [throttler.ts:103](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L103) +If a trailing execution is scheduled (due to throttling with trailing=true), +this will prevent that execution from occurring. The internal timeout and +stored arguments will be cleared. -Returns the current throttler options +Has no effect if there is no pending execution. #### Returns -`Required`\<[`ThrottlerOptions`](../../interfaces/throttleroptions.md)\<`TFn`\>\> +`void` *** -### getWait() +### flush() ```ts -getWait(): number +flush(): void ``` -Defined in: [throttler.ts:117](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L117) +Defined in: [throttler.ts:254](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L254) -Returns the current wait time in milliseconds +Processes the current pending execution immediately #### Returns -`number` +`void` *** @@ -206,7 +138,7 @@ Returns the current wait time in milliseconds maybeExecute(...args): void ``` -Defined in: [throttler.ts:143](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L143) +Defined in: [throttler.ts:204](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L204) Attempts to execute the throttled function. The execution behavior depends on the throttler options: @@ -242,13 +174,29 @@ throttled.maybeExecute('c', 'd'); *** +### reset() + +```ts +reset(): void +``` + +Defined in: [throttler.ts:287](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L287) + +Resets the throttler state to its default values + +#### Returns + +`void` + +*** + ### setOptions() ```ts setOptions(newOptions): void ``` -Defined in: [throttler.ts:91](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L91) +Defined in: [throttler.ts:147](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L147) Updates the throttler options diff --git a/docs/reference/functions/asyncbatch.md b/docs/reference/functions/asyncbatch.md new file mode 100644 index 000000000..e39ca42be --- /dev/null +++ b/docs/reference/functions/asyncbatch.md @@ -0,0 +1,94 @@ +--- +id: asyncBatch +title: asyncBatch +--- + + + +# Function: asyncBatch() + +```ts +function asyncBatch(fn, options): (item) => void +``` + +Defined in: [async-batcher.ts:469](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L469) + +Creates an async batcher that processes items in batches + +Unlike the sync batcher, this async version: +- Handles promises and returns results from batch executions +- Provides error handling with configurable error behavior +- Tracks success, error, and settle counts separately +- Has state tracking for when batches are executing + +Error Handling: +- If an `onError` handler is provided, it will be called with the error and batcher instance +- If `throwOnError` is true (default when no onError handler is provided), the error will be thrown +- If `throwOnError` is false (default when onError handler is provided), the error will be swallowed +- Both onError and throwOnError can be used together - the handler will be called before any error is thrown +- The error state can be checked using the underlying AsyncBatcher instance + +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the async batcher +- Use `onSuccess` callback to react to successful batch execution and implement custom logic +- Use `onError` callback to react to batch execution errors and implement custom error handling +- Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic +- Use `onExecute` callback to react to batch execution and implement custom logic +- Use `onItemsChange` callback to react to items being added or removed from the batcher +- The state includes total items processed, success/error counts, and execution status +- State can be accessed via the underlying AsyncBatcher instance's `store.state` property +- When using framework adapters (React/Solid), state is accessed from the hook's state property + +## Type Parameters + +• **TValue** + +## Parameters + +### fn + +(`items`) => `Promise`\<`any`\> + +### options + +[`AsyncBatcherOptions`](../../interfaces/asyncbatcheroptions.md)\<`TValue`\> + +## Returns + +`Function` + +Adds an item to the async batcher +If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed + +### Parameters + +#### item + +`TValue` + +### Returns + +`void` + +## Example + +```ts +const batchItems = asyncBatch( + async (items) => { + const result = await processApiCall(items); + console.log('Processing:', items); + return result; + }, + { + maxSize: 3, + wait: 1000, + onSuccess: (result) => console.log('Batch succeeded:', result), + onError: (error) => console.error('Batch failed:', error) + } +); + +batchItems(1); +batchItems(2); +batchItems(3); // Triggers batch processing +``` diff --git a/docs/reference/functions/asyncdebounce.md b/docs/reference/functions/asyncdebounce.md index ab8f81b78..72e5b0d8f 100644 --- a/docs/reference/functions/asyncdebounce.md +++ b/docs/reference/functions/asyncdebounce.md @@ -11,7 +11,7 @@ title: asyncDebounce function asyncDebounce(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-debouncer.ts:341](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L341) +Defined in: [async-debouncer.ts:421](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L421) Creates an async debounced function that delays execution until after a specified wait time. The debounced function will only execute once the wait period has elapsed without any new calls. @@ -28,6 +28,16 @@ Error Handling: - The error state can be checked using the underlying AsyncDebouncer instance - Both onError and throwOnError can be used together - the handler will be called before any error is thrown +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the async debouncer +- Use `onSuccess` callback to react to successful function execution and implement custom logic +- Use `onError` callback to react to function execution errors and implement custom error handling +- Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic +- The state includes canLeadingExecute, error count, execution status, and success/settle counts +- State can be accessed via `asyncDebouncer.store.state` when using the class directly +- When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state` + ## Type Parameters • **TFn** *extends* [`AnyAsyncFunction`](../../type-aliases/anyasyncfunction.md) diff --git a/docs/reference/functions/asyncqueue.md b/docs/reference/functions/asyncqueue.md index 686b77452..d4499c271 100644 --- a/docs/reference/functions/asyncqueue.md +++ b/docs/reference/functions/asyncqueue.md @@ -8,10 +8,10 @@ title: asyncQueue # Function: asyncQueue() ```ts -function asyncQueue(fn, initialOptions): (item, position, runOnItemsChange) => void +function asyncQueue(fn, initialOptions): (item, position, runOnItemsChange) => boolean ``` -Defined in: [async-queuer.ts:612](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L612) +Defined in: [async-queuer.ts:733](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L733) Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks. The queuer is started automatically and ready to process items. @@ -23,6 +23,19 @@ Error Handling: - Both onError and throwOnError can be used together; the handler will be called before any error is thrown - The error state can be checked using the underlying AsyncQueuer instance +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the async queuer +- Use `onSuccess` callback to react to successful task execution and implement custom logic +- Use `onError` callback to react to task execution errors and implement custom error handling +- Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic +- Use `onItemsChange` callback to react to items being added or removed from the queue +- Use `onExpire` callback to react to items expiring and implement custom logic +- Use `onReject` callback to react to items being rejected when the queue is full +- The state includes error count, expiration count, rejection count, running status, and success/settle counts +- State can be accessed via the underlying AsyncQueuer instance's `store.state` property +- When using framework adapters (React/Solid), state is accessed from the hook's state property + Example usage: ```ts const enqueue = asyncQueue(async (item) => { @@ -57,7 +70,7 @@ Items can be inserted based on priority or at the front/back depending on config #### item -`TValue` & `object` +`TValue` #### position @@ -69,7 +82,7 @@ Items can be inserted based on priority or at the front/back depending on config ### Returns -`void` +`boolean` ### Example diff --git a/docs/reference/functions/asyncratelimit.md b/docs/reference/functions/asyncratelimit.md index 57d2b95f8..e8df3493d 100644 --- a/docs/reference/functions/asyncratelimit.md +++ b/docs/reference/functions/asyncratelimit.md @@ -11,7 +11,7 @@ title: asyncRateLimit function asyncRateLimit(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-rate-limiter.ts:407](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L407) +Defined in: [async-rate-limiter.ts:447](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L447) Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window. @@ -30,6 +30,18 @@ Note that rate limiting is a simpler form of execution control compared to throt - A throttler ensures even spacing between executions, which can be better for consistent performance - A debouncer collapses multiple calls into one, which is better for handling bursts of events +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the rate limiter +- `initialState` can be a partial state object +- Use `onSuccess` callback to react to successful function execution and implement custom logic +- Use `onError` callback to react to function execution errors and implement custom error handling +- Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic +- Use `onReject` callback to react to executions being rejected when rate limit is exceeded +- The state includes execution times, success/error counts, and current execution status +- State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property +- When using framework adapters (React/Solid), state is accessed from the hook's state property + Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically need to enforce a hard limit on the number of executions within a time period. @@ -61,17 +73,13 @@ Error Handling: Attempts to execute the rate-limited function if within the configured limits. Will reject execution if the number of calls in the current window exceeds the limit. -If execution is allowed, waits for any previous execution to complete before proceeding. Error Handling: - If the rate-limited function throws and no `onError` handler is configured, the error will be thrown from this method. - If an `onError` handler is configured, errors will be caught and passed to the handler, and this method will return undefined. -- If the rate limit is exceeded, the execution will be rejected and the `onReject` handler - will be called if configured. - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. -- Rate limit rejections can be tracked using `getRejectionCount()`. ### Parameters @@ -94,11 +102,11 @@ The error from the rate-limited function if no onError handler is configured ```ts const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 }); -// First 5 calls will execute -await rateLimiter.maybeExecute('arg1', 'arg2'); +// First 5 calls will return a promise that resolves with the result +const result = await rateLimiter.maybeExecute('arg1', 'arg2'); -// Additional calls within the window will be rejected -await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected +// Additional calls within the window will return undefined +const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined ``` ## Example diff --git a/docs/reference/functions/asyncthrottle.md b/docs/reference/functions/asyncthrottle.md index 24f7c0190..f43059681 100644 --- a/docs/reference/functions/asyncthrottle.md +++ b/docs/reference/functions/asyncthrottle.md @@ -11,7 +11,7 @@ title: asyncThrottle function asyncThrottle(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-throttler.ts:363](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L363) +Defined in: [async-throttler.ts:457](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L457) Creates an async throttled function that limits how often the function can execute. The throttled function will execute at most once per wait period, even if called multiple times. @@ -28,6 +28,16 @@ Error Handling: - Both onError and throwOnError can be used together - the handler will be called before any error is thrown - The error state can be checked using the underlying AsyncThrottler instance +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the async throttler +- Use `onSuccess` callback to react to successful function execution and implement custom logic +- Use `onError` callback to react to function execution errors and implement custom error handling +- Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic +- The state includes error count, execution status, last execution time, and success/settle counts +- State can be accessed via the underlying AsyncThrottler instance's `store.state` property +- When using framework adapters (React/Solid), state is accessed from the hook's state property + ## Type Parameters • **TFn** *extends* [`AnyAsyncFunction`](../../type-aliases/anyasyncfunction.md) @@ -46,15 +56,15 @@ Error Handling: `Function` -Attempts to execute the throttled function. -If a call is already in progress, it may be blocked or queued depending on the `wait` option. +Attempts to execute the throttled function. The execution behavior depends on the throttler options: -Error Handling: -- If the throttled function throws and no `onError` handler is configured, - the error will be thrown from this method. -- If an `onError` handler is configured, errors will be caught and passed to the handler, - and this method will return undefined. -- The error state can be checked using `getErrorCount()` and `getIsExecuting()`. +- If enough time has passed since the last execution (>= wait period): + - With leading=true: Executes immediately + - With leading=false: Waits for the next trailing execution + +- If within the wait period: + - With trailing=true: Schedules execution for end of wait period + - With trailing=false: Drops the execution ### Parameters @@ -66,11 +76,17 @@ Error Handling: `Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> -A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError +### Example -### Throws +```ts +const throttled = new AsyncThrottler(fn, { wait: 1000 }); + +// First call executes immediately +await throttled.maybeExecute('a', 'b'); -The error from the throttled function if no onError handler is configured +// Call during wait period - gets throttled +await throttled.maybeExecute('c', 'd'); +``` ## Example diff --git a/docs/reference/functions/batch.md b/docs/reference/functions/batch.md index c3dfc8fc0..d10aa9c94 100644 --- a/docs/reference/functions/batch.md +++ b/docs/reference/functions/batch.md @@ -11,7 +11,7 @@ title: batch function batch(fn, options): (item) => void ``` -Defined in: [batcher.ts:247](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L247) +Defined in: [batcher.ts:313](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L313) Creates a batcher that processes items in batches @@ -49,10 +49,13 @@ If the batch size is reached, timeout occurs, or shouldProcess returns true, the ## Example ```ts -const batchItems = batch({ - batchSize: 3, - processBatch: (items) => console.log('Processing:', items) -}); +const batchItems = batch( + (items) => console.log('Processing:', items), + { + maxSize: 3, + onExecute: (batcher) => console.log('Batch executed') + } +); batchItems(1); batchItems(2); diff --git a/docs/reference/functions/bindinstancemethods.md b/docs/reference/functions/bindinstancemethods.md deleted file mode 100644 index d8579bc89..000000000 --- a/docs/reference/functions/bindinstancemethods.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -id: bindInstanceMethods -title: bindInstanceMethods ---- - - - -# Function: bindInstanceMethods() - -```ts -function bindInstanceMethods(instance): T -``` - -Defined in: [utils.ts:14](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/utils.ts#L14) - -## Type Parameters - -• **T** *extends* `Record`\<`string`, `any`\> - -## Parameters - -### instance - -`T` - -## Returns - -`T` diff --git a/docs/reference/functions/debounce.md b/docs/reference/functions/debounce.md index 8856e4e31..ecb39132d 100644 --- a/docs/reference/functions/debounce.md +++ b/docs/reference/functions/debounce.md @@ -11,7 +11,7 @@ title: debounce function debounce(fn, initialOptions): (...args) => void ``` -Defined in: [debouncer.ts:203](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L203) +Defined in: [debouncer.ts:286](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L286) Creates a debounced function that delays invoking the provided function until after a specified wait time. Multiple calls during the wait period will cancel previous pending invocations and reset the timer. @@ -22,6 +22,14 @@ more control over the debouncing behavior, use the Debouncer class directly. If leading option is true, the function will execute immediately on the first call, then wait the delay before allowing another execution. +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the debouncer +- Use `onExecute` callback to react to function execution and implement custom logic +- The state includes canLeadingExecute, execution count, and isPending status +- State can be accessed via the underlying Debouncer instance's `store.state` property +- When using framework adapters (React/Solid), state is accessed from the hook's state property + ## Type Parameters • **TFn** *extends* [`AnyFunction`](../../type-aliases/anyfunction.md) diff --git a/docs/reference/functions/isplainarray.md b/docs/reference/functions/isplainarray.md deleted file mode 100644 index ceaaf8e1c..000000000 --- a/docs/reference/functions/isplainarray.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -id: isPlainArray -title: isPlainArray ---- - - - -# Function: isPlainArray() - -```ts -function isPlainArray(value): boolean -``` - -Defined in: [compare.ts:66](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/compare.ts#L66) - -## Parameters - -### value - -`unknown` - -## Returns - -`boolean` diff --git a/docs/reference/functions/isplainobject.md b/docs/reference/functions/isplainobject.md deleted file mode 100644 index 6156327b2..000000000 --- a/docs/reference/functions/isplainobject.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -id: isPlainObject -title: isPlainObject ---- - - - -# Function: isPlainObject() - -```ts -function isPlainObject(o): o is Object -``` - -Defined in: [compare.ts:72](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/compare.ts#L72) - -## Parameters - -### o - -`any` - -## Returns - -`o is Object` diff --git a/docs/reference/functions/queue.md b/docs/reference/functions/queue.md index 74b088820..937f9bce7 100644 --- a/docs/reference/functions/queue.md +++ b/docs/reference/functions/queue.md @@ -8,18 +8,29 @@ title: queue # Function: queue() ```ts -function queue(fn, options): (item, position, runOnUpdate) => boolean +function queue(fn, initialOptions): (item, position, runOnItemsChange) => boolean ``` -Defined in: [queuer.ts:549](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L549) +Defined in: [queuer.ts:671](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L671) Creates a queue that processes items immediately upon addition. Items are processed sequentially in FIFO order by default. This is a simplified wrapper around the Queuer class that only exposes the -`addItem` method. The queue is always running and will process items as they are added. +`addItem` method. The queue is always isRunning and will process items as they are added. For more control over queue processing, use the Queuer class directly. +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the queuer +- Use `onExecute` callback to react to item execution and implement custom logic +- Use `onItemsChange` callback to react to items being added or removed from the queue +- Use `onExpire` callback to react to items expiring and implement custom logic +- Use `onReject` callback to react to items being rejected when the queue is full +- The state includes execution count, expiration count, rejection count, and isRunning status +- State can be accessed via the underlying Queuer instance's `store.state` property +- When using framework adapters (React/Solid), state is accessed from the hook's state property + Example usage: ```ts // Basic sequential processing @@ -48,7 +59,7 @@ processPriority(3); // Processed before 1 (`item`) => `void` -### options +### initialOptions [`QueuerOptions`](../../interfaces/queueroptions.md)\<`TValue`\> @@ -77,7 +88,7 @@ queuer.addItem('task2', 'front'); [`QueuePosition`](../../type-aliases/queueposition.md) = `...` -#### runOnUpdate +#### runOnItemsChange `boolean` = `true` diff --git a/docs/reference/functions/ratelimit.md b/docs/reference/functions/ratelimit.md index f83d03a2f..584a283ce 100644 --- a/docs/reference/functions/ratelimit.md +++ b/docs/reference/functions/ratelimit.md @@ -11,7 +11,7 @@ title: rateLimit function rateLimit(fn, initialOptions): (...args) => boolean ``` -Defined in: [rate-limiter.ts:275](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L275) +Defined in: [rate-limiter.ts:320](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L320) Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window. @@ -26,6 +26,15 @@ The rate limiter supports two types of windows: - 'sliding': A rolling window that allows executions as old ones expire. This provides a more consistent rate of execution over time. +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the rate limiter +- Use `onExecute` callback to react to function execution and implement custom logic +- Use `onReject` callback to react to executions being rejected when rate limit is exceeded +- The state includes execution count, execution times, and rejection count +- State can be accessed via the underlying RateLimiter instance's `store.state` property +- When using framework adapters (React/Solid), state is accessed from the hook's state property + Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically need to enforce a hard limit on the number of executions within a time period. diff --git a/docs/reference/functions/replaceequaldeep.md b/docs/reference/functions/replaceequaldeep.md deleted file mode 100644 index 5a2e5a77c..000000000 --- a/docs/reference/functions/replaceequaldeep.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: replaceEqualDeep -title: replaceEqualDeep ---- - - - -# Function: replaceEqualDeep() - -```ts -function replaceEqualDeep(a, b): T -``` - -Defined in: [compare.ts:6](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/compare.ts#L6) - -This function returns `a` if `b` is deeply equal. -If not, it will replace any deeply equal children of `b` with those of `a`. -This can be used for structural sharing between JSON values for example. - -## Type Parameters - -• **T** - -## Parameters - -### a - -`unknown` - -### b - -`T` - -## Returns - -`T` diff --git a/docs/reference/functions/shallowequalobjects.md b/docs/reference/functions/shallowequalobjects.md deleted file mode 100644 index cf37d6dc1..000000000 --- a/docs/reference/functions/shallowequalobjects.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -id: shallowEqualObjects -title: shallowEqualObjects ---- - - - -# Function: shallowEqualObjects() - -```ts -function shallowEqualObjects(a, b): boolean -``` - -Defined in: [compare.ts:49](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/compare.ts#L49) - -Shallow compare objects. - -## Type Parameters - -• **T** *extends* `Record`\<`string`, `any`\> - -## Parameters - -### a - -`T` - -### b - -`undefined` | `T` - -## Returns - -`boolean` diff --git a/docs/reference/functions/throttle.md b/docs/reference/functions/throttle.md index 0f12a6976..cf75eaf7c 100644 --- a/docs/reference/functions/throttle.md +++ b/docs/reference/functions/throttle.md @@ -11,7 +11,7 @@ title: throttle function throttle(fn, initialOptions): (...args) => void ``` -Defined in: [throttler.ts:252](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L252) +Defined in: [throttler.ts:326](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L326) Creates a throttled function that limits how often the provided function can execute. @@ -25,6 +25,14 @@ edge of the throttle window via options. For handling bursts of events, consider using debounce() instead. For hard execution limits, consider using rateLimit(). +State Management: +- Uses TanStack Store for reactive state management +- Use `initialState` to provide initial state values when creating the throttler +- Use `onExecute` callback to react to function execution and implement custom logic +- The state includes execution count, last execution time, pending status, and more +- State can be accessed via the underlying Throttler instance's `store.state` property +- When using framework adapters (React/Solid), state is accessed from the hook's state property + ## Type Parameters • **TFn** *extends* [`AnyFunction`](../../type-aliases/anyfunction.md) diff --git a/docs/reference/index.md b/docs/reference/index.md index 4e2a51641..a592ae553 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -9,6 +9,7 @@ title: "@tanstack/pacer" ## Classes +- [AsyncBatcher](../classes/asyncbatcher.md) - [AsyncDebouncer](../classes/asyncdebouncer.md) - [AsyncQueuer](../classes/asyncqueuer.md) - [AsyncRateLimiter](../classes/asyncratelimiter.md) @@ -21,15 +22,26 @@ title: "@tanstack/pacer" ## Interfaces +- [AsyncBatcherOptions](../interfaces/asyncbatcheroptions.md) +- [AsyncBatcherState](../interfaces/asyncbatcherstate.md) - [AsyncDebouncerOptions](../interfaces/asyncdebounceroptions.md) +- [AsyncDebouncerState](../interfaces/asyncdebouncerstate.md) - [AsyncQueuerOptions](../interfaces/asyncqueueroptions.md) +- [AsyncQueuerState](../interfaces/asyncqueuerstate.md) - [AsyncRateLimiterOptions](../interfaces/asyncratelimiteroptions.md) +- [AsyncRateLimiterState](../interfaces/asyncratelimiterstate.md) - [AsyncThrottlerOptions](../interfaces/asyncthrottleroptions.md) +- [AsyncThrottlerState](../interfaces/asyncthrottlerstate.md) - [BatcherOptions](../interfaces/batcheroptions.md) +- [BatcherState](../interfaces/batcherstate.md) - [DebouncerOptions](../interfaces/debounceroptions.md) +- [DebouncerState](../interfaces/debouncerstate.md) - [QueuerOptions](../interfaces/queueroptions.md) +- [QueuerState](../interfaces/queuerstate.md) - [RateLimiterOptions](../interfaces/ratelimiteroptions.md) +- [RateLimiterState](../interfaces/ratelimiterstate.md) - [ThrottlerOptions](../interfaces/throttleroptions.md) +- [ThrottlerState](../interfaces/throttlerstate.md) ## Type Aliases @@ -40,19 +52,15 @@ title: "@tanstack/pacer" ## Functions +- [asyncBatch](../functions/asyncbatch.md) - [asyncDebounce](../functions/asyncdebounce.md) - [asyncQueue](../functions/asyncqueue.md) - [asyncRateLimit](../functions/asyncratelimit.md) - [asyncThrottle](../functions/asyncthrottle.md) - [batch](../functions/batch.md) -- [bindInstanceMethods](../functions/bindinstancemethods.md) - [debounce](../functions/debounce.md) - [isFunction](../functions/isfunction.md) -- [isPlainArray](../functions/isplainarray.md) -- [isPlainObject](../functions/isplainobject.md) - [parseFunctionOrValue](../functions/parsefunctionorvalue.md) - [queue](../functions/queue.md) - [rateLimit](../functions/ratelimit.md) -- [replaceEqualDeep](../functions/replaceequaldeep.md) -- [shallowEqualObjects](../functions/shallowequalobjects.md) - [throttle](../functions/throttle.md) diff --git a/docs/reference/interfaces/asyncbatcheroptions.md b/docs/reference/interfaces/asyncbatcheroptions.md new file mode 100644 index 000000000..3ead4e04b --- /dev/null +++ b/docs/reference/interfaces/asyncbatcheroptions.md @@ -0,0 +1,249 @@ +--- +id: AsyncBatcherOptions +title: AsyncBatcherOptions +--- + + + +# Interface: AsyncBatcherOptions\ + +Defined in: [async-batcher.ts:86](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L86) + +Options for configuring an AsyncBatcher instance + +## Type Parameters + +• **TValue** + +## Properties + +### getShouldExecute()? + +```ts +optional getShouldExecute: (items, batcher) => boolean; +``` + +Defined in: [async-batcher.ts:91](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L91) + +Custom function to determine if a batch should be processed +Return true to process the batch immediately + +#### Parameters + +##### items + +`TValue`[] + +##### batcher + +[`AsyncBatcher`](../../classes/asyncbatcher.md)\<`TValue`\> + +#### Returns + +`boolean` + +*** + +### initialState? + +```ts +optional initialState: Partial>; +``` + +Defined in: [async-batcher.ts:98](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L98) + +Initial state for the async batcher + +*** + +### maxSize? + +```ts +optional maxSize: number; +``` + +Defined in: [async-batcher.ts:103](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L103) + +Maximum number of items in a batch + +#### Default + +```ts +Infinity +``` + +*** + +### onError()? + +```ts +optional onError: (error, failedItems, batcher) => void; +``` + +Defined in: [async-batcher.ts:109](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L109) + +Optional error handler for when the batch function throws. +If provided, the handler will be called with the error and batcher instance. +This can be used alongside throwOnError - the handler will be called before any error is thrown. + +#### Parameters + +##### error + +`unknown` + +##### failedItems + +`TValue`[] + +##### batcher + +[`AsyncBatcher`](../../classes/asyncbatcher.md)\<`TValue`\> + +#### Returns + +`void` + +*** + +### onExecute()? + +```ts +optional onExecute: (batcher) => void; +``` + +Defined in: [async-batcher.ts:117](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L117) + +Callback fired after a batch is processed + +#### Parameters + +##### batcher + +[`AsyncBatcher`](../../classes/asyncbatcher.md)\<`TValue`\> + +#### Returns + +`void` + +*** + +### onItemsChange()? + +```ts +optional onItemsChange: (batcher) => void; +``` + +Defined in: [async-batcher.ts:121](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L121) + +Callback fired after items are added to the batcher + +#### Parameters + +##### batcher + +[`AsyncBatcher`](../../classes/asyncbatcher.md)\<`TValue`\> + +#### Returns + +`void` + +*** + +### onSettled()? + +```ts +optional onSettled: (batcher) => void; +``` + +Defined in: [async-batcher.ts:125](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L125) + +Optional callback to call when a batch is settled (completed or failed) + +#### Parameters + +##### batcher + +[`AsyncBatcher`](../../classes/asyncbatcher.md)\<`TValue`\> + +#### Returns + +`void` + +*** + +### onSuccess()? + +```ts +optional onSuccess: (result, batcher) => void; +``` + +Defined in: [async-batcher.ts:129](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L129) + +Optional callback to call when a batch succeeds + +#### Parameters + +##### result + +`any` + +##### batcher + +[`AsyncBatcher`](../../classes/asyncbatcher.md)\<`TValue`\> + +#### Returns + +`void` + +*** + +### started? + +```ts +optional started: boolean; +``` + +Defined in: [async-batcher.ts:134](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L134) + +Whether the batcher should start processing immediately + +#### Default + +```ts +true +``` + +*** + +### throwOnError? + +```ts +optional throwOnError: boolean; +``` + +Defined in: [async-batcher.ts:140](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L140) + +Whether to throw errors when they occur. +Defaults to true if no onError handler is provided, false if an onError handler is provided. +Can be explicitly set to override these defaults. + +*** + +### wait? + +```ts +optional wait: number | (asyncBatcher) => number; +``` + +Defined in: [async-batcher.ts:147](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L147) + +Maximum time in milliseconds to wait before processing a batch. +If the wait duration has elapsed, the batch will be processed. +If not provided, the batch will not be triggered by a timeout. + +#### Default + +```ts +Infinity +``` diff --git a/docs/reference/interfaces/asyncbatcherstate.md b/docs/reference/interfaces/asyncbatcherstate.md new file mode 100644 index 000000000..cd81a0452 --- /dev/null +++ b/docs/reference/interfaces/asyncbatcherstate.md @@ -0,0 +1,182 @@ +--- +id: AsyncBatcherState +title: AsyncBatcherState +--- + + + +# Interface: AsyncBatcherState\ + +Defined in: [async-batcher.ts:5](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L5) + +## Type Parameters + +• **TValue** + +## Properties + +### errorCount + +```ts +errorCount: number; +``` + +Defined in: [async-batcher.ts:9](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L9) + +Number of batch executions that have resulted in errors + +*** + +### failedItems + +```ts +failedItems: TValue[]; +``` + +Defined in: [async-batcher.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L13) + +Array of items that failed during batch processing + +*** + +### isEmpty + +```ts +isEmpty: boolean; +``` + +Defined in: [async-batcher.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L17) + +Whether the batcher has no items to process (items array is empty) + +*** + +### isExecuting + +```ts +isExecuting: boolean; +``` + +Defined in: [async-batcher.ts:21](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L21) + +Whether a batch is currently being processed asynchronously + +*** + +### isPending + +```ts +isPending: boolean; +``` + +Defined in: [async-batcher.ts:25](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L25) + +Whether the batcher is waiting for the timeout to trigger batch processing + +*** + +### isRunning + +```ts +isRunning: boolean; +``` + +Defined in: [async-batcher.ts:29](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L29) + +Whether the batcher is active and will process items automatically + +*** + +### items + +```ts +items: TValue[]; +``` + +Defined in: [async-batcher.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L33) + +Array of items currently queued for batch processing + +*** + +### lastResult + +```ts +lastResult: any; +``` + +Defined in: [async-batcher.ts:37](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L37) + +The result from the most recent batch execution + +*** + +### settleCount + +```ts +settleCount: number; +``` + +Defined in: [async-batcher.ts:41](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L41) + +Number of batch executions that have completed (either successfully or with errors) + +*** + +### size + +```ts +size: number; +``` + +Defined in: [async-batcher.ts:45](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L45) + +Number of items currently in the batch queue + +*** + +### status + +```ts +status: "idle" | "pending" | "executing" | "populated"; +``` + +Defined in: [async-batcher.ts:49](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L49) + +Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured + +*** + +### successCount + +```ts +successCount: number; +``` + +Defined in: [async-batcher.ts:53](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L53) + +Number of batch executions that have completed successfully + +*** + +### totalItemsFailed + +```ts +totalItemsFailed: number; +``` + +Defined in: [async-batcher.ts:61](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L61) + +Total number of items that have failed processing across all batches + +*** + +### totalItemsProcessed + +```ts +totalItemsProcessed: number; +``` + +Defined in: [async-batcher.ts:57](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-batcher.ts#L57) + +Total number of items that have been processed across all batches diff --git a/docs/reference/interfaces/asyncdebounceroptions.md b/docs/reference/interfaces/asyncdebounceroptions.md index 21a71de5b..3ff9f7246 100644 --- a/docs/reference/interfaces/asyncdebounceroptions.md +++ b/docs/reference/interfaces/asyncdebounceroptions.md @@ -7,7 +7,7 @@ title: AsyncDebouncerOptions # Interface: AsyncDebouncerOptions\ -Defined in: [async-debouncer.ts:7](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L7) +Defined in: [async-debouncer.ts:63](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L63) Options for configuring an async debounced function @@ -23,7 +23,7 @@ Options for configuring an async debounced function optional enabled: boolean | (debouncer) => boolean; ``` -Defined in: [async-debouncer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L13) +Defined in: [async-debouncer.ts:69](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L69) Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions. Can be a boolean or a function that returns a boolean. @@ -31,13 +31,25 @@ Defaults to true. *** +### initialState? + +```ts +optional initialState: Partial>; +``` + +Defined in: [async-debouncer.ts:73](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L73) + +Initial state for the async debouncer + +*** + ### leading? ```ts optional leading: boolean; ``` -Defined in: [async-debouncer.ts:18](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L18) +Defined in: [async-debouncer.ts:78](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L78) Whether to execute on the leading edge of the timeout. Defaults to false. @@ -50,7 +62,7 @@ Defaults to false. optional onError: (error, debouncer) => void; ``` -Defined in: [async-debouncer.ts:24](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L24) +Defined in: [async-debouncer.ts:84](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L84) Optional error handler for when the debounced function throws. If provided, the handler will be called with the error and debouncer instance. @@ -78,7 +90,7 @@ This can be used alongside throwOnError - the handler will be called before any optional onSettled: (debouncer) => void; ``` -Defined in: [async-debouncer.ts:28](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L28) +Defined in: [async-debouncer.ts:88](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L88) Optional callback to call when the debounced function is executed @@ -100,7 +112,7 @@ Optional callback to call when the debounced function is executed optional onSuccess: (result, debouncer) => void; ``` -Defined in: [async-debouncer.ts:32](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L32) +Defined in: [async-debouncer.ts:92](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L92) Optional callback to call when the debounced function is executed @@ -126,7 +138,7 @@ Optional callback to call when the debounced function is executed optional throwOnError: boolean; ``` -Defined in: [async-debouncer.ts:38](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L38) +Defined in: [async-debouncer.ts:98](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L98) Whether to throw errors when they occur. Defaults to true if no onError handler is provided, false if an onError handler is provided. @@ -140,7 +152,7 @@ Can be explicitly set to override these defaults. optional trailing: boolean; ``` -Defined in: [async-debouncer.ts:43](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L43) +Defined in: [async-debouncer.ts:103](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L103) Whether to execute on the trailing edge of the timeout. Defaults to true. @@ -153,7 +165,7 @@ Defaults to true. wait: number | (debouncer) => number; ``` -Defined in: [async-debouncer.ts:49](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L49) +Defined in: [async-debouncer.ts:109](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L109) Delay in milliseconds to wait after the last call before executing. Can be a number or a function that returns a number. diff --git a/docs/reference/interfaces/asyncdebouncerstate.md b/docs/reference/interfaces/asyncdebouncerstate.md new file mode 100644 index 000000000..a106a2ff4 --- /dev/null +++ b/docs/reference/interfaces/asyncdebouncerstate.md @@ -0,0 +1,122 @@ +--- +id: AsyncDebouncerState +title: AsyncDebouncerState +--- + + + +# Interface: AsyncDebouncerState\ + +Defined in: [async-debouncer.ts:5](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L5) + +## Type Parameters + +• **TFn** *extends* [`AnyAsyncFunction`](../../type-aliases/anyasyncfunction.md) + +## Properties + +### canLeadingExecute + +```ts +canLeadingExecute: boolean; +``` + +Defined in: [async-debouncer.ts:9](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L9) + +Whether the debouncer can execute on the leading edge of the timeout + +*** + +### errorCount + +```ts +errorCount: number; +``` + +Defined in: [async-debouncer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L13) + +Number of function executions that have resulted in errors + +*** + +### isExecuting + +```ts +isExecuting: boolean; +``` + +Defined in: [async-debouncer.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L17) + +Whether the debounced function is currently executing asynchronously + +*** + +### isPending + +```ts +isPending: boolean; +``` + +Defined in: [async-debouncer.ts:21](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L21) + +Whether the debouncer is waiting for the timeout to trigger execution + +*** + +### lastArgs + +```ts +lastArgs: undefined | Parameters; +``` + +Defined in: [async-debouncer.ts:25](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L25) + +The arguments from the most recent call to maybeExecute + +*** + +### lastResult + +```ts +lastResult: undefined | ReturnType; +``` + +Defined in: [async-debouncer.ts:29](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L29) + +The result from the most recent successful function execution + +*** + +### settleCount + +```ts +settleCount: number; +``` + +Defined in: [async-debouncer.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L33) + +Number of function executions that have completed (either successfully or with errors) + +*** + +### status + +```ts +status: "disabled" | "idle" | "pending" | "executing" | "settled"; +``` + +Defined in: [async-debouncer.ts:37](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L37) + +Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed + +*** + +### successCount + +```ts +successCount: number; +``` + +Defined in: [async-debouncer.ts:41](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L41) + +Number of function executions that have completed successfully diff --git a/docs/reference/interfaces/asyncqueueroptions.md b/docs/reference/interfaces/asyncqueueroptions.md index 43246585a..0e6d6f2ac 100644 --- a/docs/reference/interfaces/asyncqueueroptions.md +++ b/docs/reference/interfaces/asyncqueueroptions.md @@ -7,7 +7,7 @@ title: AsyncQueuerOptions # Interface: AsyncQueuerOptions\ -Defined in: [async-queuer.ts:5](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L5) +Defined in: [async-queuer.ts:94](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L94) ## Type Parameters @@ -21,7 +21,7 @@ Defined in: [async-queuer.ts:5](https://github.com/TanStack/pacer/blob/main/pack optional addItemsTo: QueuePosition; ``` -Defined in: [async-queuer.ts:10](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L10) +Defined in: [async-queuer.ts:99](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L99) Default position to add items to the queuer @@ -39,7 +39,7 @@ Default position to add items to the queuer optional concurrency: number | (queuer) => number; ``` -Defined in: [async-queuer.ts:16](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L16) +Defined in: [async-queuer.ts:105](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L105) Maximum number of concurrent tasks to process. Can be a number or a function that returns a number. @@ -58,7 +58,7 @@ Can be a number or a function that returns a number. optional expirationDuration: number; ``` -Defined in: [async-queuer.ts:21](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L21) +Defined in: [async-queuer.ts:110](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L110) Maximum time in milliseconds that an item can stay in the queue If not provided, items will never expire @@ -71,7 +71,7 @@ If not provided, items will never expire optional getIsExpired: (item, addedAt) => boolean; ``` -Defined in: [async-queuer.ts:26](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L26) +Defined in: [async-queuer.ts:115](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L115) Function to determine if an item has expired If provided, this overrides the expirationDuration behavior @@ -98,7 +98,7 @@ If provided, this overrides the expirationDuration behavior optional getItemsFrom: QueuePosition; ``` -Defined in: [async-queuer.ts:31](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L31) +Defined in: [async-queuer.ts:120](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L120) Default position to get items from during processing @@ -116,7 +116,7 @@ Default position to get items from during processing optional getPriority: (item) => number; ``` -Defined in: [async-queuer.ts:37](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L37) +Defined in: [async-queuer.ts:126](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L126) Function to determine priority of items in the queuer Higher priority items will be processed first @@ -140,19 +140,31 @@ If not provided, will use static priority values attached to tasks optional initialItems: TValue[]; ``` -Defined in: [async-queuer.ts:41](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L41) +Defined in: [async-queuer.ts:130](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L130) Initial items to populate the queuer with *** +### initialState? + +```ts +optional initialState: Partial>; +``` + +Defined in: [async-queuer.ts:134](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L134) + +Initial state for the async queuer + +*** + ### maxSize? ```ts optional maxSize: number; ``` -Defined in: [async-queuer.ts:45](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L45) +Defined in: [async-queuer.ts:138](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L138) Maximum number of items allowed in the queuer @@ -164,7 +176,7 @@ Maximum number of items allowed in the queuer optional onError: (error, queuer) => void; ``` -Defined in: [async-queuer.ts:51](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L51) +Defined in: [async-queuer.ts:144](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L144) Optional error handler for when a task throws. If provided, the handler will be called with the error and queuer instance. @@ -192,7 +204,7 @@ This can be used alongside throwOnError - the handler will be called before any optional onExpire: (item, queuer) => void; ``` -Defined in: [async-queuer.ts:55](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L55) +Defined in: [async-queuer.ts:148](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L148) Callback fired whenever an item expires in the queuer @@ -212,35 +224,13 @@ Callback fired whenever an item expires in the queuer *** -### onIsRunningChange()? - -```ts -optional onIsRunningChange: (queuer) => void; -``` - -Defined in: [async-queuer.ts:59](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L59) - -Callback fired whenever the queuer's running state changes - -#### Parameters - -##### queuer - -[`AsyncQueuer`](../../classes/asyncqueuer.md)\<`TValue`\> - -#### Returns - -`void` - -*** - ### onItemsChange()? ```ts optional onItemsChange: (queuer) => void; ``` -Defined in: [async-queuer.ts:63](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L63) +Defined in: [async-queuer.ts:152](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L152) Callback fired whenever an item is added or removed from the queuer @@ -262,7 +252,7 @@ Callback fired whenever an item is added or removed from the queuer optional onReject: (item, queuer) => void; ``` -Defined in: [async-queuer.ts:67](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L67) +Defined in: [async-queuer.ts:156](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L156) Callback fired whenever an item is rejected from being added to the queuer @@ -288,7 +278,7 @@ Callback fired whenever an item is rejected from being added to the queuer optional onSettled: (queuer) => void; ``` -Defined in: [async-queuer.ts:71](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L71) +Defined in: [async-queuer.ts:160](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L160) Optional callback to call when a task is settled @@ -310,7 +300,7 @@ Optional callback to call when a task is settled optional onSuccess: (result, queuer) => void; ``` -Defined in: [async-queuer.ts:75](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L75) +Defined in: [async-queuer.ts:164](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L164) Optional callback to call when a task succeeds @@ -336,7 +326,7 @@ Optional callback to call when a task succeeds optional started: boolean; ``` -Defined in: [async-queuer.ts:79](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L79) +Defined in: [async-queuer.ts:168](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L168) Whether the queuer should start processing tasks immediately or not. @@ -348,7 +338,7 @@ Whether the queuer should start processing tasks immediately or not. optional throwOnError: boolean; ``` -Defined in: [async-queuer.ts:85](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L85) +Defined in: [async-queuer.ts:174](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L174) Whether to throw errors when they occur. Defaults to true if no onError handler is provided, false if an onError handler is provided. @@ -362,7 +352,7 @@ Can be explicitly set to override these defaults. optional wait: number | (queuer) => number; ``` -Defined in: [async-queuer.ts:91](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L91) +Defined in: [async-queuer.ts:180](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L180) Time in milliseconds to wait between processing items. Can be a number or a function that returns a number. diff --git a/docs/reference/interfaces/asyncqueuerstate.md b/docs/reference/interfaces/asyncqueuerstate.md new file mode 100644 index 000000000..6413f5fbb --- /dev/null +++ b/docs/reference/interfaces/asyncqueuerstate.md @@ -0,0 +1,206 @@ +--- +id: AsyncQueuerState +title: AsyncQueuerState +--- + + + +# Interface: AsyncQueuerState\ + +Defined in: [async-queuer.ts:6](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L6) + +## Type Parameters + +• **TValue** + +## Properties + +### activeItems + +```ts +activeItems: TValue[]; +``` + +Defined in: [async-queuer.ts:10](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L10) + +Items currently being processed by the queuer + +*** + +### errorCount + +```ts +errorCount: number; +``` + +Defined in: [async-queuer.ts:14](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L14) + +Number of task executions that have resulted in errors + +*** + +### expirationCount + +```ts +expirationCount: number; +``` + +Defined in: [async-queuer.ts:18](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L18) + +Number of items that have been removed from the queue due to expiration + +*** + +### isEmpty + +```ts +isEmpty: boolean; +``` + +Defined in: [async-queuer.ts:22](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L22) + +Whether the queuer has no items to process (items array is empty) + +*** + +### isFull + +```ts +isFull: boolean; +``` + +Defined in: [async-queuer.ts:26](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L26) + +Whether the queuer has reached its maximum capacity + +*** + +### isIdle + +```ts +isIdle: boolean; +``` + +Defined in: [async-queuer.ts:30](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L30) + +Whether the queuer is not currently processing any items + +*** + +### isRunning + +```ts +isRunning: boolean; +``` + +Defined in: [async-queuer.ts:34](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L34) + +Whether the queuer is active and will process items automatically + +*** + +### items + +```ts +items: TValue[]; +``` + +Defined in: [async-queuer.ts:42](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L42) + +Array of items currently waiting to be processed + +*** + +### itemTimestamps + +```ts +itemTimestamps: number[]; +``` + +Defined in: [async-queuer.ts:38](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L38) + +Timestamps when items were added to the queue for expiration tracking + +*** + +### lastResult + +```ts +lastResult: any; +``` + +Defined in: [async-queuer.ts:46](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L46) + +The result from the most recent task execution + +*** + +### pendingTick + +```ts +pendingTick: boolean; +``` + +Defined in: [async-queuer.ts:50](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L50) + +Whether the queuer has a pending timeout for processing the next item + +*** + +### rejectionCount + +```ts +rejectionCount: number; +``` + +Defined in: [async-queuer.ts:54](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L54) + +Number of items that have been rejected from being added to the queue + +*** + +### settledCount + +```ts +settledCount: number; +``` + +Defined in: [async-queuer.ts:58](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L58) + +Number of task executions that have completed (either successfully or with errors) + +*** + +### size + +```ts +size: number; +``` + +Defined in: [async-queuer.ts:62](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L62) + +Number of items currently in the queue + +*** + +### status + +```ts +status: "idle" | "running" | "stopped"; +``` + +Defined in: [async-queuer.ts:66](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L66) + +Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused + +*** + +### successCount + +```ts +successCount: number; +``` + +Defined in: [async-queuer.ts:70](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L70) + +Number of task executions that have completed successfully diff --git a/docs/reference/interfaces/asyncratelimiteroptions.md b/docs/reference/interfaces/asyncratelimiteroptions.md index 9a8e4b169..3f74064ff 100644 --- a/docs/reference/interfaces/asyncratelimiteroptions.md +++ b/docs/reference/interfaces/asyncratelimiteroptions.md @@ -7,7 +7,7 @@ title: AsyncRateLimiterOptions # Interface: AsyncRateLimiterOptions\ -Defined in: [async-rate-limiter.ts:7](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L7) +Defined in: [async-rate-limiter.ts:53](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L53) Options for configuring an async rate-limited function @@ -23,7 +23,7 @@ Options for configuring an async rate-limited function optional enabled: boolean | (rateLimiter) => boolean; ``` -Defined in: [async-rate-limiter.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L13) +Defined in: [async-rate-limiter.ts:59](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L59) Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions. Can be a boolean or a function that returns a boolean. @@ -31,13 +31,25 @@ Defaults to true. *** +### initialState? + +```ts +optional initialState: Partial>; +``` + +Defined in: [async-rate-limiter.ts:63](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L63) + +Initial state for the rate limiter + +*** + ### limit ```ts limit: number | (rateLimiter) => number; ``` -Defined in: [async-rate-limiter.ts:18](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L18) +Defined in: [async-rate-limiter.ts:68](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L68) Maximum number of executions allowed within the time window. Can be a number or a function that returns a number. @@ -50,7 +62,7 @@ Can be a number or a function that returns a number. optional onError: (error, rateLimiter) => void; ``` -Defined in: [async-rate-limiter.ts:24](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L24) +Defined in: [async-rate-limiter.ts:74](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L74) Optional error handler for when the rate-limited function throws. If provided, the handler will be called with the error and rate limiter instance. @@ -78,7 +90,7 @@ This can be used alongside throwOnError - the handler will be called before any optional onReject: (rateLimiter) => void; ``` -Defined in: [async-rate-limiter.ts:28](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L28) +Defined in: [async-rate-limiter.ts:78](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L78) Optional callback function that is called when an execution is rejected due to rate limiting @@ -100,7 +112,7 @@ Optional callback function that is called when an execution is rejected due to r optional onSettled: (rateLimiter) => void; ``` -Defined in: [async-rate-limiter.ts:32](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L32) +Defined in: [async-rate-limiter.ts:82](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L82) Optional function to call when the rate-limited function is executed @@ -122,7 +134,7 @@ Optional function to call when the rate-limited function is executed optional onSuccess: (result, rateLimiter) => void; ``` -Defined in: [async-rate-limiter.ts:36](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L36) +Defined in: [async-rate-limiter.ts:86](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L86) Optional function to call when the rate-limited function is executed @@ -148,7 +160,7 @@ Optional function to call when the rate-limited function is executed optional throwOnError: boolean; ``` -Defined in: [async-rate-limiter.ts:45](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L45) +Defined in: [async-rate-limiter.ts:95](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L95) Whether to throw errors when they occur. Defaults to true if no onError handler is provided, false if an onError handler is provided. @@ -162,7 +174,7 @@ Can be explicitly set to override these defaults. window: number | (rateLimiter) => number; ``` -Defined in: [async-rate-limiter.ts:50](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L50) +Defined in: [async-rate-limiter.ts:100](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L100) Time window in milliseconds within which the limit applies. Can be a number or a function that returns a number. @@ -175,7 +187,7 @@ Can be a number or a function that returns a number. optional windowType: "fixed" | "sliding"; ``` -Defined in: [async-rate-limiter.ts:57](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L57) +Defined in: [async-rate-limiter.ts:107](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L107) Type of window to use for rate limiting - 'fixed': Uses a fixed window that resets after the window period diff --git a/docs/reference/interfaces/asyncratelimiterstate.md b/docs/reference/interfaces/asyncratelimiterstate.md new file mode 100644 index 000000000..d85a9fb0c --- /dev/null +++ b/docs/reference/interfaces/asyncratelimiterstate.md @@ -0,0 +1,98 @@ +--- +id: AsyncRateLimiterState +title: AsyncRateLimiterState +--- + + + +# Interface: AsyncRateLimiterState\ + +Defined in: [async-rate-limiter.ts:5](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L5) + +## Type Parameters + +• **TFn** *extends* [`AnyAsyncFunction`](../../type-aliases/anyasyncfunction.md) + +## Properties + +### errorCount + +```ts +errorCount: number; +``` + +Defined in: [async-rate-limiter.ts:9](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L9) + +Number of function executions that have resulted in errors + +*** + +### executionTimes + +```ts +executionTimes: number[]; +``` + +Defined in: [async-rate-limiter.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L13) + +Array of timestamps when executions occurred for rate limiting calculations + +*** + +### isExecuting + +```ts +isExecuting: boolean; +``` + +Defined in: [async-rate-limiter.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L17) + +Whether the rate-limited function is currently executing asynchronously + +*** + +### lastResult + +```ts +lastResult: undefined | ReturnType; +``` + +Defined in: [async-rate-limiter.ts:21](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L21) + +The result from the most recent successful function execution + +*** + +### rejectionCount + +```ts +rejectionCount: number; +``` + +Defined in: [async-rate-limiter.ts:25](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L25) + +Number of function executions that have been rejected due to rate limiting + +*** + +### settleCount + +```ts +settleCount: number; +``` + +Defined in: [async-rate-limiter.ts:29](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L29) + +Number of function executions that have completed (either successfully or with errors) + +*** + +### successCount + +```ts +successCount: number; +``` + +Defined in: [async-rate-limiter.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L33) + +Number of function executions that have completed successfully diff --git a/docs/reference/interfaces/asyncthrottleroptions.md b/docs/reference/interfaces/asyncthrottleroptions.md index acac7dc5b..08ac75a5b 100644 --- a/docs/reference/interfaces/asyncthrottleroptions.md +++ b/docs/reference/interfaces/asyncthrottleroptions.md @@ -7,7 +7,7 @@ title: AsyncThrottlerOptions # Interface: AsyncThrottlerOptions\ -Defined in: [async-throttler.ts:7](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L7) +Defined in: [async-throttler.ts:68](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L68) Options for configuring an async throttled function @@ -23,7 +23,7 @@ Options for configuring an async throttled function optional enabled: boolean | (throttler) => boolean; ``` -Defined in: [async-throttler.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L13) +Defined in: [async-throttler.ts:74](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L74) Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions. Can be a boolean or a function that returns a boolean. @@ -31,13 +31,25 @@ Defaults to true. *** +### initialState? + +```ts +optional initialState: Partial>; +``` + +Defined in: [async-throttler.ts:78](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L78) + +Initial state for the async throttler + +*** + ### leading? ```ts optional leading: boolean; ``` -Defined in: [async-throttler.ts:18](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L18) +Defined in: [async-throttler.ts:83](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L83) Whether to execute the function immediately when called Defaults to true @@ -50,7 +62,7 @@ Defaults to true optional onError: (error, asyncThrottler) => void; ``` -Defined in: [async-throttler.ts:24](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L24) +Defined in: [async-throttler.ts:89](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L89) Optional error handler for when the throttled function throws. If provided, the handler will be called with the error and throttler instance. @@ -78,7 +90,7 @@ This can be used alongside throwOnError - the handler will be called before any optional onSettled: (asyncThrottler) => void; ``` -Defined in: [async-throttler.ts:28](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L28) +Defined in: [async-throttler.ts:93](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L93) Optional function to call when the throttled function is executed @@ -100,7 +112,7 @@ Optional function to call when the throttled function is executed optional onSuccess: (result, asyncThrottler) => void; ``` -Defined in: [async-throttler.ts:32](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L32) +Defined in: [async-throttler.ts:97](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L97) Optional function to call when the throttled function is executed @@ -126,7 +138,7 @@ Optional function to call when the throttled function is executed optional throwOnError: boolean; ``` -Defined in: [async-throttler.ts:41](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L41) +Defined in: [async-throttler.ts:106](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L106) Whether to throw errors when they occur. Defaults to true if no onError handler is provided, false if an onError handler is provided. @@ -140,7 +152,7 @@ Can be explicitly set to override these defaults. optional trailing: boolean; ``` -Defined in: [async-throttler.ts:46](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L46) +Defined in: [async-throttler.ts:111](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L111) Whether to execute the function on the trailing edge of the wait period Defaults to true @@ -153,7 +165,7 @@ Defaults to true wait: number | (throttler) => number; ``` -Defined in: [async-throttler.ts:52](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L52) +Defined in: [async-throttler.ts:117](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L117) Time window in milliseconds during which the function can only be executed once. Can be a number or a function that returns a number. diff --git a/docs/reference/interfaces/asyncthrottlerstate.md b/docs/reference/interfaces/asyncthrottlerstate.md new file mode 100644 index 000000000..abb70ea27 --- /dev/null +++ b/docs/reference/interfaces/asyncthrottlerstate.md @@ -0,0 +1,134 @@ +--- +id: AsyncThrottlerState +title: AsyncThrottlerState +--- + + + +# Interface: AsyncThrottlerState\ + +Defined in: [async-throttler.ts:5](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L5) + +## Type Parameters + +• **TFn** *extends* [`AnyAsyncFunction`](../../type-aliases/anyasyncfunction.md) + +## Properties + +### errorCount + +```ts +errorCount: number; +``` + +Defined in: [async-throttler.ts:9](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L9) + +Number of function executions that have resulted in errors + +*** + +### isExecuting + +```ts +isExecuting: boolean; +``` + +Defined in: [async-throttler.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L13) + +Whether the throttled function is currently executing asynchronously + +*** + +### isPending + +```ts +isPending: boolean; +``` + +Defined in: [async-throttler.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L17) + +Whether the throttler is waiting for the timeout to trigger execution + +*** + +### lastArgs + +```ts +lastArgs: undefined | Parameters; +``` + +Defined in: [async-throttler.ts:21](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L21) + +The arguments from the most recent call to maybeExecute + +*** + +### lastExecutionTime + +```ts +lastExecutionTime: number; +``` + +Defined in: [async-throttler.ts:25](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L25) + +Timestamp of the last function execution in milliseconds + +*** + +### lastResult + +```ts +lastResult: undefined | ReturnType; +``` + +Defined in: [async-throttler.ts:29](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L29) + +The result from the most recent successful function execution + +*** + +### nextExecutionTime + +```ts +nextExecutionTime: number; +``` + +Defined in: [async-throttler.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L33) + +Timestamp when the next execution can occur in milliseconds + +*** + +### settleCount + +```ts +settleCount: number; +``` + +Defined in: [async-throttler.ts:37](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L37) + +Number of function executions that have completed (either successfully or with errors) + +*** + +### status + +```ts +status: "disabled" | "idle" | "pending" | "executing" | "settled"; +``` + +Defined in: [async-throttler.ts:41](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L41) + +Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed + +*** + +### successCount + +```ts +successCount: number; +``` + +Defined in: [async-throttler.ts:45](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L45) + +Number of function executions that have completed successfully diff --git a/docs/reference/interfaces/batcheroptions.md b/docs/reference/interfaces/batcheroptions.md index 4ea283744..1c75f0319 100644 --- a/docs/reference/interfaces/batcheroptions.md +++ b/docs/reference/interfaces/batcheroptions.md @@ -7,7 +7,7 @@ title: BatcherOptions # Interface: BatcherOptions\ -Defined in: [batcher.ts:6](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L6) +Defined in: [batcher.ts:56](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L56) Options for configuring a Batcher instance @@ -23,7 +23,7 @@ Options for configuring a Batcher instance optional getShouldExecute: (items, batcher) => boolean; ``` -Defined in: [batcher.ts:11](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L11) +Defined in: [batcher.ts:61](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L61) Custom function to determine if a batch should be processed Return true to process the batch immediately @@ -44,13 +44,25 @@ Return true to process the batch immediately *** +### initialState? + +```ts +optional initialState: Partial>; +``` + +Defined in: [batcher.ts:65](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L65) + +Initial state for the batcher + +*** + ### maxSize? ```ts optional maxSize: number; ``` -Defined in: [batcher.ts:16](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L16) +Defined in: [batcher.ts:70](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L70) Maximum number of items in a batch @@ -68,7 +80,7 @@ Infinity optional onExecute: (batcher) => void; ``` -Defined in: [batcher.ts:20](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L20) +Defined in: [batcher.ts:74](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L74) Callback fired after a batch is processed @@ -84,35 +96,13 @@ Callback fired after a batch is processed *** -### onIsRunningChange()? - -```ts -optional onIsRunningChange: (batcher) => void; -``` - -Defined in: [batcher.ts:24](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L24) - -Callback fired when the batcher's running state changes - -#### Parameters - -##### batcher - -[`Batcher`](../../classes/batcher.md)\<`TValue`\> - -#### Returns - -`void` - -*** - ### onItemsChange()? ```ts optional onItemsChange: (batcher) => void; ``` -Defined in: [batcher.ts:28](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L28) +Defined in: [batcher.ts:78](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L78) Callback fired after items are added to the batcher @@ -134,7 +124,7 @@ Callback fired after items are added to the batcher optional started: boolean; ``` -Defined in: [batcher.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L33) +Defined in: [batcher.ts:83](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L83) Whether the batcher should start processing immediately @@ -149,10 +139,10 @@ true ### wait? ```ts -optional wait: number; +optional wait: number | (batcher) => number; ``` -Defined in: [batcher.ts:40](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L40) +Defined in: [batcher.ts:90](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L90) Maximum time in milliseconds to wait before processing a batch. If the wait duration has elapsed, the batch will be processed. diff --git a/docs/reference/interfaces/batcherstate.md b/docs/reference/interfaces/batcherstate.md new file mode 100644 index 000000000..841ccdffc --- /dev/null +++ b/docs/reference/interfaces/batcherstate.md @@ -0,0 +1,110 @@ +--- +id: BatcherState +title: BatcherState +--- + + + +# Interface: BatcherState\ + +Defined in: [batcher.ts:5](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L5) + +## Type Parameters + +• **TValue** + +## Properties + +### executionCount + +```ts +executionCount: number; +``` + +Defined in: [batcher.ts:9](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L9) + +Number of batch executions that have been completed + +*** + +### isEmpty + +```ts +isEmpty: boolean; +``` + +Defined in: [batcher.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L13) + +Whether the batcher has no items to process (items array is empty) + +*** + +### isPending + +```ts +isPending: boolean; +``` + +Defined in: [batcher.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L17) + +Whether the batcher is waiting for the timeout to trigger batch processing + +*** + +### isRunning + +```ts +isRunning: boolean; +``` + +Defined in: [batcher.ts:21](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L21) + +Whether the batcher is active and will process items automatically + +*** + +### items + +```ts +items: TValue[]; +``` + +Defined in: [batcher.ts:29](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L29) + +Array of items currently queued for batch processing + +*** + +### size + +```ts +size: number; +``` + +Defined in: [batcher.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L33) + +Number of items currently in the batch queue + +*** + +### status + +```ts +status: "idle" | "pending"; +``` + +Defined in: [batcher.ts:37](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L37) + +Current processing status - 'idle' when not processing, 'pending' when waiting for timeout + +*** + +### totalItemsProcessed + +```ts +totalItemsProcessed: number; +``` + +Defined in: [batcher.ts:25](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L25) + +Total number of items that have been processed across all batches diff --git a/docs/reference/interfaces/debounceroptions.md b/docs/reference/interfaces/debounceroptions.md index 4ec4b9290..56e6ba679 100644 --- a/docs/reference/interfaces/debounceroptions.md +++ b/docs/reference/interfaces/debounceroptions.md @@ -7,7 +7,7 @@ title: DebouncerOptions # Interface: DebouncerOptions\ -Defined in: [debouncer.ts:7](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L7) +Defined in: [debouncer.ts:43](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L43) Options for configuring a debounced function @@ -23,7 +23,7 @@ Options for configuring a debounced function optional enabled: boolean | (debouncer) => boolean; ``` -Defined in: [debouncer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L13) +Defined in: [debouncer.ts:49](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L49) Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions. Can be a boolean or a function that returns a boolean. @@ -31,13 +31,25 @@ Defaults to true. *** +### initialState? + +```ts +optional initialState: Partial>; +``` + +Defined in: [debouncer.ts:53](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L53) + +Initial state for the debouncer + +*** + ### leading? ```ts optional leading: boolean; ``` -Defined in: [debouncer.ts:19](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L19) +Defined in: [debouncer.ts:59](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L59) Whether to execute on the leading edge of the timeout. The first call will execute immediately and the rest will wait the delay. @@ -51,7 +63,7 @@ Defaults to false. optional onExecute: (debouncer) => void; ``` -Defined in: [debouncer.ts:23](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L23) +Defined in: [debouncer.ts:63](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L63) Callback function that is called after the function is executed @@ -73,7 +85,7 @@ Callback function that is called after the function is executed optional trailing: boolean; ``` -Defined in: [debouncer.ts:28](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L28) +Defined in: [debouncer.ts:68](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L68) Whether to execute on the trailing edge of the timeout. Defaults to true. @@ -86,7 +98,7 @@ Defaults to true. wait: number | (debouncer) => number; ``` -Defined in: [debouncer.ts:34](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L34) +Defined in: [debouncer.ts:74](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L74) Delay in milliseconds before executing the function. Can be a number or a function that returns a number. diff --git a/docs/reference/interfaces/debouncerstate.md b/docs/reference/interfaces/debouncerstate.md new file mode 100644 index 000000000..728ee596b --- /dev/null +++ b/docs/reference/interfaces/debouncerstate.md @@ -0,0 +1,74 @@ +--- +id: DebouncerState +title: DebouncerState +--- + + + +# Interface: DebouncerState\ + +Defined in: [debouncer.ts:5](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L5) + +## Type Parameters + +• **TFn** *extends* [`AnyFunction`](../../type-aliases/anyfunction.md) + +## Properties + +### canLeadingExecute + +```ts +canLeadingExecute: boolean; +``` + +Defined in: [debouncer.ts:9](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L9) + +Whether the debouncer can execute on the leading edge of the timeout + +*** + +### executionCount + +```ts +executionCount: number; +``` + +Defined in: [debouncer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L13) + +Number of function executions that have been completed + +*** + +### isPending + +```ts +isPending: boolean; +``` + +Defined in: [debouncer.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L17) + +Whether the debouncer is waiting for the timeout to trigger execution + +*** + +### lastArgs + +```ts +lastArgs: undefined | Parameters; +``` + +Defined in: [debouncer.ts:21](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L21) + +The arguments from the most recent call to maybeExecute + +*** + +### status + +```ts +status: "disabled" | "idle" | "pending"; +``` + +Defined in: [debouncer.ts:25](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L25) + +Current execution status - 'idle' when not active, 'pending' when waiting for timeout diff --git a/docs/reference/interfaces/queueroptions.md b/docs/reference/interfaces/queueroptions.md index f2ba260e1..cab583fa7 100644 --- a/docs/reference/interfaces/queueroptions.md +++ b/docs/reference/interfaces/queueroptions.md @@ -7,7 +7,7 @@ title: QueuerOptions # Interface: QueuerOptions\ -Defined in: [queuer.ts:8](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L8) +Defined in: [queuer.ts:77](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L77) Options for configuring a Queuer instance. @@ -25,7 +25,7 @@ These options control queue behavior, item expiration, callbacks, and more. optional addItemsTo: QueuePosition; ``` -Defined in: [queuer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L13) +Defined in: [queuer.ts:82](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L82) Default position to add items to the queuer @@ -43,7 +43,7 @@ Default position to add items to the queuer optional expirationDuration: number; ``` -Defined in: [queuer.ts:18](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L18) +Defined in: [queuer.ts:87](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L87) Maximum time in milliseconds that an item can stay in the queue If not provided, items will never expire @@ -56,7 +56,7 @@ If not provided, items will never expire optional getIsExpired: (item, addedAt) => boolean; ``` -Defined in: [queuer.ts:23](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L23) +Defined in: [queuer.ts:92](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L92) Function to determine if an item has expired If provided, this overrides the expirationDuration behavior @@ -83,7 +83,7 @@ If provided, this overrides the expirationDuration behavior optional getItemsFrom: QueuePosition; ``` -Defined in: [queuer.ts:28](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L28) +Defined in: [queuer.ts:97](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L97) Default position to get items from during processing @@ -101,7 +101,7 @@ Default position to get items from during processing optional getPriority: (item) => number; ``` -Defined in: [queuer.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L33) +Defined in: [queuer.ts:102](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L102) Function to determine priority of items in the queuer Higher priority items will be processed first @@ -124,19 +124,31 @@ Higher priority items will be processed first optional initialItems: TValue[]; ``` -Defined in: [queuer.ts:37](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L37) +Defined in: [queuer.ts:106](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L106) Initial items to populate the queuer with *** +### initialState? + +```ts +optional initialState: Partial>; +``` + +Defined in: [queuer.ts:110](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L110) + +Initial state for the queuer + +*** + ### maxSize? ```ts optional maxSize: number; ``` -Defined in: [queuer.ts:41](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L41) +Defined in: [queuer.ts:114](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L114) Maximum number of items allowed in the queuer @@ -148,7 +160,7 @@ Maximum number of items allowed in the queuer optional onExecute: (item, queuer) => void; ``` -Defined in: [queuer.ts:49](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L49) +Defined in: [queuer.ts:122](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L122) Callback fired whenever an item is removed from the queuer @@ -174,7 +186,7 @@ Callback fired whenever an item is removed from the queuer optional onExpire: (item, queuer) => void; ``` -Defined in: [queuer.ts:45](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L45) +Defined in: [queuer.ts:118](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L118) Callback fired whenever an item expires in the queuer @@ -194,35 +206,13 @@ Callback fired whenever an item expires in the queuer *** -### onIsRunningChange()? - -```ts -optional onIsRunningChange: (queuer) => void; -``` - -Defined in: [queuer.ts:53](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L53) - -Callback fired whenever the queuer's running state changes - -#### Parameters - -##### queuer - -[`Queuer`](../../classes/queuer.md)\<`TValue`\> - -#### Returns - -`void` - -*** - ### onItemsChange()? ```ts optional onItemsChange: (queuer) => void; ``` -Defined in: [queuer.ts:57](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L57) +Defined in: [queuer.ts:126](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L126) Callback fired whenever an item is added or removed from the queuer @@ -244,7 +234,7 @@ Callback fired whenever an item is added or removed from the queuer optional onReject: (item, queuer) => void; ``` -Defined in: [queuer.ts:61](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L61) +Defined in: [queuer.ts:130](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L130) Callback fired whenever an item is rejected from being added to the queuer @@ -270,7 +260,7 @@ Callback fired whenever an item is rejected from being added to the queuer optional started: boolean; ``` -Defined in: [queuer.ts:65](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L65) +Defined in: [queuer.ts:134](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L134) Whether the queuer should start processing tasks immediately @@ -282,7 +272,7 @@ Whether the queuer should start processing tasks immediately optional wait: number | (queuer) => number; ``` -Defined in: [queuer.ts:71](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L71) +Defined in: [queuer.ts:140](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L140) Time in milliseconds to wait between processing items. Can be a number or a function that returns a number. diff --git a/docs/reference/interfaces/queuerstate.md b/docs/reference/interfaces/queuerstate.md new file mode 100644 index 000000000..ab6a3c5cf --- /dev/null +++ b/docs/reference/interfaces/queuerstate.md @@ -0,0 +1,158 @@ +--- +id: QueuerState +title: QueuerState +--- + + + +# Interface: QueuerState\ + +Defined in: [queuer.ts:4](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L4) + +## Type Parameters + +• **TValue** + +## Properties + +### executionCount + +```ts +executionCount: number; +``` + +Defined in: [queuer.ts:8](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L8) + +Number of items that have been processed by the queuer + +*** + +### expirationCount + +```ts +expirationCount: number; +``` + +Defined in: [queuer.ts:12](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L12) + +Number of items that have been removed from the queue due to expiration + +*** + +### isEmpty + +```ts +isEmpty: boolean; +``` + +Defined in: [queuer.ts:16](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L16) + +Whether the queuer has no items to process (items array is empty) + +*** + +### isFull + +```ts +isFull: boolean; +``` + +Defined in: [queuer.ts:20](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L20) + +Whether the queuer has reached its maximum capacity + +*** + +### isIdle + +```ts +isIdle: boolean; +``` + +Defined in: [queuer.ts:24](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L24) + +Whether the queuer is not currently processing any items + +*** + +### isRunning + +```ts +isRunning: boolean; +``` + +Defined in: [queuer.ts:28](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L28) + +Whether the queuer is active and will process items automatically + +*** + +### items + +```ts +items: TValue[]; +``` + +Defined in: [queuer.ts:36](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L36) + +Array of items currently waiting to be processed + +*** + +### itemTimestamps + +```ts +itemTimestamps: number[]; +``` + +Defined in: [queuer.ts:32](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L32) + +Timestamps when items were added to the queue for expiration tracking + +*** + +### pendingTick + +```ts +pendingTick: boolean; +``` + +Defined in: [queuer.ts:40](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L40) + +Whether the queuer has a pending timeout for processing the next item + +*** + +### rejectionCount + +```ts +rejectionCount: number; +``` + +Defined in: [queuer.ts:44](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L44) + +Number of items that have been rejected from being added to the queue + +*** + +### size + +```ts +size: number; +``` + +Defined in: [queuer.ts:48](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L48) + +Number of items currently in the queue + +*** + +### status + +```ts +status: "idle" | "running" | "stopped"; +``` + +Defined in: [queuer.ts:52](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L52) + +Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused diff --git a/docs/reference/interfaces/ratelimiteroptions.md b/docs/reference/interfaces/ratelimiteroptions.md index 59c1f4f4e..a30e1ebd0 100644 --- a/docs/reference/interfaces/ratelimiteroptions.md +++ b/docs/reference/interfaces/ratelimiteroptions.md @@ -7,7 +7,7 @@ title: RateLimiterOptions # Interface: RateLimiterOptions\ -Defined in: [rate-limiter.ts:7](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L7) +Defined in: [rate-limiter.ts:31](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L31) Options for configuring a rate-limited function @@ -23,20 +23,32 @@ Options for configuring a rate-limited function optional enabled: boolean | (rateLimiter) => boolean; ``` -Defined in: [rate-limiter.ts:12](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L12) +Defined in: [rate-limiter.ts:36](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L36) Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions. Defaults to true. *** +### initialState? + +```ts +optional initialState: Partial; +``` + +Defined in: [rate-limiter.ts:40](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L40) + +Initial state for the rate limiter + +*** + ### limit ```ts limit: number | (rateLimiter) => number; ``` -Defined in: [rate-limiter.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L17) +Defined in: [rate-limiter.ts:45](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L45) Maximum number of executions allowed within the time window. Can be a number or a callback function that receives the rate limiter instance and returns a number. @@ -49,7 +61,7 @@ Can be a number or a callback function that receives the rate limiter instance a optional onExecute: (rateLimiter) => void; ``` -Defined in: [rate-limiter.ts:21](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L21) +Defined in: [rate-limiter.ts:49](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L49) Callback function that is called after the function is executed @@ -71,7 +83,7 @@ Callback function that is called after the function is executed optional onReject: (rateLimiter) => void; ``` -Defined in: [rate-limiter.ts:25](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L25) +Defined in: [rate-limiter.ts:53](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L53) Optional callback function that is called when an execution is rejected due to rate limiting @@ -93,7 +105,7 @@ Optional callback function that is called when an execution is rejected due to r window: number | (rateLimiter) => number; ``` -Defined in: [rate-limiter.ts:30](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L30) +Defined in: [rate-limiter.ts:58](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L58) Time window in milliseconds within which the limit applies. Can be a number or a callback function that receives the rate limiter instance and returns a number. @@ -106,7 +118,7 @@ Can be a number or a callback function that receives the rate limiter instance a optional windowType: "fixed" | "sliding"; ``` -Defined in: [rate-limiter.ts:37](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L37) +Defined in: [rate-limiter.ts:65](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L65) Type of window to use for rate limiting - 'fixed': Uses a fixed window that resets after the window period diff --git a/docs/reference/interfaces/ratelimiterstate.md b/docs/reference/interfaces/ratelimiterstate.md new file mode 100644 index 000000000..1375ddd0f --- /dev/null +++ b/docs/reference/interfaces/ratelimiterstate.md @@ -0,0 +1,46 @@ +--- +id: RateLimiterState +title: RateLimiterState +--- + + + +# Interface: RateLimiterState + +Defined in: [rate-limiter.ts:5](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L5) + +## Properties + +### executionCount + +```ts +executionCount: number; +``` + +Defined in: [rate-limiter.ts:9](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L9) + +Number of function executions that have been completed + +*** + +### executionTimes + +```ts +executionTimes: number[]; +``` + +Defined in: [rate-limiter.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L13) + +Array of timestamps when executions occurred for rate limiting calculations + +*** + +### rejectionCount + +```ts +rejectionCount: number; +``` + +Defined in: [rate-limiter.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L17) + +Number of function executions that have been rejected due to rate limiting diff --git a/docs/reference/interfaces/throttleroptions.md b/docs/reference/interfaces/throttleroptions.md index 761ca59db..d59f1810a 100644 --- a/docs/reference/interfaces/throttleroptions.md +++ b/docs/reference/interfaces/throttleroptions.md @@ -7,7 +7,7 @@ title: ThrottlerOptions # Interface: ThrottlerOptions\ -Defined in: [throttler.ts:7](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L7) +Defined in: [throttler.ts:48](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L48) Options for configuring a throttled function @@ -23,7 +23,7 @@ Options for configuring a throttled function optional enabled: boolean | (throttler) => boolean; ``` -Defined in: [throttler.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L13) +Defined in: [throttler.ts:54](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L54) Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions. Can be a boolean or a function that returns a boolean. @@ -31,13 +31,25 @@ Defaults to true. *** +### initialState? + +```ts +optional initialState: Partial>; +``` + +Defined in: [throttler.ts:58](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L58) + +Initial state for the throttler + +*** + ### leading? ```ts optional leading: boolean; ``` -Defined in: [throttler.ts:18](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L18) +Defined in: [throttler.ts:63](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L63) Whether to execute on the leading edge of the timeout. Defaults to true. @@ -50,7 +62,7 @@ Defaults to true. optional onExecute: (throttler) => void; ``` -Defined in: [throttler.ts:22](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L22) +Defined in: [throttler.ts:67](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L67) Callback function that is called after the function is executed @@ -72,7 +84,7 @@ Callback function that is called after the function is executed optional trailing: boolean; ``` -Defined in: [throttler.ts:27](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L27) +Defined in: [throttler.ts:72](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L72) Whether to execute on the trailing edge of the timeout. Defaults to true. @@ -85,7 +97,7 @@ Defaults to true. wait: number | (throttler) => number; ``` -Defined in: [throttler.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L33) +Defined in: [throttler.ts:78](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L78) Time window in milliseconds during which the function can only be executed once. Can be a number or a function that returns a number. diff --git a/docs/reference/interfaces/throttlerstate.md b/docs/reference/interfaces/throttlerstate.md new file mode 100644 index 000000000..4f1488cd7 --- /dev/null +++ b/docs/reference/interfaces/throttlerstate.md @@ -0,0 +1,86 @@ +--- +id: ThrottlerState +title: ThrottlerState +--- + + + +# Interface: ThrottlerState\ + +Defined in: [throttler.ts:5](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L5) + +## Type Parameters + +• **TFn** *extends* [`AnyFunction`](../../type-aliases/anyfunction.md) + +## Properties + +### executionCount + +```ts +executionCount: number; +``` + +Defined in: [throttler.ts:9](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L9) + +Number of function executions that have been completed + +*** + +### isPending + +```ts +isPending: boolean; +``` + +Defined in: [throttler.ts:25](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L25) + +Whether the throttler is waiting for the timeout to trigger execution + +*** + +### lastArgs + +```ts +lastArgs: undefined | Parameters; +``` + +Defined in: [throttler.ts:13](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L13) + +The arguments from the most recent call to maybeExecute + +*** + +### lastExecutionTime + +```ts +lastExecutionTime: number; +``` + +Defined in: [throttler.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L17) + +Timestamp of the last function execution in milliseconds + +*** + +### nextExecutionTime + +```ts +nextExecutionTime: number; +``` + +Defined in: [throttler.ts:21](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L21) + +Timestamp when the next execution can occur in milliseconds + +*** + +### status + +```ts +status: "disabled" | "idle" | "pending"; +``` + +Defined in: [throttler.ts:29](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L29) + +Current execution status - 'idle' when not active, 'pending' when waiting for timeout diff --git a/docs/reference/type-aliases/queueposition.md b/docs/reference/type-aliases/queueposition.md index e8ae3e53c..4bd7fef25 100644 --- a/docs/reference/type-aliases/queueposition.md +++ b/docs/reference/type-aliases/queueposition.md @@ -11,7 +11,7 @@ title: QueuePosition type QueuePosition = "front" | "back"; ``` -Defined in: [queuer.ts:97](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L97) +Defined in: [queuer.ts:169](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L169) Position type for addItem and getNextItem operations. diff --git a/examples/react/asyncDebounce/package.json b/examples/react/asyncDebounce/package.json index 4c5a762e5..5764c73e8 100644 --- a/examples/react/asyncDebounce/package.json +++ b/examples/react/asyncDebounce/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/asyncRateLimit/package.json b/examples/react/asyncRateLimit/package.json index a873edd28..9746da0cf 100644 --- a/examples/react/asyncRateLimit/package.json +++ b/examples/react/asyncRateLimit/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/asyncThrottle/package.json b/examples/react/asyncThrottle/package.json index c636088bc..8ce62f4a5 100644 --- a/examples/react/asyncThrottle/package.json +++ b/examples/react/asyncThrottle/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/batch/package.json b/examples/react/batch/package.json index 8dd0f936f..b342e2902 100644 --- a/examples/react/batch/package.json +++ b/examples/react/batch/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/debounce/package.json b/examples/react/debounce/package.json index ec2a01d7f..038ede402 100644 --- a/examples/react/debounce/package.json +++ b/examples/react/debounce/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/queue/package.json b/examples/react/queue/package.json index 2c01eee3d..965bdcecb 100644 --- a/examples/react/queue/package.json +++ b/examples/react/queue/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/queue/src/index.tsx b/examples/react/queue/src/index.tsx index e47361cc2..17f63a083 100644 --- a/examples/react/queue/src/index.tsx +++ b/examples/react/queue/src/index.tsx @@ -17,7 +17,7 @@ function App1() { wait: 1000, onItemsChange: (queue) => { setQueueItems(queue.peekAllItems()) - setProcessedCount(queue.getExecutionCount()) + setProcessedCount(queue.store.state.executionCount) }, }), [], @@ -74,7 +74,7 @@ function App2() { wait: 500, onItemsChange: (queue) => { setQueueItems(queue.peekAllItems()) - setProcessedCount(queue.getExecutionCount()) + setProcessedCount(queue.store.state.executionCount) }, }), [], @@ -139,7 +139,7 @@ function App3() { wait: 100, onItemsChange: (queue) => { setQueueItems(queue.peekAllItems()) - setProcessedCount(queue.getExecutionCount()) + setProcessedCount(queue.store.state.executionCount) }, }), [], diff --git a/examples/react/rateLimit/package.json b/examples/react/rateLimit/package.json index 00ccb7776..1d0dce7df 100644 --- a/examples/react/rateLimit/package.json +++ b/examples/react/rateLimit/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/react-query-debounced-prefetch/package.json b/examples/react/react-query-debounced-prefetch/package.json index 40a4d1914..a3bd779ca 100644 --- a/examples/react/react-query-debounced-prefetch/package.json +++ b/examples/react/react-query-debounced-prefetch/package.json @@ -10,16 +10,16 @@ }, "dependencies": { "@tanstack/react-pacer": "^0.8.0", - "@tanstack/react-query": "^5.80.6", - "@tanstack/react-query-devtools": "^5.80.6", + "@tanstack/react-query": "^5.82.0", + "@tanstack/react-query-devtools": "^5.82.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/react-query-queued-prefetch/package.json b/examples/react/react-query-queued-prefetch/package.json index 8a3152fa8..1c7df8814 100644 --- a/examples/react/react-query-queued-prefetch/package.json +++ b/examples/react/react-query-queued-prefetch/package.json @@ -10,16 +10,16 @@ }, "dependencies": { "@tanstack/react-pacer": "^0.8.0", - "@tanstack/react-query": "^5.80.6", - "@tanstack/react-query-devtools": "^5.80.6", + "@tanstack/react-query": "^5.82.0", + "@tanstack/react-query-devtools": "^5.82.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/react-query-throttled-prefetch/package.json b/examples/react/react-query-throttled-prefetch/package.json index f9fc2ff44..e00d56dae 100644 --- a/examples/react/react-query-throttled-prefetch/package.json +++ b/examples/react/react-query-throttled-prefetch/package.json @@ -10,16 +10,16 @@ }, "dependencies": { "@tanstack/react-pacer": "^0.8.0", - "@tanstack/react-query": "^5.80.6", - "@tanstack/react-query-devtools": "^5.80.6", + "@tanstack/react-query": "^5.82.0", + "@tanstack/react-query-devtools": "^5.82.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/throttle/package.json b/examples/react/throttle/package.json index 6ee496e4b..3a0db1561 100644 --- a/examples/react/throttle/package.json +++ b/examples/react/throttle/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useAsyncBatcher/.eslintrc.cjs b/examples/react/useAsyncBatcher/.eslintrc.cjs new file mode 100644 index 000000000..9ff0b9fc9 --- /dev/null +++ b/examples/react/useAsyncBatcher/.eslintrc.cjs @@ -0,0 +1,13 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = { + settings: { + extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'], + rules: { + 'react/no-children-prop': 'off', + }, + }, +} + +module.exports = config diff --git a/examples/react/useAsyncBatcher/.gitignore b/examples/react/useAsyncBatcher/.gitignore new file mode 100644 index 000000000..872082d17 --- /dev/null +++ b/examples/react/useAsyncBatcher/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +pnpm-lock.yaml +yarn.lock +package-lock.json + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/examples/react/useAsyncBatcher/README.md b/examples/react/useAsyncBatcher/README.md new file mode 100644 index 000000000..14522a7f2 --- /dev/null +++ b/examples/react/useAsyncBatcher/README.md @@ -0,0 +1,6 @@ +# useAsyncBatcher Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/react/useAsyncBatcher/index.html b/examples/react/useAsyncBatcher/index.html new file mode 100644 index 000000000..a1a24961b --- /dev/null +++ b/examples/react/useAsyncBatcher/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Pacer useAsyncBatcher Example + + + +
+ + + diff --git a/examples/react/useAsyncBatcher/package.json b/examples/react/useAsyncBatcher/package.json new file mode 100644 index 000000000..306d4a2d9 --- /dev/null +++ b/examples/react/useAsyncBatcher/package.json @@ -0,0 +1,34 @@ +{ + "name": "@tanstack/pacer-example-react-use-async-batcher", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3005", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/react-pacer": "^0.8.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/react/useAsyncBatcher/public/emblem-light.svg b/examples/react/useAsyncBatcher/public/emblem-light.svg new file mode 100644 index 000000000..16d574897 --- /dev/null +++ b/examples/react/useAsyncBatcher/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/useAsyncBatcher/src/index.tsx b/examples/react/useAsyncBatcher/src/index.tsx new file mode 100644 index 000000000..ff45ceff7 --- /dev/null +++ b/examples/react/useAsyncBatcher/src/index.tsx @@ -0,0 +1,225 @@ +import { useState } from 'react' +import ReactDOM from 'react-dom/client' +import { useAsyncBatcher } from '@tanstack/react-pacer/async-batcher' + +const fakeProcessingTime = 1000 + +type Item = { + id: number + value: string + timestamp: number +} + +function App() { + const [processedBatches, setProcessedBatches] = useState< + Array<{ items: Array; result: string; timestamp: number }> + >([]) + const [errors, setErrors] = useState>([]) + const [shouldFail, setShouldFail] = useState(false) + + // The async function that will process a batch of items + async function processBatch(items: Array): Promise { + console.log('Processing batch of', items.length, 'items:', items) + + // Simulate async processing time + await new Promise((resolve) => setTimeout(resolve, fakeProcessingTime)) + + // Simulate occasional failures for demo purposes + if (shouldFail && Math.random() < 0.3) { + throw new Error(`Processing failed for batch with ${items.length} items`) + } + + // Return a result from the batch processing + const result = `Processed ${items.length} items: ${items.map((item) => item.value).join(', ')}` + + setProcessedBatches((prev) => [ + ...prev, + { items, result, timestamp: Date.now() }, + ]) + + return result + } + + const asyncBatcher = useAsyncBatcher(processBatch, { + maxSize: 5, // Process in batches of 5 (if reached before wait time) + wait: 4000, // Wait up to 4 seconds before processing a batch + getShouldExecute: (items) => + items.some((item) => item.value.includes('urgent')), // Process immediately if any item is marked urgent + throwOnError: false, // Don't throw errors, handle them via onError + onSuccess: (result, batcher) => { + console.log('Batch succeeded:', result) + console.log('Total successful batches:', batcher.store.state.successCount) + }, + onError: (error: any, _batcher) => { + console.error('Batch failed:', error) + setErrors((prev) => [ + ...prev, + `Error: ${error.message} (${new Date().toLocaleTimeString()})`, + ]) + }, + onSettled: (batcher) => { + console.log( + 'Batch settled. Total processed items:', + batcher.store.state.totalItemsProcessed, + ) + }, + }) + + const addItem = (isUrgent = false) => { + const nextId = Date.now() + const item: Item = { + id: nextId, + value: isUrgent ? `urgent-${nextId}` : `item-${nextId}`, + timestamp: nextId, + } + asyncBatcher.addItem(item) + } + + const executeCurrentBatch = async () => { + try { + const result = await asyncBatcher.flush() + console.log('Manual execution result:', result) + } catch (error) { + console.error('Manual execution failed:', error) + } + } + + return ( +
+

TanStack Pacer useAsyncBatcher Example

+ +
+

Batch Status

+
Current Batch Size: {asyncBatcher.state.size}
+
Max Batch Size: 5
+
Is Executing: {asyncBatcher.state.isExecuting ? 'Yes' : 'No'}
+
Is Running: {asyncBatcher.state.isRunning ? 'Yes' : 'No'}
+
Status: {asyncBatcher.state.status}
+
Successful Batches: {asyncBatcher.state.successCount}
+
Failed Batches: {asyncBatcher.state.errorCount}
+
+ Total Items Processed: {asyncBatcher.state.totalItemsProcessed} +
+
+ +
+

Current Batch Items

+
+ {asyncBatcher.peekAllItems().length === 0 ? ( + No items in current batch + ) : ( + asyncBatcher.peekAllItems().map((item, index) => ( +
+ {index + 1}: {item.value} (added at{' '} + {new Date(item.timestamp).toLocaleTimeString()}) +
+ )) + )} +
+
+ +
+

Controls

+
+ + + + + + +
+ +
+ +
+
+ +
+

Processed Batches ({processedBatches.length})

+
+ {processedBatches.length === 0 ? ( + No batches processed yet + ) : ( + processedBatches.map((batch, index) => ( +
+ Batch {index + 1} (processed at{' '} + {new Date(batch.timestamp).toLocaleTimeString()}) +
{batch.result}
+
+ )) + )} +
+
+ + {errors.length > 0 && ( +
+

Errors ({errors.length})

+
+ {errors.map((error, index) => ( +
{error}
+ ))} +
+ +
+ )} + +
+

How it works:

+
    +
  • Items are batched up to 3 at a time
  • +
  • Batches are processed after 2 seconds if not full
  • +
  • Items marked "urgent" trigger immediate processing
  • +
  • Processing takes 1 second to simulate async work
  • +
  • Each batch returns a result showing what was processed
  • +
  • Errors are handled gracefully and don't stop the batcher
  • +
+
+
+        {JSON.stringify(asyncBatcher.state, null, 2)}
+      
+
+ ) +} + +const root = ReactDOM.createRoot(document.getElementById('root')!) +root.render() diff --git a/examples/react/useAsyncBatcher/tsconfig.json b/examples/react/useAsyncBatcher/tsconfig.json new file mode 100644 index 000000000..6e9088d67 --- /dev/null +++ b/examples/react/useAsyncBatcher/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/react/useAsyncBatcher/vite.config.ts b/examples/react/useAsyncBatcher/vite.config.ts new file mode 100644 index 000000000..4e1943662 --- /dev/null +++ b/examples/react/useAsyncBatcher/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + react({ + // babel: { + // plugins: [['babel-plugin-react-compiler', { target: '19' }]], + // }, + }), + ], +}) diff --git a/examples/react/useAsyncDebouncer/package.json b/examples/react/useAsyncDebouncer/package.json index 825e0bc5f..7e04ebb5d 100644 --- a/examples/react/useAsyncDebouncer/package.json +++ b/examples/react/useAsyncDebouncer/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useAsyncDebouncer/src/index.tsx b/examples/react/useAsyncDebouncer/src/index.tsx index 8942276b4..8dffab9f1 100644 --- a/examples/react/useAsyncDebouncer/src/index.tsx +++ b/examples/react/useAsyncDebouncer/src/index.tsx @@ -20,8 +20,6 @@ const fakeApi = async (term: string): Promise> => { function App() { const [searchTerm, setSearchTerm] = useState('') const [results, setResults] = useState>([]) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) // The function that will become debounced const handleSearch = async (term: string) => { @@ -29,35 +27,36 @@ function App() { setResults([]) return } - // throw new Error('Test error') // you don't have to catch errors here (though you still can). The onError optional handler will catch it - if (!results.length) { - setIsLoading(true) - } - const data = await fakeApi(term) setResults(data) - setIsLoading(false) - setError(null) return data // this could alternatively be a void function without a return } // hook that gives you an async debouncer instance - const setSearchAsyncDebouncer = useAsyncDebouncer(handleSearch, { - // leading: true, // optional leading execution - wait: 500, // Wait 500ms between API calls - onError: (error) => { - // optional error handler - console.error('Search failed:', error) - setError(error as Error) - setResults([]) + const asyncDebouncer = useAsyncDebouncer( + handleSearch, + { + // leading: true, // optional leading execution + wait: 500, // Wait 500ms between API calls + onError: (error) => { + // optional error handler + console.error('Search failed:', error) + setResults([]) + }, }, - }) + // optionally subscribe to only the react state re-renders you care about + // (state) => ({ + // isExecuting: state.isExecuting, + // isPending: state.isPending, + // successCount: state.successCount, + // }), + ) // get and name our debounced function - const handleSearchDebounced = setSearchAsyncDebouncer.maybeExecute + const handleSearchDebounced = asyncDebouncer.maybeExecute // instant event handler that calls both the instant local state setter and the debounced function async function onSearchChange(e: React.ChangeEvent) { @@ -81,9 +80,11 @@ function App() { autoComplete="new-password" /> - {error &&
Error: {error.message}
} +
+ +
-

API calls made: {setSearchAsyncDebouncer.getSuccessCount()}

+

API calls made: {asyncDebouncer.state.successCount}

{results.length > 0 && (
    {results.map((item) => ( @@ -91,7 +92,11 @@ function App() { ))}
)} - {isLoading &&

Loading...

} + {asyncDebouncer.state.isPending &&

Pending...

} + {asyncDebouncer.state.isExecuting &&

Executing...

} +
+          {JSON.stringify(asyncDebouncer.state, null, 2)}
+        
) @@ -104,7 +109,7 @@ root.render() // demo unmounting and cancellation document.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { + if (e.shiftKey && e.key === 'Enter') { mounted = !mounted root.render(mounted ? : null) } diff --git a/examples/react/useAsyncQueuedState/package.json b/examples/react/useAsyncQueuedState/package.json index d5fa0d4f1..7c22ada6a 100644 --- a/examples/react/useAsyncQueuedState/package.json +++ b/examples/react/useAsyncQueuedState/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useAsyncQueuedState/src/index.tsx b/examples/react/useAsyncQueuedState/src/index.tsx index fcb4f7a49..8ab7a937f 100644 --- a/examples/react/useAsyncQueuedState/src/index.tsx +++ b/examples/react/useAsyncQueuedState/src/index.tsx @@ -10,49 +10,53 @@ function App() { // Use your state management library of choice const [concurrency, setConcurrency] = useState(2) - const [, rerender] = useState(0) // demo - rerender when start/stop changes - // The function to process each item (now a number) async function processItem(item: Item): Promise { await new Promise((resolve) => setTimeout(resolve, fakeWaitTime)) console.log(`Processed ${item}`) } - const [queueItems, asyncQueuer] = useAsyncQueuedState(processItem, { - maxSize: 25, - initialItems: Array.from({ length: 10 }, (_, i) => i + 1), - concurrency, // Process 2 items concurrently - started: false, - wait: 100, // for demo purposes - usually you would not want extra wait time if you are also throttling with concurrency - onIsRunningChange: (_asyncQueuer) => { - rerender((prev) => prev + 1) - }, - onReject: (item: Item, asyncQueuer) => { - console.log( - 'Queue is full, rejecting item', - item, - asyncQueuer.getRejectionCount(), - ) + const [queueItems, asyncQueuer] = useAsyncQueuedState( + processItem, // your function to queue/process items + { + maxSize: 25, + initialItems: Array.from({ length: 10 }, (_, i) => i + 1), + concurrency, // Process 2 items concurrently + started: false, + wait: 100, // for demo purposes - usually you would not want extra wait time if you are also throttling with concurrency + onReject: (item: Item, asyncQueuer) => { + console.log( + 'Queue is full, rejecting item', + item, + asyncQueuer.store.state.rejectionCount, + ) + }, + onError: (error: unknown, asyncQueuer) => { + console.error( + 'Error processing item', + error, + asyncQueuer.store.state.errorCount, + ) // optionally, handle errors here instead of your own try/catch + }, }, - onError: (error: unknown, _asyncQueuer) => { - console.error('Error processing item', error, asyncQueuer.getErrorCount()) // optionally, handle errors here instead of your own try/catch - }, - }) + // optionally, you can select a subset of the state to re-render when it changes + // (state) => ({ }), + ) return (

TanStack Pacer useAsyncQueuer Example

-
Queue Size: {asyncQueuer.getSize()}
+
Queue Size: {asyncQueuer.state.size}
Queue Max Size: {25}
-
Queue Full: {asyncQueuer.getIsFull() ? 'Yes' : 'No'}
-
Queue Empty: {asyncQueuer.getIsEmpty() ? 'Yes' : 'No'}
-
Queue Idle: {asyncQueuer.getIsIdle() ? 'Yes' : 'No'}
+
Queue Full: {asyncQueuer.state.isFull ? 'Yes' : 'No'}
+
Queue Empty: {asyncQueuer.state.isEmpty ? 'Yes' : 'No'}
+
Queue Idle: {asyncQueuer.state.isIdle ? 'Yes' : 'No'}
- Queuer Status: {asyncQueuer.getIsRunning() ? 'Running' : 'Stopped'} + Queuer Status: {asyncQueuer.state.status ? 'Running' : 'Stopped'}
-
Items Processed: {asyncQueuer.getSuccessCount()}
-
Items Rejected: {asyncQueuer.getRejectionCount()}
+
Items Processed: {asyncQueuer.state.successCount}
+
Items Rejected: {asyncQueuer.state.rejectionCount}
Active Tasks: {asyncQueuer.peekActiveItems().length}
Pending Tasks: {asyncQueuer.peekPendingItems().length}
@@ -91,31 +95,34 @@ function App() { : 1 asyncQueuer.addItem(nextNumber) }} - disabled={asyncQueuer.getIsFull()} + disabled={asyncQueuer.state.isFull} > Add Async Task - +
+
+        {JSON.stringify(asyncQueuer.state, null, 2)}
+      
) } diff --git a/examples/react/useAsyncQueuer/package.json b/examples/react/useAsyncQueuer/package.json index 96b498718..5c69f7244 100644 --- a/examples/react/useAsyncQueuer/package.json +++ b/examples/react/useAsyncQueuer/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useAsyncQueuer/src/index.tsx b/examples/react/useAsyncQueuer/src/index.tsx index a4ac7d3f2..472cd4686 100644 --- a/examples/react/useAsyncQueuer/src/index.tsx +++ b/examples/react/useAsyncQueuer/src/index.tsx @@ -7,58 +7,55 @@ const fakeWaitTime = 500 type Item = number function App() { - // Use your state management library of choice - const [queueItems, setQueueItems] = useState>([]) const [concurrency, setConcurrency] = useState(2) - const [, rerender] = useState(0) // demo - rerender when start/stop changes - // The function to process each item (now a number) async function processItem(item: Item): Promise { await new Promise((resolve) => setTimeout(resolve, fakeWaitTime)) console.log(`Processed ${item}`) } - const asyncQueuer = useAsyncQueuer(processItem, { - maxSize: 25, - initialItems: Array.from({ length: 10 }, (_, i) => i + 1), - concurrency, // Process 2 items concurrently - started: false, - wait: 100, // for demo purposes - usually you would not want extra wait time if you are also throttling with concurrency - onItemsChange: (asyncQueuer) => { - setQueueItems(asyncQueuer.peekAllItems()) - }, - onIsRunningChange: (_asyncQueuer) => { - rerender((prev) => prev + 1) - }, - onReject: (item: Item, asyncQueuer) => { - console.log( - 'Queue is full, rejecting item', - item, - asyncQueuer.getRejectionCount(), - ) + const asyncQueuer = useAsyncQueuer( + processItem, // your function to queue/process items + { + maxSize: 25, + initialItems: Array.from({ length: 10 }, (_, i) => i + 1), + concurrency, // Process 2 items concurrently + started: false, + wait: 100, // for demo purposes - usually you would not want extra wait time if you are also throttling with concurrency + onReject: (item, asyncQueuer) => { + console.log( + 'Queue is full, rejecting item', + item, + asyncQueuer.store.state.rejectionCount, + ) + }, + onError: (error: unknown, asyncQueuer) => { + console.error( + 'Error processing item', + error, + asyncQueuer.store.state.errorCount, + ) // optionally, handle errors here instead of your own try/catch + }, }, - onError: (error: unknown, _asyncQueuer) => { - console.error('Error processing item', error, asyncQueuer.getErrorCount()) // optionally, handle errors here instead of your own try/catch - }, - }) + // optionally, you can select a subset of the state to re-render when it changes + // (state) => ({ }), + ) return (

TanStack Pacer useAsyncQueuer Example

-
Queue Size: {asyncQueuer.getSize()}
+
Queue Size: {asyncQueuer.state.size}
Queue Max Size: {25}
-
Queue Full: {asyncQueuer.getIsFull() ? 'Yes' : 'No'}
-
Queue Empty: {asyncQueuer.getIsEmpty() ? 'Yes' : 'No'}
-
Queue Idle: {asyncQueuer.getIsIdle() ? 'Yes' : 'No'}
-
- Queuer Status: {asyncQueuer.getIsRunning() ? 'Running' : 'Stopped'} -
-
Items Processed: {asyncQueuer.getSuccessCount()}
-
Items Rejected: {asyncQueuer.getRejectionCount()}
-
Active Tasks: {asyncQueuer.peekActiveItems().length}
-
Pending Tasks: {asyncQueuer.peekPendingItems().length}
+
Queue Full: {asyncQueuer.state.isFull ? 'Yes' : 'No'}
+
Queue Empty: {asyncQueuer.state.isEmpty ? 'Yes' : 'No'}
+
Queue Idle: {asyncQueuer.state.isIdle ? 'Yes' : 'No'}
+
Queuer Status: {asyncQueuer.state.status}
+
Items Processed: {asyncQueuer.state.successCount}
+
Items Rejected: {asyncQueuer.state.rejectionCount}
+
Active Tasks: {asyncQueuer.state.activeItems.length}
+
Pending Tasks: {asyncQueuer.state.items.length}
Concurrency:{' '}
Queue Items: - {queueItems.map((item, index) => ( + {asyncQueuer.peekAllItems().map((item, index) => (
{index}: {item}
@@ -90,36 +87,45 @@ function App() { > - + +
+
+        {JSON.stringify(asyncQueuer.state, null, 2)}
+      
) } diff --git a/examples/react/useAsyncRateLimiter/package.json b/examples/react/useAsyncRateLimiter/package.json index deaa8f5e0..521fa15bf 100644 --- a/examples/react/useAsyncRateLimiter/package.json +++ b/examples/react/useAsyncRateLimiter/package.json @@ -10,14 +10,15 @@ }, "dependencies": { "@tanstack/react-pacer": "^0.8.0", + "@tanstack/react-persister": "^0.0.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useAsyncRateLimiter/src/index.tsx b/examples/react/useAsyncRateLimiter/src/index.tsx index 391ad2b02..95de48539 100644 --- a/examples/react/useAsyncRateLimiter/src/index.tsx +++ b/examples/react/useAsyncRateLimiter/src/index.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react' import ReactDOM from 'react-dom/client' import { useAsyncRateLimiter } from '@tanstack/react-pacer/async-rate-limiter' +import { useStoragePersister } from '@tanstack/react-persister/storage-persister' +import type { AsyncRateLimiterState } from '@tanstack/react-pacer/async-rate-limiter' interface SearchResult { id: number @@ -35,9 +37,18 @@ function App() { setResults(data) setError(null) - console.log(setSearchAsyncRateLimiter.getSuccessCount()) + console.log(setSearchAsyncRateLimiter.state.successCount) } + const rateLimiterPersister = useStoragePersister< + AsyncRateLimiterState + >({ + key: 'my-async-rate-limiter', + storage: localStorage, + maxAge: 1000 * 60, // 1 minute + buster: 'v1', + }) + // hook that gives you an async rate limiter instance const setSearchAsyncRateLimiter = useAsyncRateLimiter(handleSearch, { // windowType: 'sliding', // default is 'fixed' @@ -54,8 +65,14 @@ function App() { setError(error as Error) setResults([]) }, + // optionally, you can persist the rate limiter state to localStorage + initialState: rateLimiterPersister.loadState(), }) + useEffect(() => { + rateLimiterPersister.saveState(setSearchAsyncRateLimiter.state) + }, [setSearchAsyncRateLimiter.state]) + // get and name our rate limited function const handleSearchRateLimited = setSearchAsyncRateLimiter.maybeExecute @@ -94,16 +111,16 @@ function App() { API calls made: - {setSearchAsyncRateLimiter.getSuccessCount()} + {setSearchAsyncRateLimiter.state.successCount} Rejected calls: - {setSearchAsyncRateLimiter.getRejectionCount()} + {setSearchAsyncRateLimiter.state.rejectionCount} Is executing: - {setSearchAsyncRateLimiter.getIsExecuting() ? 'Yes' : 'No'} + {setSearchAsyncRateLimiter.state.isExecuting ? 'Yes' : 'No'} @@ -123,6 +140,9 @@ function App() {
+
+        {JSON.stringify(setSearchAsyncRateLimiter.state, null, 2)}
+      
) } diff --git a/examples/react/useAsyncThrottler/package.json b/examples/react/useAsyncThrottler/package.json index 56b2fb452..206d039ba 100644 --- a/examples/react/useAsyncThrottler/package.json +++ b/examples/react/useAsyncThrottler/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useAsyncThrottler/src/index.tsx b/examples/react/useAsyncThrottler/src/index.tsx index c5d28640e..9a9608c76 100644 --- a/examples/react/useAsyncThrottler/src/index.tsx +++ b/examples/react/useAsyncThrottler/src/index.tsx @@ -40,8 +40,8 @@ function App() { // hook that gives you an async throttler instance const setSearchAsyncThrottler = useAsyncThrottler(handleSearch, { - // leading: false, - // trailing: false, + // leading: true, // default + // trailing: true, // default wait: 1000, // Wait 1 second between API calls onError: (error) => { // optional error handler @@ -76,9 +76,12 @@ function App() { autoComplete="new-password" /> +
+ +
{error &&
Error: {error.message}
}
-

API calls made: {setSearchAsyncThrottler.getSuccessCount()}

+

API calls made: {setSearchAsyncThrottler.state.successCount}

{results.length > 0 && (
    {results.map((item) => ( @@ -86,11 +89,14 @@ function App() { ))}
)} - {setSearchAsyncThrottler.getIsPending() ? ( + {setSearchAsyncThrottler.state.isPending ? (

Pending...

- ) : setSearchAsyncThrottler.getIsExecuting() ? ( + ) : setSearchAsyncThrottler.state.isExecuting ? (

Executing...

) : null} +
+          {JSON.stringify(setSearchAsyncThrottler.state, null, 2)}
+        
) @@ -103,7 +109,7 @@ root.render() // demo unmounting and cancellation document.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { + if (e.shiftKey && e.key === 'Enter') { mounted = !mounted root.render(mounted ? : null) } diff --git a/examples/react/useBatcher/package.json b/examples/react/useBatcher/package.json index f9ca94c0d..88709b58d 100644 --- a/examples/react/useBatcher/package.json +++ b/examples/react/useBatcher/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useBatcher/src/index.tsx b/examples/react/useBatcher/src/index.tsx index fb2c6d3e8..f9a26f521 100644 --- a/examples/react/useBatcher/src/index.tsx +++ b/examples/react/useBatcher/src/index.tsx @@ -4,7 +4,6 @@ import { useBatcher } from '@tanstack/react-pacer/batcher' function App1() { // Use your state management library of choice - const [batchItems, setBatchItems] = useState>([]) const [processedBatches, setProcessedBatches] = useState< Array> >([]) @@ -20,19 +19,16 @@ function App1() { maxSize: 5, // Process in batches of 5 (if comes before wait time) wait: 3000, // wait up to 3 seconds before processing a batch (if time elapses before maxSize is reached) getShouldExecute: (items, _batcher) => items.includes(42), // or pass in a custom function to determine if the batch should be processed - onItemsChange: (batcher) => { - setBatchItems(batcher.peekAllItems()) - }, }) return (

TanStack Pacer useBatcher Example 1

-
Batch Size: {batcher.getSize()}
+
Batch Size: {batcher.state.size}
Batch Max Size: {3}
-
Batch Items: {batchItems.join(', ')}
-
Batches Processed: {batcher.getBatchExecutionCount()}
-
Items Processed: {batcher.getItemExecutionCount()}
+
Batch Items: {batcher.peekAllItems().join(', ')}
+
Batches Processed: {batcher.state.executionCount}
+
Items Processed: {batcher.state.totalItemsProcessed}
Processed Batches:{' '} {processedBatches.map((b, i) => ( @@ -52,8 +48,8 @@ function App1() { >
+
+        {JSON.stringify(batcher.state, null, 2)}
+      
) } diff --git a/examples/react/useDebouncedCallback/package.json b/examples/react/useDebouncedCallback/package.json index 57b8e60a9..080077707 100644 --- a/examples/react/useDebouncedCallback/package.json +++ b/examples/react/useDebouncedCallback/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useDebouncedState/package.json b/examples/react/useDebouncedState/package.json index 8b1d117ff..ed2d0abf8 100644 --- a/examples/react/useDebouncedState/package.json +++ b/examples/react/useDebouncedState/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useDebouncedState/src/index.tsx b/examples/react/useDebouncedState/src/index.tsx index d03db81a7..fd1ea2c30 100644 --- a/examples/react/useDebouncedState/src/index.tsx +++ b/examples/react/useDebouncedState/src/index.tsx @@ -30,17 +30,13 @@ function App1() {

TanStack Pacer useDebouncedState Example 1

- - - - - + - + - + - + - + @@ -45,7 +45,16 @@ function App1() {
Enabled:{debouncer.getOptions().enabled.toString()}
Is Pending:{debouncer.getIsPending().toString()}{debouncer.state.isPending.toString()}
Execution Count:{debouncer.getExecutionCount()}{debouncer.state.executionCount}
@@ -60,6 +56,9 @@ function App1() {
+
+        {JSON.stringify(debouncer.state, null, 2)}
+      
) } @@ -97,17 +96,13 @@ function App2() { - - - - - + - +
Enabled:{debouncer.getOptions().enabled.toString()}
Is Pending:{debouncer.getIsPending().toString()}{debouncer.state.isPending.toString()}
Execution Count:{debouncer.getExecutionCount()}{debouncer.state.executionCount}
@@ -124,6 +119,9 @@ function App2() {
+
+        {JSON.stringify(debouncer.state, null, 2)}
+      
) } @@ -180,13 +178,9 @@ function App3() { - - - - - + @@ -194,11 +188,11 @@ function App3() { - + - + @@ -206,7 +200,7 @@ function App3() { {instantExecutionCount === 0 ? '0' : Math.round( - ((instantExecutionCount - debouncer.getExecutionCount()) / + ((instantExecutionCount - debouncer.state.executionCount) / instantExecutionCount) * 100, )} @@ -218,6 +212,9 @@ function App3() {

Debounced to 250ms wait time

+
+        {JSON.stringify(debouncer.state, null, 2)}
+      
) } diff --git a/examples/react/useDebouncedValue/package.json b/examples/react/useDebouncedValue/package.json index 73cfba0fe..2fc0c983f 100644 --- a/examples/react/useDebouncedValue/package.json +++ b/examples/react/useDebouncedValue/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useDebouncedValue/src/index.tsx b/examples/react/useDebouncedValue/src/index.tsx index 4ff2dfa90..d46d5b57d 100644 --- a/examples/react/useDebouncedValue/src/index.tsx +++ b/examples/react/useDebouncedValue/src/index.tsx @@ -129,13 +129,9 @@ function App3() {
Enabled:{debouncer.getOptions().enabled.toString()}
Is Pending:{debouncer.getIsPending().toString()}{debouncer.state.isPending.toString()}
Instant Executions:
Debounced Executions:{debouncer.getExecutionCount()}{debouncer.state.executionCount}
Saved Executions:{instantExecutionCount - debouncer.getExecutionCount()}{instantExecutionCount - debouncer.state.executionCount}
% Reduction:
- - - - - + @@ -143,11 +139,11 @@ function App3() { - + - + @@ -155,7 +151,7 @@ function App3() { {instantExecutionCount === 0 ? '0' : Math.round( - ((instantExecutionCount - debouncer.getExecutionCount()) / + ((instantExecutionCount - debouncer.state.executionCount) / instantExecutionCount) * 100, )} @@ -167,6 +163,9 @@ function App3() {

Debounced to 250ms wait time

+
+        {JSON.stringify(debouncer.state, null, 2)}
+      
) } diff --git a/examples/react/useDebouncer/package.json b/examples/react/useDebouncer/package.json index 90093f292..cbf5907cd 100644 --- a/examples/react/useDebouncer/package.json +++ b/examples/react/useDebouncer/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useDebouncer/src/index.tsx b/examples/react/useDebouncer/src/index.tsx index 4a149cb2f..c0adb7984 100644 --- a/examples/react/useDebouncer/src/index.tsx +++ b/examples/react/useDebouncer/src/index.tsx @@ -8,9 +8,9 @@ function App1() { const [debouncedCount, setDebouncedCount] = useState(0) // Lower-level useDebouncer hook - requires you to manage your own state - const setCountDebouncer = useDebouncer(setDebouncedCount, { - wait: 500, - // enabled: () => instantCount > 2, // optional, defaults to true + const debouncer = useDebouncer(setDebouncedCount, { + wait: 800, + enabled: () => instantCount > 2, // optional, defaults to true // leading: true, // optional, defaults to false }) @@ -18,7 +18,7 @@ function App1() { // this pattern helps avoid common bugs with stale closures and state setInstantCount((c) => { const newInstantCount = c + 1 // common new value for both - setCountDebouncer.maybeExecute(newInstantCount) // debounced state update + debouncer.maybeExecute(newInstantCount) // debounced state update return newInstantCount // instant state update }) } @@ -29,16 +29,12 @@ function App1() {
Enabled:{debouncer.getOptions().enabled.toString()}
Is Pending:{debouncer.getIsPending().toString()}{debouncer.state.isPending.toString()}
Instant Executions:
Debounced Executions:{debouncer.getExecutionCount()}{debouncer.state.executionCount}
Saved Executions:{instantExecutionCount - debouncer.getExecutionCount()}{instantExecutionCount - debouncer.state.executionCount}
% Reduction:
- - - - - - + + - +
Enabled:{setCountDebouncer.getEnabled().toString()}
Is Pending:{setCountDebouncer.getIsPending().toString()}Status:{debouncer.state.status}
Execution Count:{setCountDebouncer.getExecutionCount()}{debouncer.state.executionCount}
@@ -57,7 +53,16 @@ function App1() {
+
+
+        {JSON.stringify(debouncer.state, null, 2)}
+      
) } @@ -93,17 +98,13 @@ function App2() { - - - - - + - +
Enabled:{setSearchDebouncer.getOptions().enabled.toString()}
Is Pending:{setSearchDebouncer.getIsPending().toString()}{setSearchDebouncer.state.isPending.toString()}
Execution Count:{setSearchDebouncer.getExecutionCount()}{setSearchDebouncer.state.executionCount}
@@ -120,6 +121,12 @@ function App2() {
+
+ +
+
+        {JSON.stringify(setSearchDebouncer.state, null, 2)}
+      
) } @@ -174,13 +181,9 @@ function App3() { - - - - - + @@ -188,12 +191,12 @@ function App3() { - + @@ -203,7 +206,7 @@ function App3() { ? '0' : Math.round( ((instantExecutionCount - - setValueDebouncer.getExecutionCount()) / + setValueDebouncer.state.executionCount) / instantExecutionCount) * 100, )} @@ -215,6 +218,12 @@ function App3() {

Debounced to 250ms wait time

+
+ +
+
+        {JSON.stringify(setValueDebouncer.state, null, 2)}
+      
) } diff --git a/examples/react/useQueuedState/package.json b/examples/react/useQueuedState/package.json index 2cf89b277..6b84926af 100644 --- a/examples/react/useQueuedState/package.json +++ b/examples/react/useQueuedState/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useQueuedState/src/index.tsx b/examples/react/useQueuedState/src/index.tsx index 6ffdf6b5e..f4dcae537 100644 --- a/examples/react/useQueuedState/src/index.tsx +++ b/examples/react/useQueuedState/src/index.tsx @@ -18,14 +18,14 @@ function App1() { return (

TanStack Pacer useQueuedState Example 1

-
Queue Size: {queuer.getSize()}
+
Queue Size: {queuer.state.size}
Queue Max Size: {25}
-
Queue Full: {queuer.getIsFull() ? 'Yes' : 'No'}
+
Queue Full: {queuer.state.isFull ? 'Yes' : 'No'}
Queue Peek: {queuer.peekNextItem()}
-
Queue Empty: {queuer.getIsEmpty() ? 'Yes' : 'No'}
-
Queue Idle: {queuer.getIsIdle() ? 'Yes' : 'No'}
-
Queuer Status: {queuer.getIsRunning() ? 'Running' : 'Stopped'}
-
Items Processed: {queuer.getExecutionCount()}
+
Queue Empty: {queuer.state.isEmpty ? 'Yes' : 'No'}
+
Queue Idle: {queuer.state.isIdle ? 'Yes' : 'No'}
+
Queuer Status: {queuer.state.status}
+
Items Processed: {queuer.state.executionCount}
Queue Items: {queueItems.join(', ')}
Add Number - - - -
+
+        {JSON.stringify(queuer.state, null, 2)}
+      
) } @@ -131,23 +140,23 @@ function App2() { - + - + - + - + - + @@ -155,11 +164,11 @@ function App2() { - + - + @@ -167,7 +176,7 @@ function App2() { {instantExecutionCount === 0 ? '0' : Math.round( - ((instantExecutionCount - queuer.getExecutionCount()) / + ((instantExecutionCount - queuer.state.executionCount) / instantExecutionCount) * 100, )} @@ -179,6 +188,9 @@ function App2() {

Queued with 100ms wait time

+
+        {JSON.stringify(queuer.state, null, 2)}
+      
) } diff --git a/examples/react/useQueuedValue/package.json b/examples/react/useQueuedValue/package.json index 9a87422ba..26c0b6fa8 100644 --- a/examples/react/useQueuedValue/package.json +++ b/examples/react/useQueuedValue/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useQueuedValue/src/index.tsx b/examples/react/useQueuedValue/src/index.tsx index 80a49eae5..c395ce4a3 100644 --- a/examples/react/useQueuedValue/src/index.tsx +++ b/examples/react/useQueuedValue/src/index.tsx @@ -8,7 +8,6 @@ function App1() { // Queuer that processes a single value with delays const [value, queuer] = useQueuedValue(instantSearchValue, { maxSize: 25, - started: false, wait: 500, // wait 500ms between processing value changes }) @@ -17,13 +16,13 @@ function App1() {

TanStack Pacer useQueuedValue Example 1

Current Value: {value}

-
Queue Size: {queuer.getSize()}
-
Queue Full: {queuer.getIsFull() ? 'Yes' : 'No'}
+
Queue Size: {queuer.state.size}
+
Queue Full: {queuer.state.isFull ? 'Yes' : 'No'}
Queue Peek: {queuer.peekNextItem()}
-
Queue Empty: {queuer.getIsEmpty() ? 'Yes' : 'No'}
-
Queue Idle: {queuer.getIsIdle() ? 'Yes' : 'No'}
-
Queuer Status: {queuer.getIsRunning() ? 'Running' : 'Stopped'}
-
Items Processed: {queuer.getExecutionCount()}
+
Queue Empty: {queuer.state.isEmpty ? 'Yes' : 'No'}
+
Queue Idle: {queuer.state.isIdle ? 'Yes' : 'No'}
+
Queuer Status: {queuer.state.status}
+
Items Processed: {queuer.state.executionCount}
Queue Items: {queuer.peekAllItems().join(', ')}
- - - -
+
+        {JSON.stringify(queuer.state, null, 2)}
+      
) } @@ -120,23 +128,23 @@ function App2() { - + - + - + - + - + @@ -144,11 +152,11 @@ function App2() { - + - + @@ -156,7 +164,7 @@ function App2() { {instantExecutionCount === 0 ? '0' : Math.round( - ((instantExecutionCount - queuer.getExecutionCount()) / + ((instantExecutionCount - queuer.state.executionCount) / instantExecutionCount) * 100, )} @@ -168,6 +176,9 @@ function App2() {

Queued with 100ms wait time

+
+        {JSON.stringify(queuer.state, null, 2)}
+      
) } diff --git a/examples/react/useQueuer/package.json b/examples/react/useQueuer/package.json index 6fc5c3d90..4323f9d4e 100644 --- a/examples/react/useQueuer/package.json +++ b/examples/react/useQueuer/package.json @@ -10,14 +10,15 @@ }, "dependencies": { "@tanstack/react-pacer": "^0.8.0", + "@tanstack/react-persister": "^0.0.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useQueuer/src/index.tsx b/examples/react/useQueuer/src/index.tsx index f41b7ceff..d07a9b664 100644 --- a/examples/react/useQueuer/src/index.tsx +++ b/examples/react/useQueuer/src/index.tsx @@ -1,10 +1,17 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import ReactDOM from 'react-dom/client' import { useQueuer } from '@tanstack/react-pacer/queuer' +import { useStoragePersister } from '@tanstack/react-persister' +import type { QueuerState } from '@tanstack/react-pacer/queuer' function App1() { - // Use your state management library of choice - const [queueItems, setQueueItems] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + // optional session storage persister to retain state on page refresh + const queuerPersister = useStoragePersister>({ + key: 'my-queuer', + storage: sessionStorage, + maxAge: 1000 * 60, // 1 minute + buster: 'v1', + }) // The function that we will be queuing function processItem(item: number) { @@ -12,27 +19,29 @@ function App1() { } const queuer = useQueuer(processItem, { - maxSize: 25, - initialItems: queueItems, - started: false, + initialItems: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + initialState: queuerPersister.loadState(), + maxSize: 25, // optional, defaults to Infinity + started: false, // optional, defaults to true wait: 1000, // wait 1 second between processing items - wait is optional! - onItemsChange: (queue) => { - setQueueItems(queue.peekAllItems()) - }, }) + useEffect(() => { + queuerPersister.saveState(queuer.state) + }, [queuer.state]) + return (
-

TanStack Pacer useQueuer Example 1

-
Queue Size: {queuer.getSize()}
+

TanStack Pacer useQueuer Example 1 (with persister)

+
Queue Size: {queuer.state.size}
Queue Max Size: {25}
-
Queue Full: {queuer.getIsFull() ? 'Yes' : 'No'}
+
Queue Full: {queuer.state.isFull ? 'Yes' : 'No'}
Queue Peek: {queuer.peekNextItem()}
-
Queue Empty: {queuer.getIsEmpty() ? 'Yes' : 'No'}
-
Queue Idle: {queuer.getIsIdle() ? 'Yes' : 'No'}
-
Queuer Status: {queuer.getIsRunning() ? 'Running' : 'Stopped'}
-
Items Processed: {queuer.getExecutionCount()}
-
Queue Items: {queueItems.join(', ')}
+
Queue Empty: {queuer.state.isEmpty ? 'Yes' : 'No'}
+
Queue Idle: {queuer.state.isIdle ? 'Yes' : 'No'}
+
Queuer Status: {queuer.state.status}
+
Items Processed: {queuer.state.executionCount}
+
Queue Items: {queuer.state.items.join(', ')}
- - - - +
+
+        {JSON.stringify(queuer.state, null, 2)}
+      
) } @@ -136,23 +157,23 @@ function App2() {
- + - + - + - + - + @@ -160,11 +181,11 @@ function App2() { - + - + @@ -172,7 +193,7 @@ function App2() { {instantExecutionCount === 0 ? '0' : Math.round( - ((instantExecutionCount - queuer.getExecutionCount()) / + ((instantExecutionCount - queuer.state.executionCount) / instantExecutionCount) * 100, )} @@ -184,6 +205,12 @@ function App2() {

Queued with 100ms wait time

+
+ +
+
+        {JSON.stringify(queuer.state, null, 2)}
+      
) } diff --git a/examples/react/useRateLimitedCallback/package.json b/examples/react/useRateLimitedCallback/package.json index 0f36ede69..3fec901b9 100644 --- a/examples/react/useRateLimitedCallback/package.json +++ b/examples/react/useRateLimitedCallback/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useRateLimitedState/package.json b/examples/react/useRateLimitedState/package.json index bb280c916..168356e2c 100644 --- a/examples/react/useRateLimitedState/package.json +++ b/examples/react/useRateLimitedState/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useRateLimitedState/src/index.tsx b/examples/react/useRateLimitedState/src/index.tsx index 904eb6273..0d364cdd2 100644 --- a/examples/react/useRateLimitedState/src/index.tsx +++ b/examples/react/useRateLimitedState/src/index.tsx @@ -36,11 +36,11 @@ function App1() { - + - + @@ -59,6 +59,9 @@ function App1() { +
+        {JSON.stringify(rateLimiter.state, null, 2)}
+      
) } @@ -104,11 +107,11 @@ function App2() { - + - + @@ -126,6 +129,9 @@ function App2() { +
+        {JSON.stringify(rateLimiter.state, null, 2)}
+      
) } @@ -190,11 +196,11 @@ function App3() { - + - + @@ -210,7 +216,7 @@ function App3() { - + @@ -218,7 +224,8 @@ function App3() { {instantExecutionCount === 0 ? '0' : Math.round( - ((instantExecutionCount - rateLimiter.getExecutionCount()) / + ((instantExecutionCount - + rateLimiter.state.executionCount) / instantExecutionCount) * 100, )} @@ -230,6 +237,9 @@ function App3() {

Rate limited to 20 updates per 2 seconds

+
+        {JSON.stringify(rateLimiter.state, null, 2)}
+      
) } diff --git a/examples/react/useRateLimitedValue/package.json b/examples/react/useRateLimitedValue/package.json index 2b69a638a..53aca172d 100644 --- a/examples/react/useRateLimitedValue/package.json +++ b/examples/react/useRateLimitedValue/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useRateLimitedValue/src/index.tsx b/examples/react/useRateLimitedValue/src/index.tsx index f7c07c10c..e9c58cf4a 100644 --- a/examples/react/useRateLimitedValue/src/index.tsx +++ b/examples/react/useRateLimitedValue/src/index.tsx @@ -148,11 +148,11 @@ function App3() { - + - + @@ -168,7 +168,7 @@ function App3() { - + @@ -176,7 +176,8 @@ function App3() { {instantExecutionCount === 0 ? '0' : Math.round( - ((instantExecutionCount - rateLimiter.getExecutionCount()) / + ((instantExecutionCount - + rateLimiter.state.executionCount) / instantExecutionCount) * 100, )} @@ -188,6 +189,9 @@ function App3() {

Rate limited to 20 updates per 2 seconds

+
+        {JSON.stringify(rateLimiter.state, null, 2)}
+      
) } diff --git a/examples/react/useRateLimiter/package.json b/examples/react/useRateLimiter/package.json index 0925ad554..4ac54abc1 100644 --- a/examples/react/useRateLimiter/package.json +++ b/examples/react/useRateLimiter/package.json @@ -10,14 +10,15 @@ }, "dependencies": { "@tanstack/react-pacer": "^0.8.0", + "@tanstack/react-persister": "^0.0.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useRateLimiter/src/index.tsx b/examples/react/useRateLimiter/src/index.tsx index 285715f12..bac215f2f 100644 --- a/examples/react/useRateLimiter/src/index.tsx +++ b/examples/react/useRateLimiter/src/index.tsx @@ -1,11 +1,20 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import ReactDOM from 'react-dom/client' import { useRateLimiter } from '@tanstack/react-pacer/rate-limiter' +import { useStoragePersister } from '@tanstack/react-persister/storage-persister' +import type { RateLimiterState } from '@tanstack/react-pacer/rate-limiter' function App1() { // Use your state management library of choice - const [instantCount, setInstantCount] = useState(0) - const [limitedCount, setLimitedCount] = useState(0) + const [instantCount, setInstantCount] = useState(0) // not rate-limited + const [limitedCount, setLimitedCount] = useState(0) // rate-limited + + const rateLimiterPersister = useStoragePersister({ + key: 'my-rate-limiter', + storage: localStorage, + maxAge: 1000 * 60, // 1 minute + buster: 'v1', + }) // Using useRateLimiter with a rate limit of 5 executions per 5 seconds const rateLimiter = useRateLimiter(setLimitedCount, { @@ -18,8 +27,14 @@ function App1() { 'Rejected by rate limiter', rateLimiter.getMsUntilNextWindow(), ), + // optional local storage persister to retain state on page refresh + initialState: rateLimiterPersister.loadState(), }) + useEffect(() => { + rateLimiterPersister.saveState(rateLimiter.state) + }, [rateLimiter.state]) + function increment() { // this pattern helps avoid common bugs with stale closures and state setInstantCount((c) => { @@ -31,16 +46,16 @@ function App1() { return (
-

TanStack Pacer useRateLimiter Example 1

+

TanStack Pacer useRateLimiter Example 1 (with persister)

Enabled:{setValueDebouncer.getEnabled().toString()}
Is Pending:{setValueDebouncer.getIsPending().toString()}{setValueDebouncer.state.isPending.toString()}
Instant Executions:
Debounced Executions:{setValueDebouncer.getExecutionCount()}{setValueDebouncer.state.executionCount}
Saved Executions: - {instantExecutionCount - setValueDebouncer.getExecutionCount()} + {instantExecutionCount - setValueDebouncer.state.executionCount}
Queue Size:{queuer.getSize()}{queuer.state.size}
Queue Full:{queuer.getIsFull() ? 'Yes' : 'No'}{queuer.state.isFull ? 'Yes' : 'No'}
Queue Empty:{queuer.getIsEmpty() ? 'Yes' : 'No'}{queuer.state.isEmpty ? 'Yes' : 'No'}
Queue Idle:{queuer.getIsIdle() ? 'Yes' : 'No'}{queuer.state.isIdle ? 'Yes' : 'No'}
Queuer Status:{queuer.getIsRunning() ? 'Running' : 'Stopped'}{queuer.state.status}
Instant Executions:
Items Processed:{queuer.getExecutionCount()}{queuer.state.executionCount}
Saved Executions:{instantExecutionCount - queuer.getExecutionCount()}{instantExecutionCount - queuer.state.executionCount}
% Reduction:
Queue Size:{queuer.getSize()}{queuer.state.size}
Queue Full:{queuer.getIsFull() ? 'Yes' : 'No'}{queuer.state.isFull ? 'Yes' : 'No'}
Queue Empty:{queuer.getIsEmpty() ? 'Yes' : 'No'}{queuer.state.isEmpty ? 'Yes' : 'No'}
Queue Idle:{queuer.getIsIdle() ? 'Yes' : 'No'}{queuer.state.isIdle ? 'Yes' : 'No'}
Queuer Status:{queuer.getIsRunning() ? 'Running' : 'Stopped'}{queuer.state.status}
Instant Executions:
Items Processed:{queuer.getExecutionCount()}{queuer.state.executionCount}
Saved Executions:{instantExecutionCount - queuer.getExecutionCount()}{instantExecutionCount - queuer.state.executionCount}
% Reduction:
Queue Size:{queuer.getSize()}{queuer.state.size}
Queue Full:{queuer.getIsFull() ? 'Yes' : 'No'}{queuer.state.isFull ? 'Yes' : 'No'}
Queue Empty:{queuer.getIsEmpty() ? 'Yes' : 'No'}{queuer.state.isEmpty ? 'Yes' : 'No'}
Queue Idle:{queuer.getIsIdle() ? 'Yes' : 'No'}{queuer.state.isIdle ? 'Yes' : 'No'}
Queuer Status:{queuer.getIsRunning() ? 'Running' : 'Stopped'}{queuer.state.isRunning ? 'Running' : 'Stopped'}
Instant Executions:
Items Processed:{queuer.getExecutionCount()}{queuer.state.executionCount}
Saved Executions:{instantExecutionCount - queuer.getExecutionCount()}{instantExecutionCount - queuer.state.executionCount}
% Reduction:
Execution Count:{rateLimiter.getExecutionCount()}{rateLimiter.state.executionCount}
Rejection Count:{rateLimiter.getRejectionCount()}{rateLimiter.state.rejectionCount}
Instant Count:
Execution Count:{rateLimiter.getExecutionCount()}{rateLimiter.state.executionCount}
Rejection Count:{rateLimiter.getRejectionCount()}{rateLimiter.state.rejectionCount}
Instant Search:
Execution Count:{rateLimiter.getExecutionCount()}{rateLimiter.state.executionCount}
Rejection Count:{rateLimiter.getRejectionCount()}{rateLimiter.state.rejectionCount}
Remaining in Window:
Saved Executions:{instantExecutionCount - rateLimiter.getExecutionCount()}{instantExecutionCount - rateLimiter.state.executionCount}
% Reduction:
Execution Count:{rateLimiter.getExecutionCount()}{rateLimiter.state.executionCount}
Rejection Count:{rateLimiter.getRejectionCount()}{rateLimiter.state.rejectionCount}
Remaining in Window:
Saved Executions:{instantExecutionCount - rateLimiter.getExecutionCount()}{instantExecutionCount - rateLimiter.state.executionCount}
% Reduction:
- + - + @@ -69,6 +84,9 @@ function App1() { +
+        {JSON.stringify(rateLimiter.state, null, 2)}
+      
) } @@ -113,11 +131,11 @@ function App2() { - + - + @@ -144,6 +162,9 @@ function App2() {
+
+        {JSON.stringify(rateLimiter.state, null, 2)}
+      
) } @@ -206,11 +227,11 @@ function App3() { - + - + @@ -226,7 +247,7 @@ function App3() { - + @@ -234,7 +255,8 @@ function App3() { {instantExecutionCount === 0 ? '0' : Math.round( - ((instantExecutionCount - rateLimiter.getExecutionCount()) / + ((instantExecutionCount - + rateLimiter.state.executionCount) / instantExecutionCount) * 100, )} @@ -246,6 +268,9 @@ function App3() {

Rate limited to 20 updates per 2 seconds

+
+        {JSON.stringify(rateLimiter.state, null, 2)}
+      
) } diff --git a/examples/react/useStorageState/.eslintrc.cjs b/examples/react/useStorageState/.eslintrc.cjs new file mode 100644 index 000000000..274d731f0 --- /dev/null +++ b/examples/react/useStorageState/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + '@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/examples/react/useStorageState/.gitignore b/examples/react/useStorageState/.gitignore new file mode 100644 index 000000000..e81ff5233 --- /dev/null +++ b/examples/react/useStorageState/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# TypeScript +*.tsbuildinfo \ No newline at end of file diff --git a/examples/react/useStorageState/README.md b/examples/react/useStorageState/README.md new file mode 100644 index 000000000..abfb29f36 --- /dev/null +++ b/examples/react/useStorageState/README.md @@ -0,0 +1,20 @@ +# useStorageState Examples + +This example demonstrates the TanStack Persister storage state hooks: + +- **App1**: `useStorageState` - Generic storage state hook with localStorage +- **App2**: `useLocalStorageState` - Convenience hook for localStorage +- **App3**: `useSessionStorageState` - Convenience hook for sessionStorage + +## Features Demonstrated + +- State persistence across browser refreshes +- Cross-tab synchronization (localStorage) +- Session-only storage (sessionStorage) +- Complex state management with objects and arrays +- Form state persistence + +## To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/react/useStorageState/index.html b/examples/react/useStorageState/index.html new file mode 100644 index 000000000..8f1db37d8 --- /dev/null +++ b/examples/react/useStorageState/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Persister useStorageState Example + + + +
+ + + diff --git a/examples/react/useStorageState/package.json b/examples/react/useStorageState/package.json new file mode 100644 index 000000000..db0ea6a40 --- /dev/null +++ b/examples/react/useStorageState/package.json @@ -0,0 +1,34 @@ +{ + "name": "@tanstack/pacer-example-react-use-storage-state", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3005", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/react-persister": "^0.0.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/react/useStorageState/public/emblem-light.svg b/examples/react/useStorageState/public/emblem-light.svg new file mode 100644 index 000000000..a58e69ad5 --- /dev/null +++ b/examples/react/useStorageState/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/useStorageState/src/index.tsx b/examples/react/useStorageState/src/index.tsx new file mode 100644 index 000000000..58db663b4 --- /dev/null +++ b/examples/react/useStorageState/src/index.tsx @@ -0,0 +1,326 @@ +import { useState } from 'react' +import ReactDOM from 'react-dom/client' +import { + useLocalStorageState, + useSessionStorageState, +} from '@tanstack/react-persister/storage-persister' + +// App2: useLocalStorageState hook +function App2() { + const [todos, setTodos] = useLocalStorageState( + 'todos-list', + [] as Array, + ) + const [newTodo, setNewTodo] = useState('') + + const addTodo = () => { + if (newTodo.trim()) { + setTodos((prev) => [...prev, newTodo.trim()]) + setNewTodo('') + } + } + + const removeTodo = (index: number) => { + setTodos((prev) => prev.filter((_, i) => i !== index)) + } + + const clearAll = () => { + setTodos([]) + } + + return ( +
+

TanStack Persister useLocalStorageState Example

+

This example uses the useLocalStorageState hook for a todo list

+ +
+
+ setNewTodo(e.target.value)} + placeholder="Add a new todo" + style={{ flex: 1, padding: '8px' }} + onKeyPress={(e) => e.key === 'Enter' && addTodo()} + /> + +
+ + {todos.length > 0 && ( + + )} +
+ +
+

Todo List ({todos.length} items):

+ {todos.length === 0 ? ( +

+ No todos yet. Add one above! +

+ ) : ( +
    + {todos.map((todo, index) => ( +
  • + {todo} + +
  • + ))} +
+ )} +
+ +
+

• Todos persist in localStorage

+

• Data survives browser refreshes

+

• Shared across all tabs in the same domain

+
+
+ ) +} + +// App3: useSessionStorageState hook +function App3() { + const [cart, setCart] = useSessionStorageState( + 'shopping-cart', + [] as Array<{ id: number; name: string; quantity: number; price: number }>, + ) + const [formData, setFormData] = useSessionStorageState('checkout-form', { + email: '', + address: '', + city: '', + zipCode: '', + }) + + const products = [ + { id: 1, name: 'Widget A', price: 19.99 }, + { id: 2, name: 'Widget B', price: 29.99 }, + { id: 3, name: 'Widget C', price: 39.99 }, + ] + + const addToCart = (product: (typeof products)[0]) => { + setCart((prev) => { + const existing = prev.find((item) => item.id === product.id) + if (existing) { + return prev.map((item) => + item.id === product.id + ? { ...item, quantity: item.quantity + 1 } + : item, + ) + } + return [...prev, { ...product, quantity: 1 }] + }) + } + + const removeFromCart = (id: number) => { + setCart((prev) => prev.filter((item) => item.id !== id)) + } + + const updateFormData = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })) + } + + const getTotalPrice = () => { + return cart + .reduce((sum, item) => sum + item.price * item.quantity, 0) + .toFixed(2) + } + + return ( +
+

TanStack Persister useSessionStorageState Example

+

+ This example uses the useSessionStorageState hook for a shopping cart +

+ +
+
+

Products:

+ {products.map((product) => ( +
+ + {product.name} - ${product.price} + + +
+ ))} +
+ +
+

Shopping Cart ({cart.length} items):

+ {cart.length === 0 ? ( +

Cart is empty

+ ) : ( + <> + {cart.map((item) => ( +
+ + {item.name} x{item.quantity} + + ${(item.price * item.quantity).toFixed(2)} + +
+ ))} +
+ Total: ${getTotalPrice()} +
+ + )} +
+
+ +
+

Checkout Form:

+
+ updateFormData('email', e.target.value)} + style={{ padding: '8px' }} + /> + updateFormData('address', e.target.value)} + style={{ padding: '8px' }} + /> + updateFormData('city', e.target.value)} + style={{ padding: '8px' }} + /> + updateFormData('zipCode', e.target.value)} + style={{ padding: '8px' }} + /> +
+
+ +
+

• Data persists in sessionStorage (only this tab)

+

• Cart and form data cleared when tab is closed

+

• Perfect for temporary shopping sessions

+
+
+ ) +} + +const root = ReactDOM.createRoot(document.getElementById('root')!) +root.render( +
+ +
+ +
, +) diff --git a/examples/react/useStorageState/tsconfig.json b/examples/react/useStorageState/tsconfig.json new file mode 100644 index 000000000..6e9088d67 --- /dev/null +++ b/examples/react/useStorageState/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/react/useStorageState/vite.config.ts b/examples/react/useStorageState/vite.config.ts new file mode 100644 index 000000000..4e1943662 --- /dev/null +++ b/examples/react/useStorageState/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + react({ + // babel: { + // plugins: [['babel-plugin-react-compiler', { target: '19' }]], + // }, + }), + ], +}) diff --git a/examples/react/useThrottledCallback/package.json b/examples/react/useThrottledCallback/package.json index 8cfbcdae4..546608715 100644 --- a/examples/react/useThrottledCallback/package.json +++ b/examples/react/useThrottledCallback/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useThrottledState/package.json b/examples/react/useThrottledState/package.json index f811b8eb7..0e9c313e8 100644 --- a/examples/react/useThrottledState/package.json +++ b/examples/react/useThrottledState/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useThrottledState/src/index.tsx b/examples/react/useThrottledState/src/index.tsx index b1b0de0df..2151aa029 100644 --- a/examples/react/useThrottledState/src/index.tsx +++ b/examples/react/useThrottledState/src/index.tsx @@ -31,7 +31,7 @@ function App1() {
- + @@ -46,6 +46,9 @@ function App1() {
+
+        {JSON.stringify(throttler.state, null, 2)}
+      
) } @@ -85,7 +88,7 @@ function App2() { - + @@ -97,6 +100,9 @@ function App2() {
Execution Count:{rateLimiter.getExecutionCount()}{rateLimiter.state.executionCount}
Rejection Count:{rateLimiter.getRejectionCount()}{rateLimiter.state.rejectionCount}
Remaining in Window:
Execution Count:{rateLimiter.getExecutionCount()}{rateLimiter.state.executionCount}
Rejection Count:{rateLimiter.getRejectionCount()}{rateLimiter.state.rejectionCount}
Remaining in Window:
Execution Count:{rateLimiter.getExecutionCount()}{rateLimiter.state.executionCount}
Rejection Count:{rateLimiter.getRejectionCount()}{rateLimiter.state.rejectionCount}
Remaining in Window:
Saved Executions:{instantExecutionCount - rateLimiter.getExecutionCount()}{instantExecutionCount - rateLimiter.state.executionCount}
% Reduction:
Execution Count:{throttler.getExecutionCount()}{throttler.state.executionCount}
Instant Count:
Execution Count:{throttler.getExecutionCount()}{throttler.state.executionCount}
Instant Search:
+
+        {JSON.stringify(throttler.state, null, 2)}
+      
) } @@ -159,15 +165,15 @@ function App3() {
Throttled Execution Count:{throttler.getExecutionCount()}{throttler.state.executionCount}
Saved Executions: - {instantExecutionCount - throttler.getExecutionCount()} ( + {instantExecutionCount - throttler.state.executionCount} ( {instantExecutionCount > 0 ? ( - ((instantExecutionCount - throttler.getExecutionCount()) / + ((instantExecutionCount - throttler.state.executionCount) / instantExecutionCount) * 100 ).toFixed(2) @@ -180,6 +186,9 @@ function App3() {

Throttled to 1 update per 250ms

+
+        {JSON.stringify(throttler.state, null, 2)}
+      
) } diff --git a/examples/react/useThrottledValue/package.json b/examples/react/useThrottledValue/package.json index 8156fe918..2a86840c3 100644 --- a/examples/react/useThrottledValue/package.json +++ b/examples/react/useThrottledValue/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useThrottledValue/src/index.tsx b/examples/react/useThrottledValue/src/index.tsx index 77552fbcb..bb3bc95da 100644 --- a/examples/react/useThrottledValue/src/index.tsx +++ b/examples/react/useThrottledValue/src/index.tsx @@ -134,15 +134,15 @@ function App3() {
Throttled Execution Count:{throttler.getExecutionCount()}{throttler.state.executionCount}
Saved Executions: - {instantExecutionCount - throttler.getExecutionCount()} ( + {instantExecutionCount - throttler.state.executionCount} ( {instantExecutionCount > 0 ? ( - ((instantExecutionCount - throttler.getExecutionCount()) / + ((instantExecutionCount - throttler.state.executionCount) / instantExecutionCount) * 100 ).toFixed(2) @@ -155,6 +155,9 @@ function App3() {

Throttled to 1 update per 250ms

+
+        {JSON.stringify(throttler.state, null, 2)}
+      
) } diff --git a/examples/react/useThrottler/package.json b/examples/react/useThrottler/package.json index 715222793..324e6e761 100644 --- a/examples/react/useThrottler/package.json +++ b/examples/react/useThrottler/package.json @@ -14,10 +14,10 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.6", + "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", - "vite": "^6.3.5" + "@vitejs/plugin-react": "^4.6.0", + "vite": "^7.0.4" }, "browserslist": { "production": [ diff --git a/examples/react/useThrottler/src/index.tsx b/examples/react/useThrottler/src/index.tsx index 2c4e9fb17..a5c14caff 100644 --- a/examples/react/useThrottler/src/index.tsx +++ b/examples/react/useThrottler/src/index.tsx @@ -10,8 +10,8 @@ function App1() { // Lower-level useThrottler hook - requires you to manage your own state const setCountThrottler = useThrottler(setThrottledCount, { wait: 1000, - leading: false, - // trailing: true, + // leading: true, // default + // trailing: true, // default // enabled: () => instantCount > 2, }) @@ -31,7 +31,7 @@ function App1() {
Execution Count:{setCountThrottler.getExecutionCount()}{setCountThrottler.state.executionCount}
Instant Count:
+
+
+        {JSON.stringify(setCountThrottler.state, null, 2)}
+      
) } @@ -83,7 +92,7 @@ function App2() { Execution Count: - {setSearchThrottler.getExecutionCount()} + {setSearchThrottler.state.executionCount} Instant Search: @@ -95,6 +104,12 @@ function App2() { +
+ +
+
+        {JSON.stringify(setSearchThrottler.state, null, 2)}
+      
) } @@ -107,7 +122,8 @@ function App3() { // Lower-level useThrottler hook - requires you to manage your own state const setValueThrottler = useThrottler(setThrottledValue, { wait: 250, - leading: false, + // leading: true, // default + // trailing: true, // default }) function handleRangeChange(e: React.ChangeEvent) { @@ -160,16 +176,16 @@ function App3() { Throttled Execution Count: - {setValueThrottler.getExecutionCount()} + {setValueThrottler.state.executionCount} Saved Executions: - {instantExecutionCount - setValueThrottler.getExecutionCount()} ( + {instantExecutionCount - setValueThrottler.state.executionCount} ( {instantExecutionCount > 0 ? ( ((instantExecutionCount - - setValueThrottler.getExecutionCount()) / + setValueThrottler.state.executionCount) / instantExecutionCount) * 100 ).toFixed(2) @@ -182,6 +198,12 @@ function App3() {

Throttled to 1 update per 250ms (trailing edge)

+
+ +
+
+        {JSON.stringify(setValueThrottler.state, null, 2)}
+      
) } diff --git a/examples/solid/asyncDebounce/package.json b/examples/solid/asyncDebounce/package.json index 5d4d0c198..e0d208bce 100644 --- a/examples/solid/asyncDebounce/package.json +++ b/examples/solid/asyncDebounce/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/asyncRateLimit/package.json b/examples/solid/asyncRateLimit/package.json index d11242703..771d57f53 100644 --- a/examples/solid/asyncRateLimit/package.json +++ b/examples/solid/asyncRateLimit/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/asyncThrottle/package.json b/examples/solid/asyncThrottle/package.json index 375ad2954..f9229527c 100644 --- a/examples/solid/asyncThrottle/package.json +++ b/examples/solid/asyncThrottle/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/batch/package.json b/examples/solid/batch/package.json index 6a5c2e529..2e5e94a4d 100644 --- a/examples/solid/batch/package.json +++ b/examples/solid/batch/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createAsyncBatcher/.gitignore b/examples/solid/createAsyncBatcher/.gitignore new file mode 100644 index 000000000..872082d17 --- /dev/null +++ b/examples/solid/createAsyncBatcher/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +pnpm-lock.yaml +yarn.lock +package-lock.json + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/examples/solid/createAsyncBatcher/README.md b/examples/solid/createAsyncBatcher/README.md new file mode 100644 index 000000000..1f6128ebf --- /dev/null +++ b/examples/solid/createAsyncBatcher/README.md @@ -0,0 +1,6 @@ +# createAsyncBatcher Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/solid/createAsyncBatcher/index.html b/examples/solid/createAsyncBatcher/index.html new file mode 100644 index 000000000..a0413a0cf --- /dev/null +++ b/examples/solid/createAsyncBatcher/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Pacer createAsyncBatcher Example + + + +
+ + + diff --git a/examples/solid/createAsyncBatcher/package.json b/examples/solid/createAsyncBatcher/package.json new file mode 100644 index 000000000..8b8ce041f --- /dev/null +++ b/examples/solid/createAsyncBatcher/package.json @@ -0,0 +1,31 @@ +{ + "name": "@tanstack/pacer-example-solid-create-async-batcher", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3005", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/solid-pacer": "^0.8.0", + "solid-js": "^1.9.7" + }, + "devDependencies": { + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/solid/createAsyncBatcher/public/emblem-light.svg b/examples/solid/createAsyncBatcher/public/emblem-light.svg new file mode 100644 index 000000000..16d574897 --- /dev/null +++ b/examples/solid/createAsyncBatcher/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/solid/createAsyncBatcher/src/index.tsx b/examples/solid/createAsyncBatcher/src/index.tsx new file mode 100644 index 000000000..4d94fd99a --- /dev/null +++ b/examples/solid/createAsyncBatcher/src/index.tsx @@ -0,0 +1,224 @@ +import { For, createSignal } from 'solid-js' +import { render } from 'solid-js/web' +import { createAsyncBatcher } from '@tanstack/solid-pacer/async-batcher' + +const fakeProcessingTime = 1000 + +type Item = { + id: number + value: string + timestamp: number +} + +function App() { + // Use your state management library of choice + const [processedBatches, setProcessedBatches] = createSignal< + Array<{ items: Array; result: string; timestamp: number }> + >([]) + const [errors, setErrors] = createSignal>([]) + const [shouldFail, setShouldFail] = createSignal(false) + + // The async function that will process a batch of items + async function processBatch(items: Array): Promise { + console.log('Processing batch of', items.length, 'items:', items) + + // Simulate async processing time + await new Promise((resolve) => setTimeout(resolve, fakeProcessingTime)) + + // Simulate occasional failures for demo purposes + if (shouldFail() && Math.random() < 0.3) { + throw new Error(`Processing failed for batch with ${items.length} items`) + } + + // Return a result from the batch processing + const result = `Processed ${items.length} items: ${items.map((item) => item.value).join(', ')}` + + setProcessedBatches((prev) => [ + ...prev, + { items, result, timestamp: Date.now() }, + ]) + + return result + } + + const batcher = createAsyncBatcher(processBatch, { + maxSize: 5, // Process in batches of 5 (if reached before wait time) + wait: 2000, // Wait up to 2 seconds before processing a batch + getShouldExecute: (items) => + items.some((item) => item.value.includes('urgent')), // Process immediately if any item is marked urgent + throwOnError: false, // Don't throw errors, handle them via onError + onSuccess: (result, batcher) => { + console.log('Batch succeeded:', result) + console.log('Total successful batches:', batcher.store.state.successCount) + }, + onError: (error: any, _batcher) => { + console.error('Batch failed:', error) + setErrors((prev) => [ + ...prev, + `Error: ${error.message} (${new Date().toLocaleTimeString()})`, + ]) + }, + onSettled: (batcher) => { + console.log( + 'Batch settled. Total processed items:', + batcher.store.state.totalItemsProcessed, + ) + }, + }) + + const addItem = (isUrgent = false) => { + const nextId = Date.now() + const item: Item = { + id: nextId, + value: isUrgent ? `urgent-${nextId}` : `item-${nextId}`, + timestamp: nextId, + } + batcher.addItem(item) + } + + const executeCurrentBatch = async () => { + try { + const result = await batcher.flush() + console.log('Manual execution result:', result) + } catch (error) { + console.error('Manual execution failed:', error) + } + } + + return ( +
+

TanStack Pacer createAsyncBatcher Example

+ +
+

Batch Status

+
Current Batch Size: {batcher.state().size}
+
Max Batch Size: 5
+
Is Executing: {batcher.state().isExecuting ? 'Yes' : 'No'}
+
Is Running: {batcher.state().isRunning ? 'Yes' : 'No'}
+
Status: {batcher.state().status}
+
Successful Batches: {batcher.state().successCount}
+
Failed Batches: {batcher.state().errorCount}
+
Total Items Processed: {batcher.state().totalItemsProcessed}
+
+ +
+

Current Batch Items

+
+ {batcher.state().items.length === 0 ? ( + No items in current batch + ) : ( + + {(item, index) => ( +
+ {index() + 1}: {item.value} (added at{' '} + {new Date(item.timestamp).toLocaleTimeString()}) +
+ )} +
+ )} +
+
+ +
+

Controls

+
+ + + + + + +
+ +
+ +
+
+ +
+

Processed Batches ({processedBatches().length})

+
+ {processedBatches().length === 0 ? ( + No batches processed yet + ) : ( + + {(batch, index) => ( +
+ Batch {index() + 1} (processed at{' '} + {new Date(batch.timestamp).toLocaleTimeString()}) +
{batch.result}
+
+ )} +
+ )} +
+
+ + {errors().length > 0 && ( +
+

Errors ({errors().length})

+
+ + {(error, index) => ( +
+ {index() + 1}: {error} +
+ )} +
+
+ +
+ )} + +
+

How it works:

+
    +
  • Items are batched up to 5 at a time
  • +
  • Batches are processed after 2 seconds if not full
  • +
  • Items marked "urgent" trigger immediate processing
  • +
  • Processing takes 1 second to simulate async work
  • +
  • Each batch returns a result showing what was processed
  • +
  • Errors are handled gracefully and don't stop the batcher
  • +
+
+
+ ) +} + +render(() => , document.getElementById('root')!) diff --git a/examples/solid/createAsyncBatcher/tsconfig.json b/examples/solid/createAsyncBatcher/tsconfig.json new file mode 100644 index 000000000..a0784796d --- /dev/null +++ b/examples/solid/createAsyncBatcher/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/solid/createAsyncBatcher/vite.config.ts b/examples/solid/createAsyncBatcher/vite.config.ts new file mode 100644 index 000000000..6d47c41e0 --- /dev/null +++ b/examples/solid/createAsyncBatcher/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [solid({})], +}) diff --git a/examples/solid/createAsyncDebouncer/package.json b/examples/solid/createAsyncDebouncer/package.json index 6404a9dc4..a0267f372 100644 --- a/examples/solid/createAsyncDebouncer/package.json +++ b/examples/solid/createAsyncDebouncer/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createAsyncDebouncer/src/index.tsx b/examples/solid/createAsyncDebouncer/src/index.tsx index 7412c719d..a59130ee9 100644 --- a/examples/solid/createAsyncDebouncer/src/index.tsx +++ b/examples/solid/createAsyncDebouncer/src/index.tsx @@ -20,8 +20,6 @@ const fakeApi = async (term: string): Promise> => { function App() { const [searchTerm, setSearchTerm] = createSignal('') const [results, setResults] = createSignal>([]) - const [isLoading, setIsLoading] = createSignal(false) - const [error, setError] = createSignal(null) // The function that will become debounced const handleSearch = async (term: string) => { @@ -32,32 +30,34 @@ function App() { // throw new Error('Test error') // you don't have to catch errors here (though you still can). The onError optional handler will catch it - if (!results.length) { - setIsLoading(true) - } - const data = await fakeApi(term) setResults(data) // option 1: set results immediately - setIsLoading(false) - setError(null) return data // option 2: return data if you need to } // hook that gives you an async debouncer instance - const setSearchAsyncDebouncer = createAsyncDebouncer(handleSearch, { - // leading: true, // optional leading execution - wait: 500, // Wait 500ms between API calls - onError: (error) => { - // optional error handler - console.error('Search failed:', error) - setError(error as Error) - setResults([]) + const asyncDebouncer = createAsyncDebouncer( + handleSearch, + { + // leading: true, // optional leading execution + wait: 500, // Wait 500ms between API calls + onError: (error) => { + // optional error handler + console.error('Search failed:', error) + setResults([]) + }, }, - }) + // optionally subscribe to only the solid state changes you care about + // (state) => ({ + // isExecuting: state.isExecuting, + // isPending: state.isPending, + // successCount: state.successCount, + // }), + ) // get and name our debounced function - const handleSearchDebounced = setSearchAsyncDebouncer.maybeExecute + const handleSearchDebounced = asyncDebouncer.maybeExecute // instant event handler that calls both the instant local state setter and the debounced function async function onSearchChange(e: Event) { @@ -81,15 +81,20 @@ function App() { autocomplete="new-password" /> - {error() &&
Error: {error()?.message}
} + {asyncDebouncer.state().errorCount > 0 && ( +
Errors: {asyncDebouncer.state().errorCount}
+ )}
-

API calls made: {setSearchAsyncDebouncer.successCount()}

+

API calls made: {asyncDebouncer.state().successCount}

{results().length > 0 && (
    {(item) =>
  • {item.title}
  • }
)} - {isLoading() &&

Loading...

} + {asyncDebouncer.state().isExecuting &&

Executing...

} + {asyncDebouncer.state().isPending &&

Pending...

} +
+
{JSON.stringify({ state: asyncDebouncer.state() }, null, 2)}
) diff --git a/examples/solid/createAsyncQueuer/package.json b/examples/solid/createAsyncQueuer/package.json index e5e48c5be..f20886fb8 100644 --- a/examples/solid/createAsyncQueuer/package.json +++ b/examples/solid/createAsyncQueuer/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createAsyncQueuer/src/index.tsx b/examples/solid/createAsyncQueuer/src/index.tsx index b6e56acca..c75b71431 100644 --- a/examples/solid/createAsyncQueuer/src/index.tsx +++ b/examples/solid/createAsyncQueuer/src/index.tsx @@ -8,7 +8,6 @@ type Item = number function App() { // Use your state management library of choice - const [queueItems, setQueueItems] = createSignal>([]) const [concurrency, setConcurrency] = createSignal(2) // The function to process each item (now a number) @@ -17,41 +16,47 @@ function App() { console.log(`Processed ${item}`) } - const queuer = createAsyncQueuer(processItem, { - maxSize: 25, - initialItems: Array.from({ length: 10 }, (_, i) => i + 1), - concurrency: () => concurrency(), // Process 2 items concurrently - started: false, - wait: 100, // for demo purposes - usually you would not want extra wait time if you are also throttling with concurrency - onItemsChange: (queuer) => { - setQueueItems(queuer.peekAllItems()) + const queuer = createAsyncQueuer( + processItem, + { + maxSize: 25, + initialItems: Array.from({ length: 10 }, (_, i) => i + 1), + concurrency: () => concurrency(), // Process 2 items concurrently + started: false, + wait: 100, // for demo purposes - usually you would not want extra wait time if you are also throttling with concurrency + onReject: (item, queuer) => { + console.log( + 'Queue is full, rejecting item', + item, + queuer.store.state.rejectionCount, + ) + }, + onError: (error, queuer) => { + console.error( + 'Error processing item', + error, + queuer.store.state.errorCount, + ) // optionally, handle errors here instead of your own try/catch + }, }, - onReject: (item, queuer) => { - console.log( - 'Queue is full, rejecting item', - item, - queuer.getRejectionCount(), - ) - }, - onError: (error, queuer) => { - console.error('Error processing item', error, queuer.getErrorCount()) // optionally, handle errors here instead of your own try/catch - }, - }) + // optionally, you can select a subset of the state to re-render when it changes + // (state) => ({}), + ) return (

TanStack Pacer createAsyncQueuer Example

-
Queue Size: {queuer.size()}
+
Queue Size: {queuer.state().size}
Queue Max Size: {25}
-
Queue Full: {queuer.isFull() ? 'Yes' : 'No'}
-
Queue Empty: {queuer.isEmpty() ? 'Yes' : 'No'}
-
Queue Idle: {queuer.isIdle() ? 'Yes' : 'No'}
-
Queuer Status: {queuer.isRunning() ? 'Running' : 'Stopped'}
-
Items Processed: {queuer.successCount()}
-
Items Rejected: {queuer.rejectionCount()}
-
Active Tasks: {queuer.activeItems().length}
-
Pending Tasks: {queuer.pendingItems().length}
+
Queue Full: {queuer.state().isFull ? 'Yes' : 'No'}
+
Queue Empty: {queuer.state().isEmpty ? 'Yes' : 'No'}
+
Queue Idle: {queuer.state().isIdle ? 'Yes' : 'No'}
+
Queuer Status: {queuer.state().status}
+
Items Processed: {queuer.state().successCount}
+
Items Rejected: {queuer.state().rejectionCount}
+
Active Tasks: {queuer.state().activeItems.length}
+
Pending Tasks: {queuer.state().items.length}
Concurrency:{' '}
Queue Items: - + {(item, index) => (
{index()}: {item} @@ -85,23 +90,33 @@ function App() { > - - - -
diff --git a/examples/solid/createAsyncRateLimiter/package.json b/examples/solid/createAsyncRateLimiter/package.json index a3895678c..28f598fb5 100644 --- a/examples/solid/createAsyncRateLimiter/package.json +++ b/examples/solid/createAsyncRateLimiter/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createAsyncRateLimiter/src/index.tsx b/examples/solid/createAsyncRateLimiter/src/index.tsx index 788d800a0..1b8c35243 100644 --- a/examples/solid/createAsyncRateLimiter/src/index.tsx +++ b/examples/solid/createAsyncRateLimiter/src/index.tsx @@ -20,8 +20,6 @@ const fakeApi = async (term: string): Promise> => { function App() { const [searchTerm, setSearchTerm] = createSignal('') const [results, setResults] = createSignal>([]) - const [isLoading, setIsLoading] = createSignal(false) - const [error, setError] = createSignal(null) // The function that will become rate limited const handleSearch = async (term: string) => { @@ -32,16 +30,10 @@ function App() { // throw new Error('Test error') // you don't have to catch errors here (though you still can). The onError optional handler will catch it - if (!results.length) { - setIsLoading(true) - } - const data = await fakeApi(term) setResults(data) - setIsLoading(false) - setError(null) - console.log(setSearchAsyncRateLimiter.successCount()) + console.log(setSearchAsyncRateLimiter.state().successCount) } // hook that gives you an async rate limiter instance @@ -51,9 +43,14 @@ function App() { onError: (error) => { // optional error handler console.error('Search failed:', error) - setError(error as Error) setResults([]) }, + onReject: (rateLimiter) => { + console.log( + 'Rate limit exceeded:', + rateLimiter.store.state.rejectionCount, + ) + }, }) // get and name our rate limited function @@ -80,15 +77,14 @@ function App() { autocomplete="new-password" />
- {error() &&
Error: {error()?.message}
}
-

API calls made: {setSearchAsyncRateLimiter.successCount()}

+

API calls made: {setSearchAsyncRateLimiter.state().successCount}

{results().length > 0 && (
    {(item) =>
  • {item.title}
  • }
)} - {isLoading() &&

Loading...

} + {setSearchAsyncRateLimiter.state().isExecuting &&

Loading...

}
) diff --git a/examples/solid/createAsyncThrottler/package.json b/examples/solid/createAsyncThrottler/package.json index 2228519f2..b8fb4ea51 100644 --- a/examples/solid/createAsyncThrottler/package.json +++ b/examples/solid/createAsyncThrottler/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createAsyncThrottler/src/index.tsx b/examples/solid/createAsyncThrottler/src/index.tsx index d313d58f9..d97e9788b 100644 --- a/examples/solid/createAsyncThrottler/src/index.tsx +++ b/examples/solid/createAsyncThrottler/src/index.tsx @@ -76,11 +76,11 @@ function App() {
{error() &&
Error: {error()?.message}
}
-

API calls made: {setSearchAsyncThrottler.successCount()}

+

API calls made: {setSearchAsyncThrottler.state().successCount}

{(item) =>
  • {item.title}
  • }
    - {setSearchAsyncThrottler.isPending() ? ( + {setSearchAsyncThrottler.state().isPending ? (

    Pending...

    - ) : setSearchAsyncThrottler.isExecuting() ? ( + ) : setSearchAsyncThrottler.state().isExecuting ? (

    Executing...

    ) : null}
    diff --git a/examples/solid/createBatcher/package.json b/examples/solid/createBatcher/package.json index 6691f6904..d721aa9f3 100644 --- a/examples/solid/createBatcher/package.json +++ b/examples/solid/createBatcher/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createBatcher/src/index.tsx b/examples/solid/createBatcher/src/index.tsx index 92cfa75f2..4b58fac99 100644 --- a/examples/solid/createBatcher/src/index.tsx +++ b/examples/solid/createBatcher/src/index.tsx @@ -23,11 +23,11 @@ function App1() { return (

    TanStack Pacer createBatcher Example 1

    -
    Batch Size: {batcher.size()}
    +
    Batch Size: {batcher.state().size}
    Batch Max Size: {5}
    -
    Batch Items: {batcher.allItems().join(', ')}
    -
    Batches Processed: {batcher.batchExecutionCount()}
    -
    Items Processed: {batcher.itemExecutionCount()}
    +
    Batch Items: {batcher.state().items.join(', ')}
    +
    Batches Processed: {batcher.state().executionCount}
    +
    Items Processed: {batcher.state().totalItemsProcessed}
    Processed Batches:{' '} @@ -45,8 +45,8 @@ function App1() { > - -
    diff --git a/examples/solid/createDebouncedSignal/package.json b/examples/solid/createDebouncedSignal/package.json index 87681c04b..38a859b69 100644 --- a/examples/solid/createDebouncedSignal/package.json +++ b/examples/solid/createDebouncedSignal/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createDebouncedSignal/src/index.tsx b/examples/solid/createDebouncedSignal/src/index.tsx index 2dd444bef..71180828b 100644 --- a/examples/solid/createDebouncedSignal/src/index.tsx +++ b/examples/solid/createDebouncedSignal/src/index.tsx @@ -32,7 +32,7 @@ function App1() { Execution Count: - {debouncer.executionCount()} + {debouncer.state().executionCount} @@ -89,7 +89,7 @@ function App2() { Execution Count: - {debouncer.executionCount()} + {debouncer.state().executionCount} @@ -169,11 +169,13 @@ function App3() { Debounced Executions: - {debouncer.executionCount()} + {debouncer.state().executionCount} Saved Executions: - {instantExecutionCount() - debouncer.executionCount()} + + {instantExecutionCount() - debouncer.state().executionCount} + % Reduction: @@ -181,7 +183,8 @@ function App3() { {instantExecutionCount() === 0 ? '0' : Math.round( - ((instantExecutionCount() - debouncer.executionCount()) / + ((instantExecutionCount() - + debouncer.state().executionCount) / instantExecutionCount()) * 100, )} diff --git a/examples/solid/createDebouncedValue/package.json b/examples/solid/createDebouncedValue/package.json index 4d936ca93..d7c3f85bf 100644 --- a/examples/solid/createDebouncedValue/package.json +++ b/examples/solid/createDebouncedValue/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createDebouncedValue/src/index.tsx b/examples/solid/createDebouncedValue/src/index.tsx index 7ede4c1a2..e11213ffb 100644 --- a/examples/solid/createDebouncedValue/src/index.tsx +++ b/examples/solid/createDebouncedValue/src/index.tsx @@ -135,11 +135,13 @@ function App3() { Debounced Executions: - {debouncer.executionCount()} + {debouncer.state().executionCount} Saved Executions: - {instantExecutionCount() - debouncer.executionCount()} + + {instantExecutionCount() - debouncer.state().executionCount} + % Reduction: @@ -147,7 +149,8 @@ function App3() { {instantExecutionCount() === 0 ? '0' : Math.round( - ((instantExecutionCount() - debouncer.executionCount()) / + ((instantExecutionCount() - + debouncer.state().executionCount) / instantExecutionCount()) * 100, )} diff --git a/examples/solid/createDebouncer/package.json b/examples/solid/createDebouncer/package.json index b59e69a1c..8b2d52b74 100644 --- a/examples/solid/createDebouncer/package.json +++ b/examples/solid/createDebouncer/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createDebouncer/src/index.tsx b/examples/solid/createDebouncer/src/index.tsx index 8349c7dda..eb78b7ca9 100644 --- a/examples/solid/createDebouncer/src/index.tsx +++ b/examples/solid/createDebouncer/src/index.tsx @@ -30,7 +30,7 @@ function App1() { Execution Count: - {setCountDebouncer.executionCount()} + {setCountDebouncer.state().executionCount} @@ -88,7 +88,7 @@ function App2() { Execution Count: - {setSearchDebouncer.executionCount()} + {setSearchDebouncer.state().executionCount} @@ -166,12 +166,13 @@ function App3() { Debounced Executions: - {setValueDebouncer.executionCount()} + {setValueDebouncer.state().executionCount} Saved Executions: - {instantExecutionCount() - setValueDebouncer.executionCount()} + {instantExecutionCount() - + setValueDebouncer.state().executionCount} @@ -181,7 +182,7 @@ function App3() { ? '0' : Math.round( ((instantExecutionCount() - - setValueDebouncer.executionCount()) / + setValueDebouncer.state().executionCount) / instantExecutionCount()) * 100, )} diff --git a/examples/solid/createQueuer/package.json b/examples/solid/createQueuer/package.json index 36468948d..f1cec31fc 100644 --- a/examples/solid/createQueuer/package.json +++ b/examples/solid/createQueuer/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createQueuer/src/index.tsx b/examples/solid/createQueuer/src/index.tsx index 9043dd94d..f03a19f9b 100644 --- a/examples/solid/createQueuer/src/index.tsx +++ b/examples/solid/createQueuer/src/index.tsx @@ -18,15 +18,15 @@ function App1() { return (

    TanStack Pacer createQueuer Example 1

    -
    Queue Size: {queuer.size()}
    +
    Queue Size: {queuer.state().size}
    Queue Max Size: {25}
    -
    Queue Full: {queuer.isFull() ? 'Yes' : 'No'}
    -
    Queue Peek: {queuer.nextItem()}
    -
    Queue Empty: {queuer.isEmpty() ? 'Yes' : 'No'}
    -
    Queue Idle: {queuer.isIdle() ? 'Yes' : 'No'}
    -
    Queuer Status: {queuer.isRunning() ? 'Running' : 'Stopped'}
    -
    Items Processed: {queuer.executionCount()}
    -
    Queue Items: {queuer.allItems().join(', ')}
    +
    Queue Full: {queuer.state().isFull ? 'Yes' : 'No'}
    +
    Queue Peek: {queuer.peekNextItem()}
    +
    Queue Empty: {queuer.state().isEmpty ? 'Yes' : 'No'}
    +
    Queue Idle: {queuer.state().isIdle ? 'Yes' : 'No'}
    +
    Queuer Status: {queuer.state().status}
    +
    Items Processed: {queuer.state().executionCount}
    +
    Queue Items: {queuer.state().items.join(', ')}
    - - - -
    @@ -115,15 +127,15 @@ function App2() { Queue Size: - {queuer.size()} + {queuer.state().size} Items Processed: - {queuer.executionCount()} + {queuer.state().executionCount} Queue Items: - {queuer.allItems().join(', ')} + {queuer.state().items.join(', ')} @@ -136,16 +148,28 @@ function App2() { margin: '16px 0', }} > - - - -
    @@ -215,19 +239,19 @@ function App3() { Queue Size: - {queuer.size()} + {queuer.state().size} Queue Full: - {queuer.isFull() ? 'Yes' : 'No'} + {queuer.state().isFull ? 'Yes' : 'No'} Queue Empty: - {queuer.isEmpty() ? 'Yes' : 'No'} + {queuer.state().isEmpty ? 'Yes' : 'No'} Items Processed: - {queuer.executionCount()} + {queuer.state().executionCount} % Saved: @@ -235,7 +259,7 @@ function App3() { {instantExecutionCount() === 0 ? '0' : Math.round( - ((instantExecutionCount() - queuer.executionCount()) / + ((instantExecutionCount() - queuer.state().executionCount) / instantExecutionCount()) * 100, )} @@ -244,7 +268,7 @@ function App3() { Queue Items: - {queuer.allItems().join(', ')} + {queuer.state().items.join(', ')} @@ -257,16 +281,28 @@ function App3() { margin: '16px 0', }} > - - - - diff --git a/examples/solid/createRateLimitedSignal/package.json b/examples/solid/createRateLimitedSignal/package.json index 2b402365b..2e09beed6 100644 --- a/examples/solid/createRateLimitedSignal/package.json +++ b/examples/solid/createRateLimitedSignal/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createRateLimitedSignal/src/index.tsx b/examples/solid/createRateLimitedSignal/src/index.tsx index e51b13675..46ecdea0b 100644 --- a/examples/solid/createRateLimitedSignal/src/index.tsx +++ b/examples/solid/createRateLimitedSignal/src/index.tsx @@ -30,11 +30,11 @@ function App1() { Execution Count: - {rateLimiter.executionCount()} + {rateLimiter.state().executionCount} Rejection Count: - {rateLimiter.rejectionCount()} + {rateLimiter.state().rejectionCount} Instant Count: @@ -48,7 +48,7 @@ function App1() {
    - @@ -91,11 +91,11 @@ function App2() { Execution Count: - {rateLimiter.executionCount()} + {rateLimiter.state().executionCount} Rejection Count: - {rateLimiter.rejectionCount()} + {rateLimiter.state().rejectionCount} Instant Search: @@ -108,7 +108,7 @@ function App2() {
    - @@ -183,11 +183,11 @@ function App3() { Rate Limited Executions: - {rateLimiter.executionCount()} + {rateLimiter.state().executionCount} Rejected Executions: - {rateLimiter.rejectionCount()} + {rateLimiter.state().rejectionCount} % Reduction: @@ -195,7 +195,8 @@ function App3() { {instantExecutionCount() === 0 ? '0' : Math.round( - ((instantExecutionCount() - rateLimiter.executionCount()) / + ((instantExecutionCount() - + rateLimiter.state().executionCount) / instantExecutionCount()) * 100, )} diff --git a/examples/solid/createRateLimitedValue/package.json b/examples/solid/createRateLimitedValue/package.json index 1674ac30d..70261a75f 100644 --- a/examples/solid/createRateLimitedValue/package.json +++ b/examples/solid/createRateLimitedValue/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createRateLimitedValue/src/index.tsx b/examples/solid/createRateLimitedValue/src/index.tsx index c26c409d7..c82da0807 100644 --- a/examples/solid/createRateLimitedValue/src/index.tsx +++ b/examples/solid/createRateLimitedValue/src/index.tsx @@ -141,11 +141,11 @@ function App3() { Rate Limited Executions: - {rateLimiter.executionCount()} + {rateLimiter.state().executionCount} Rejected Executions: - {rateLimiter.rejectionCount()} + {rateLimiter.state().rejectionCount} % Reduction: @@ -153,7 +153,8 @@ function App3() { {instantExecutionCount() === 0 ? '0' : Math.round( - ((instantExecutionCount() - rateLimiter.executionCount()) / + ((instantExecutionCount() - + rateLimiter.state().executionCount) / instantExecutionCount()) * 100, )} diff --git a/examples/solid/createRateLimiter/package.json b/examples/solid/createRateLimiter/package.json index 5cc1c9b7b..4100be27f 100644 --- a/examples/solid/createRateLimiter/package.json +++ b/examples/solid/createRateLimiter/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createRateLimiter/src/index.tsx b/examples/solid/createRateLimiter/src/index.tsx index d9877f0c0..26b0653cb 100644 --- a/examples/solid/createRateLimiter/src/index.tsx +++ b/examples/solid/createRateLimiter/src/index.tsx @@ -29,11 +29,11 @@ function App1() { Execution Count: - {rateLimiter.executionCount()} + {rateLimiter.state().executionCount} Rejection Count: - {rateLimiter.rejectionCount()} + {rateLimiter.state().rejectionCount} Instant Count: @@ -47,7 +47,7 @@ function App1() {
    - @@ -90,11 +90,11 @@ function App2() { Execution Count: - {rateLimiter.executionCount()} + {rateLimiter.state().executionCount} Rejection Count: - {rateLimiter.rejectionCount()} + {rateLimiter.state().rejectionCount} Instant Search: @@ -107,7 +107,7 @@ function App2() {
    - @@ -180,11 +180,11 @@ function App3() { Rate Limited Executions: - {rateLimiter.executionCount()} + {rateLimiter.state().executionCount} Rejected Executions: - {rateLimiter.rejectionCount()} + {rateLimiter.state().rejectionCount} % Reduction: @@ -192,7 +192,8 @@ function App3() { {instantExecutionCount() === 0 ? '0' : Math.round( - ((instantExecutionCount() - rateLimiter.executionCount()) / + ((instantExecutionCount() - + rateLimiter.state().executionCount) / instantExecutionCount()) * 100, )} diff --git a/examples/solid/createThrottledSignal/package.json b/examples/solid/createThrottledSignal/package.json index d8443c410..bf8131fef 100644 --- a/examples/solid/createThrottledSignal/package.json +++ b/examples/solid/createThrottledSignal/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createThrottledSignal/src/index.tsx b/examples/solid/createThrottledSignal/src/index.tsx index 03d1b85ab..65401d4f8 100644 --- a/examples/solid/createThrottledSignal/src/index.tsx +++ b/examples/solid/createThrottledSignal/src/index.tsx @@ -31,7 +31,7 @@ function App1() { Execution Count: - {throttler.executionCount()} + {throttler.state().executionCount} Instant Count: @@ -84,7 +84,7 @@ function App2() { Execution Count: - {throttler.executionCount()} + {throttler.state().executionCount} Instant Search: @@ -159,11 +159,13 @@ function App3() { Throttled Executions: - {throttler.executionCount()} + {throttler.state().executionCount} Saved Executions: - {instantExecutionCount() - throttler.executionCount()} + + {instantExecutionCount() - throttler.state().executionCount} + % Reduction: @@ -171,7 +173,8 @@ function App3() { {instantExecutionCount() === 0 ? '0' : Math.round( - ((instantExecutionCount() - throttler.executionCount()) / + ((instantExecutionCount() - + throttler.state().executionCount) / instantExecutionCount()) * 100, )} diff --git a/examples/solid/createThrottledValue/package.json b/examples/solid/createThrottledValue/package.json index 16ba557a5..ae870a4d8 100644 --- a/examples/solid/createThrottledValue/package.json +++ b/examples/solid/createThrottledValue/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createThrottledValue/src/index.tsx b/examples/solid/createThrottledValue/src/index.tsx index a10bf258d..56c8cb3c5 100644 --- a/examples/solid/createThrottledValue/src/index.tsx +++ b/examples/solid/createThrottledValue/src/index.tsx @@ -133,11 +133,13 @@ function App3() { Throttled Executions: - {throttler.executionCount()} + {throttler.state().executionCount} Saved Executions: - {instantExecutionCount() - throttler.executionCount()} + + {instantExecutionCount() - throttler.state().executionCount} + % Reduction: @@ -145,7 +147,8 @@ function App3() { {instantExecutionCount() === 0 ? '0' : Math.round( - ((instantExecutionCount() - throttler.executionCount()) / + ((instantExecutionCount() - + throttler.state().executionCount) / instantExecutionCount()) * 100, )} diff --git a/examples/solid/createThrottler/package.json b/examples/solid/createThrottler/package.json index 4130a93f4..74f442fca 100644 --- a/examples/solid/createThrottler/package.json +++ b/examples/solid/createThrottler/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/createThrottler/src/index.tsx b/examples/solid/createThrottler/src/index.tsx index 5a23d5d80..0e5170f3d 100644 --- a/examples/solid/createThrottler/src/index.tsx +++ b/examples/solid/createThrottler/src/index.tsx @@ -29,7 +29,7 @@ function App1() { Execution Count: - {setCountThrottler.executionCount()} + {setCountThrottler.state().executionCount} Instant Count: @@ -82,7 +82,7 @@ function App2() { Execution Count: - {setSearchThrottler.executionCount()} + {setSearchThrottler.state().executionCount} Instant Search: @@ -155,12 +155,13 @@ function App3() { Throttled Executions: - {setValueThrottler.executionCount()} + {setValueThrottler.state().executionCount} Saved Executions: - {instantExecutionCount() - setValueThrottler.executionCount()} + {instantExecutionCount() - + setValueThrottler.state().executionCount} @@ -170,7 +171,7 @@ function App3() { ? '0' : Math.round( ((instantExecutionCount() - - setValueThrottler.executionCount()) / + setValueThrottler.state().executionCount) / instantExecutionCount()) * 100, )} diff --git a/examples/solid/debounce/package.json b/examples/solid/debounce/package.json index 9b72bd0cb..1ff7cde36 100644 --- a/examples/solid/debounce/package.json +++ b/examples/solid/debounce/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/queue/package.json b/examples/solid/queue/package.json index a8110d57e..35a85aafd 100644 --- a/examples/solid/queue/package.json +++ b/examples/solid/queue/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/queue/src/index.tsx b/examples/solid/queue/src/index.tsx index c0191c662..1a8655a72 100644 --- a/examples/solid/queue/src/index.tsx +++ b/examples/solid/queue/src/index.tsx @@ -16,7 +16,7 @@ function App1() { wait: 1000, onItemsChange: (queue) => { setQueueItems(queue.peekAllItems()) - setProcessedCount(queue.getExecutionCount()) + setProcessedCount(queue.store.state.executionCount) }, }) @@ -70,7 +70,7 @@ function App2() { wait: 500, onItemsChange: (queue) => { setQueueItems(queue.peekAllItems()) - setProcessedCount(queue.getExecutionCount()) + setProcessedCount(queue.store.state.executionCount) }, }) @@ -132,7 +132,7 @@ function App3() { wait: 100, onItemsChange: (queue) => { setQueueItems(queue.peekAllItems()) - setProcessedCount(queue.getExecutionCount()) + setProcessedCount(queue.store.state.executionCount) }, }) diff --git a/examples/solid/rateLimit/package.json b/examples/solid/rateLimit/package.json index 3f51db1ea..346195812 100644 --- a/examples/solid/rateLimit/package.json +++ b/examples/solid/rateLimit/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/examples/solid/throttle/package.json b/examples/solid/throttle/package.json index 4600b6369..40c14f022 100644 --- a/examples/solid/throttle/package.json +++ b/examples/solid/throttle/package.json @@ -13,8 +13,8 @@ "solid-js": "^1.9.7" }, "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^7.0.4", + "vite-plugin-solid": "^2.11.7" }, "browserslist": { "production": [ diff --git a/knip.json b/knip.json index e4fc4b1c1..dfc6c27a8 100644 --- a/knip.json +++ b/knip.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "ignoreDependencies": ["@size-limit/preset-small-lib", "@faker-js/faker"], + "ignoreDependencies": ["@faker-js/faker"], "ignoreWorkspaces": ["examples/**"], "workspaces": { "packages/react-pacer": { diff --git a/package.json b/package.json index 170670ab4..c26339dbb 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "nx affected --targets=build --exclude=examples/** && size-limit", "build:all": "nx run-many --targets=build --exclude=examples/** && size-limit", - "build:core": "nx build @tanstack/pacer && size-limit", + "build:core": "nx run-many --targets=build --projects=packages/pacer,packages/persister && size-limit", "changeset": "changeset", "changeset:publish": "changeset publish", "changeset:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm prettier:write", @@ -48,37 +48,39 @@ "size-limit": [ { "path": "packages/pacer/dist/esm/index.js", - "limit": "4 KB" + "limit": "8 KB" } ], "devDependencies": { - "@changesets/cli": "^2.29.4", - "@faker-js/faker": "^9.8.0", + "@changesets/cli": "^2.29.5", + "@faker-js/faker": "^9.9.0", "@size-limit/preset-small-lib": "^11.2.0", "@svitejs/changesets-changelog-github-compact": "^1.2.0", - "@tanstack/config": "0.18.2", + "@tanstack/config": "0.19.0", "@testing-library/jest-dom": "^6.6.3", - "@types/node": "^22.15.30", - "eslint": "^9.28.0", + "@types/node": "^24.0.13", + "eslint": "^9.30.1", "eslint-plugin-unused-imports": "^4.1.4", "fast-glob": "^3.3.3", "jsdom": "^26.1.0", - "knip": "^5.60.2", + "knip": "^5.61.3", "markdown-link-extractor": "^4.0.2", - "nx": "^21.1.3", + "nx": "^21.2.2", "premove": "^4.0.0", - "prettier": "^3.5.3", + "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", "publint": "^0.3.12", - "sherif": "^1.5.0", + "sherif": "^1.6.1", "size-limit": "^11.2.0", "typescript": "5.8.3", - "vite": "^6.3.5", - "vitest": "^3.2.2" + "vite": "^7.0.4", + "vitest": "^3.2.4" }, "overrides": { "@tanstack/pacer": "workspace:*", + "@tanstack/persister": "workspace:*", "@tanstack/react-pacer": "workspace:*", + "@tanstack/react-persister": "workspace:*", "@tanstack/solid-pacer": "workspace:*" } } diff --git a/packages/pacer/package.json b/packages/pacer/package.json index 5c8b2e3de..63e89f25d 100644 --- a/packages/pacer/package.json +++ b/packages/pacer/package.json @@ -37,6 +37,16 @@ "default": "./dist/cjs/index.cjs" } }, + "./async-batcher": { + "import": { + "types": "./dist/esm/async-batcher.d.ts", + "default": "./dist/esm/async-batcher.js" + }, + "require": { + "types": "./dist/cjs/async-batcher.d.cts", + "default": "./dist/cjs/async-batcher.cjs" + } + }, "./async-debouncer": { "import": { "types": "./dist/esm/async-debouncer.d.ts", @@ -87,16 +97,6 @@ "default": "./dist/cjs/batcher.cjs" } }, - "./compare": { - "import": { - "types": "./dist/esm/compare.d.ts", - "default": "./dist/esm/compare.js" - }, - "require": { - "types": "./dist/cjs/compare.d.cts", - "default": "./dist/cjs/compare.cjs" - } - }, "./debouncer": { "import": { "types": "./dist/esm/debouncer.d.ts", @@ -169,5 +169,8 @@ "test:types": "tsc", "test:build": "publint --strict", "build": "vite build" + }, + "dependencies": { + "@tanstack/store": "^0.7.2" } } diff --git a/packages/pacer/src/async-batcher.ts b/packages/pacer/src/async-batcher.ts new file mode 100644 index 000000000..f70b77534 --- /dev/null +++ b/packages/pacer/src/async-batcher.ts @@ -0,0 +1,475 @@ +import { Store } from '@tanstack/store' +import { parseFunctionOrValue } from './utils' +import type { OptionalKeys } from './types' + +export interface AsyncBatcherState { + /** + * Number of batch executions that have resulted in errors + */ + errorCount: number + /** + * Array of items that failed during batch processing + */ + failedItems: Array + /** + * Whether the batcher has no items to process (items array is empty) + */ + isEmpty: boolean + /** + * Whether a batch is currently being processed asynchronously + */ + isExecuting: boolean + /** + * Whether the batcher is waiting for the timeout to trigger batch processing + */ + isPending: boolean + /** + * Whether the batcher is active and will process items automatically + */ + isRunning: boolean + /** + * Array of items currently queued for batch processing + */ + items: Array + /** + * The result from the most recent batch execution + */ + lastResult: any + /** + * Number of batch executions that have completed (either successfully or with errors) + */ + settleCount: number + /** + * Number of items currently in the batch queue + */ + size: number + /** + * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured + */ + status: 'idle' | 'pending' | 'executing' | 'populated' + /** + * Number of batch executions that have completed successfully + */ + successCount: number + /** + * Total number of items that have been processed across all batches + */ + totalItemsProcessed: number + /** + * Total number of items that have failed processing across all batches + */ + totalItemsFailed: number +} + +function getDefaultAsyncBatcherState(): AsyncBatcherState { + return { + errorCount: 0, + failedItems: [], + isEmpty: true, + isExecuting: false, + isPending: false, + isRunning: true, + items: [], + lastResult: undefined, + settleCount: 0, + size: 0, + status: 'idle', + successCount: 0, + totalItemsProcessed: 0, + totalItemsFailed: 0, + } +} + +/** + * Options for configuring an AsyncBatcher instance + */ +export interface AsyncBatcherOptions { + /** + * Custom function to determine if a batch should be processed + * Return true to process the batch immediately + */ + getShouldExecute?: ( + items: Array, + batcher: AsyncBatcher, + ) => boolean + /** + * Initial state for the async batcher + */ + initialState?: Partial> + /** + * Maximum number of items in a batch + * @default Infinity + */ + maxSize?: number + /** + * Optional error handler for when the batch function throws. + * If provided, the handler will be called with the error and batcher instance. + * This can be used alongside throwOnError - the handler will be called before any error is thrown. + */ + onError?: ( + error: unknown, + failedItems: Array, + batcher: AsyncBatcher, + ) => void + /** + * Callback fired after a batch is processed + */ + onExecute?: (batcher: AsyncBatcher) => void + /** + * Callback fired after items are added to the batcher + */ + onItemsChange?: (batcher: AsyncBatcher) => void + /** + * Optional callback to call when a batch is settled (completed or failed) + */ + onSettled?: (batcher: AsyncBatcher) => void + /** + * Optional callback to call when a batch succeeds + */ + onSuccess?: (result: any, batcher: AsyncBatcher) => void + /** + * Whether the batcher should start processing immediately + * @default true + */ + started?: boolean + /** + * Whether to throw errors when they occur. + * Defaults to true if no onError handler is provided, false if an onError handler is provided. + * Can be explicitly set to override these defaults. + */ + throwOnError?: boolean + /** + * Maximum time in milliseconds to wait before processing a batch. + * If the wait duration has elapsed, the batch will be processed. + * If not provided, the batch will not be triggered by a timeout. + * @default Infinity + */ + wait?: number | ((asyncBatcher: AsyncBatcher) => number) +} + +type AsyncBatcherOptionsWithOptionalCallbacks = OptionalKeys< + Required>, + | 'initialState' + | 'onError' + | 'onExecute' + | 'onItemsChange' + | 'onSettled' + | 'onSuccess' +> + +const defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks = { + getShouldExecute: () => false, + maxSize: Infinity, + started: true, + throwOnError: true, + wait: Infinity, +} + +/** + * A class that collects items and processes them in batches asynchronously. + * + * This is the async version of the Batcher class. Unlike the sync version, this async batcher: + * - Handles promises and returns results from batch executions + * - Provides error handling with configurable error behavior + * - Tracks success, error, and settle counts separately + * - Has state tracking for when batches are executing + * - Returns the result of the batch function execution + * + * Batching is a technique for grouping multiple operations together to be processed as a single unit. + * + * The AsyncBatcher provides a flexible way to implement async batching with configurable: + * - Maximum batch size (number of items per batch) + * - Time-based batching (process after X milliseconds) + * - Custom batch processing logic via getShouldExecute + * - Event callbacks for monitoring batch operations + * - Error handling for failed batch operations + * + * Error Handling: + * - If an `onError` handler is provided, it will be called with the error and batcher instance + * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown + * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed + * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown + * - The error state can be checked using the AsyncBatcher instance + * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the async batcher + * - Use `onSuccess` callback to react to successful batch execution and implement custom logic + * - Use `onError` callback to react to batch execution errors and implement custom error handling + * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic + * - Use `onExecute` callback to react to batch execution and implement custom logic + * - Use `onItemsChange` callback to react to items being added or removed from the batcher + * - The state includes total items processed, success/error counts, and execution status + * - State can be accessed via `asyncBatcher.store.state` when using the class directly + * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state` + * + * @example + * ```ts + * const batcher = new AsyncBatcher( + * async (items) => { + * const result = await processItems(items); + * console.log('Processing batch:', items); + * return result; + * }, + * { + * maxSize: 5, + * wait: 2000, + * onSuccess: (result) => console.log('Batch succeeded:', result), + * onError: (error) => console.error('Batch failed:', error) + * } + * ); + * + * batcher.addItem(1); + * batcher.addItem(2); + * // After 2 seconds or when 5 items are added, whichever comes first, + * // the batch will be processed and the result will be available + * // batcher.execute() // manually trigger a batch + * ``` + */ +export class AsyncBatcher { + readonly store: Store>> = new Store( + getDefaultAsyncBatcherState(), + ) + options: AsyncBatcherOptionsWithOptionalCallbacks + #timeoutId: NodeJS.Timeout | null = null + + constructor( + private fn: (items: Array) => Promise, + initialOptions: AsyncBatcherOptions, + ) { + this.options = { + ...defaultOptions, + ...initialOptions, + throwOnError: initialOptions.throwOnError ?? !initialOptions.onError, + } + this.#setState(this.options.initialState ?? {}) + } + + /** + * Updates the async batcher options + */ + setOptions = (newOptions: Partial>): void => { + this.options = { ...this.options, ...newOptions } + } + + #setState = (newState: Partial>): void => { + this.store.setState((state) => { + const combinedState = { + ...state, + ...newState, + } + const { isExecuting, isPending, items } = combinedState + const size = items.length + const isEmpty = size === 0 + return { + ...combinedState, + isEmpty, + size, + status: isExecuting + ? 'executing' + : isPending + ? 'pending' + : isEmpty + ? 'idle' + : 'populated', + } + }) + } + + #getWait = (): number => { + return parseFunctionOrValue(this.options.wait, this) + } + + /** + * Adds an item to the async batcher + * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed + */ + addItem = (item: TValue): void => { + this.#setState({ + items: [...this.store.state.items, item], + isPending: this.options.wait !== Infinity, + }) + this.options.onItemsChange?.(this) + + const shouldProcess = + this.store.state.items.length >= this.options.maxSize || + this.options.getShouldExecute(this.store.state.items, this) + + if (shouldProcess) { + this.#execute() + } else if (this.store.state.isRunning && this.options.wait !== Infinity) { + this.#clearTimeout() // clear any pending timeout to replace it with a new one + this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()) + } + } + + /** + * Processes the current batch of items asynchronously. + * This method will automatically be triggered if the batcher is running and any of these conditions are met: + * - The number of items reaches maxSize + * - The wait duration has elapsed + * - The getShouldExecute function returns true upon adding an item + * + * You can also call this method manually to process the current batch at any time. + * + * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError + * @throws The error from the batch function if no onError handler is configured + */ + #execute = async (): Promise => { + if (this.store.state.items.length === 0) { + return undefined + } + + const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions) + this.clear() // Clear items before processing to prevent race conditions + this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed + + this.#setState({ isExecuting: true }) + + try { + const result = await this.fn(batch) // EXECUTE + this.#setState({ + totalItemsProcessed: + this.store.state.totalItemsProcessed + batch.length, + lastResult: result, + successCount: this.store.state.successCount + 1, + }) + this.options.onSuccess?.(result, this) + return result + } catch (error) { + this.#setState({ + errorCount: this.store.state.errorCount + 1, + failedItems: [...this.store.state.failedItems, ...batch], + totalItemsFailed: this.store.state.totalItemsFailed + batch.length, + }) + this.options.onError?.(error, batch, this) + if (this.options.throwOnError) { + throw error + } + return undefined + } finally { + this.#setState({ + isExecuting: false, + settleCount: this.store.state.settleCount + 1, + }) + this.options.onSettled?.(this) + this.options.onExecute?.(this) + } + } + + /** + * Processes the current batch of items immediately + */ + flush = async (): Promise => { + this.#clearTimeout() // clear any pending timeout + return await this.#execute() + } + + /** + * Stops the async batcher from processing batches + */ + stop = (): void => { + this.#setState({ isRunning: false }) + this.#clearTimeout() + } + + /** + * Starts the async batcher and processes any pending items + */ + start = (): void => { + this.#setState({ isRunning: true }) + if (this.store.state.items.length > 0 && !this.#timeoutId) { + this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()) + } + } + + /** + * Returns a copy of all items in the async batcher + */ + peekAllItems = (): Array => { + return [...this.store.state.items] + } + + peekFailedItems = (): Array => { + return [...this.store.state.failedItems] + } + + #clearTimeout = (): void => { + if (this.#timeoutId) { + clearTimeout(this.#timeoutId) + this.#timeoutId = null + } + } + + /** + * Removes all items from the async batcher + */ + clear = (): void => { + this.#setState({ items: [], failedItems: [], isPending: false }) + } + + /** + * Resets the async batcher state to its default values + */ + reset = (): void => { + this.#setState(getDefaultAsyncBatcherState()) + this.options.onItemsChange?.(this) + } +} + +/** + * Creates an async batcher that processes items in batches + * + * Unlike the sync batcher, this async version: + * - Handles promises and returns results from batch executions + * - Provides error handling with configurable error behavior + * - Tracks success, error, and settle counts separately + * - Has state tracking for when batches are executing + * + * Error Handling: + * - If an `onError` handler is provided, it will be called with the error and batcher instance + * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown + * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed + * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown + * - The error state can be checked using the underlying AsyncBatcher instance + * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the async batcher + * - Use `onSuccess` callback to react to successful batch execution and implement custom logic + * - Use `onError` callback to react to batch execution errors and implement custom error handling + * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic + * - Use `onExecute` callback to react to batch execution and implement custom logic + * - Use `onItemsChange` callback to react to items being added or removed from the batcher + * - The state includes total items processed, success/error counts, and execution status + * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property + * - When using framework adapters (React/Solid), state is accessed from the hook's state property + * + * @example + * ```ts + * const batchItems = asyncBatch( + * async (items) => { + * const result = await processApiCall(items); + * console.log('Processing:', items); + * return result; + * }, + * { + * maxSize: 3, + * wait: 1000, + * onSuccess: (result) => console.log('Batch succeeded:', result), + * onError: (error) => console.error('Batch failed:', error) + * } + * ); + * + * batchItems(1); + * batchItems(2); + * batchItems(3); // Triggers batch processing + * ``` + */ +export function asyncBatch( + fn: (items: Array) => Promise, + options: AsyncBatcherOptions, +) { + const batcher = new AsyncBatcher(fn, options) + return batcher.addItem +} diff --git a/packages/pacer/src/async-debouncer.ts b/packages/pacer/src/async-debouncer.ts index 59787261f..c7ab604f2 100644 --- a/packages/pacer/src/async-debouncer.ts +++ b/packages/pacer/src/async-debouncer.ts @@ -1,6 +1,62 @@ +import { Store } from '@tanstack/store' import { parseFunctionOrValue } from './utils' import type { AnyAsyncFunction, OptionalKeys } from './types' +export interface AsyncDebouncerState { + /** + * Whether the debouncer can execute on the leading edge of the timeout + */ + canLeadingExecute: boolean + /** + * Number of function executions that have resulted in errors + */ + errorCount: number + /** + * Whether the debounced function is currently executing asynchronously + */ + isExecuting: boolean + /** + * Whether the debouncer is waiting for the timeout to trigger execution + */ + isPending: boolean + /** + * The arguments from the most recent call to maybeExecute + */ + lastArgs: Parameters | undefined + /** + * The result from the most recent successful function execution + */ + lastResult: ReturnType | undefined + /** + * Number of function executions that have completed (either successfully or with errors) + */ + settleCount: number + /** + * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed + */ + status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled' + /** + * Number of function executions that have completed successfully + */ + successCount: number +} + +function getDefaultAsyncDebouncerState< + TFn extends AnyAsyncFunction, +>(): AsyncDebouncerState { + return structuredClone({ + canLeadingExecute: true, + errorCount: 0, + isExecuting: false, + isPending: false, + lastArgs: undefined, + lastResult: undefined, + settleCount: 0, + successCount: 0, + status: 'idle', + }) +} + /** * Options for configuring an async debounced function */ @@ -11,6 +67,10 @@ export interface AsyncDebouncerOptions { * Defaults to true. */ enabled?: boolean | ((debouncer: AsyncDebouncer) => boolean) + /** + * Initial state for the async debouncer + */ + initialState?: Partial> /** * Whether to execute on the leading edge of the timeout. * Defaults to false. @@ -51,7 +111,7 @@ export interface AsyncDebouncerOptions { type AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys< AsyncDebouncerOptions, - 'onError' | 'onSettled' | 'onSuccess' + 'initialState' | 'onError' | 'onSettled' | 'onSuccess' > const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = { @@ -76,10 +136,18 @@ const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = { * instead of setting the result on a state variable from within the debounced function. * * Error Handling: - * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller. - * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown. - * - The error count can be tracked using `getErrorCount()`. - * - The debouncer maintains its state and can continue to be used after an error occurs. + * - If an `onError` handler is provided, it will be called with the error and debouncer instance + * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown + * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed + * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown + * - The error state can be checked using the underlying store + * + * State Management: + * - The debouncer uses a reactive store for state management + * - Use `initialState` to provide initial state values when creating the async debouncer + * - The state includes canLeadingExecute, error count, execution status, and success/settle counts + * - State can be accessed via the `store` property and its `state` getter + * - The store is reactive and will notify subscribers of state changes * * @example * ```ts @@ -99,18 +167,13 @@ const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = { * ``` */ export class AsyncDebouncer { - private _options: AsyncDebouncerOptionsWithOptionalCallbacks - private _abortController: AbortController | null = null - private _canLeadingExecute = true - private _errorCount = 0 - private _isExecuting = false - private _isPending = false - private _lastArgs: Parameters | undefined - private _lastResult: ReturnType | undefined - private _settleCount = 0 - private _successCount = 0 - private _timeoutId: NodeJS.Timeout | null = null - private _resolvePreviousPromise: + readonly store: Store>> = new Store< + AsyncDebouncerState + >(getDefaultAsyncDebouncerState()) + options: AsyncDebouncerOptions + #abortController: AbortController | null = null + #timeoutId: NodeJS.Timeout | null = null + #resolvePreviousPromise: | ((value?: ReturnType | undefined) => void) | null = null @@ -118,44 +181,60 @@ export class AsyncDebouncer { private fn: TFn, initialOptions: AsyncDebouncerOptions, ) { - this._options = { + this.options = { ...defaultOptions, ...initialOptions, throwOnError: initialOptions.throwOnError ?? !initialOptions.onError, } + this.#setState(this.options.initialState ?? {}) } /** - * Updates the debouncer options + * Updates the async debouncer options */ - setOptions(newOptions: Partial>): void { - this._options = { ...this._options, ...newOptions } + setOptions = (newOptions: Partial>): void => { + this.options = { ...this.options, ...newOptions } - // End the pending state if the debouncer is disabled - if (!this._options.enabled) { - this._isPending = false + // Cancel pending execution if the debouncer is disabled + if (!this.#getEnabled()) { + this.cancel() } } - /** - * Returns the current debouncer options - */ - getOptions(): AsyncDebouncerOptions { - return this._options + #setState = (newState: Partial>): void => { + this.store.setState((state) => { + const combinedState = { + ...state, + ...newState, + } + const { isPending, isExecuting, settleCount } = combinedState + return { + ...combinedState, + status: !this.#getEnabled() + ? 'disabled' + : isPending + ? 'pending' + : isExecuting + ? 'executing' + : settleCount > 0 + ? 'settled' + : 'idle', + } + }) } /** * Returns the current debouncer enabled state */ - getEnabled(): boolean { - return !!parseFunctionOrValue(this._options.enabled, this) + #getEnabled = (): boolean => { + return !!parseFunctionOrValue(this.options.enabled, this) } /** * Returns the current debouncer wait state */ - getWait(): number { - return parseFunctionOrValue(this._options.wait, this) + #getWait = (): number => { + return parseFunctionOrValue(this.options.wait, this) } /** @@ -172,135 +251,126 @@ export class AsyncDebouncer { * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError * @throws The error from the debounced function if no onError handler is configured */ - async maybeExecute( + maybeExecute = async ( ...args: Parameters - ): Promise | undefined> { - this._cancel() - this._lastArgs = args + ): Promise | undefined> => { + if (!this.#getEnabled()) return undefined + this.#cancelPendingExecution() + this.#setState({ lastArgs: args }) // Handle leading execution - if (this._options.leading && this._canLeadingExecute) { - this._canLeadingExecute = false - await this.execute(...args) - return this._lastResult + if (this.options.leading && this.store.state.canLeadingExecute) { + this.#setState({ canLeadingExecute: false }) + await this.#execute(...args) + return this.store.state.lastResult } // Handle trailing execution - if (this._options.trailing) { - this._isPending = true + if (this.options.trailing && this.#getEnabled()) { + this.#setState({ isPending: true }) } return new Promise((resolve) => { - this._resolvePreviousPromise = resolve - this._timeoutId = setTimeout(async () => { + this.#resolvePreviousPromise = resolve + this.#timeoutId = setTimeout(async () => { // Execute trailing if enabled - if (this._options.trailing && this._lastArgs) { - await this.execute(...this._lastArgs) + if (this.options.trailing && this.store.state.lastArgs) { + await this.#execute(...this.store.state.lastArgs) } // Reset state and resolve - this._canLeadingExecute = true - this._resolvePreviousPromise = null - resolve(this._lastResult) - }, this.getWait()) + this.#setState({ canLeadingExecute: true }) + this.#resolvePreviousPromise = null + resolve(this.store.state.lastResult) + }, this.#getWait()) }) } - private async execute( + #execute = async ( ...args: Parameters - ): Promise | undefined> { - if (!this.getEnabled()) return undefined - this._abortController = new AbortController() + ): Promise | undefined> => { + if (!this.#getEnabled()) return undefined + this.#abortController = new AbortController() try { - this._isExecuting = true - this._lastResult = await this.fn(...args) // EXECUTE! - this._successCount++ - this._options.onSuccess?.(this._lastResult!, this) + this.#setState({ isExecuting: true }) + const result = await this.fn(...args) // EXECUTE! + this.#setState({ + lastResult: result, + successCount: this.store.state.successCount + 1, + }) + this.options.onSuccess?.(result, this) } catch (error) { - this._errorCount++ - this._options.onError?.(error, this) - if (this._options.throwOnError) { + this.#setState({ + errorCount: this.store.state.errorCount + 1, + }) + this.options.onError?.(error, this) + if (this.options.throwOnError) { throw error } } finally { - this._isExecuting = false - this._isPending = false - this._settleCount++ - this._abortController = null - this._options.onSettled?.(this) + this.#setState({ + isExecuting: false, + isPending: false, + settleCount: this.store.state.settleCount + 1, + }) + this.#abortController = null + this.options.onSettled?.(this) } - return this._lastResult + return this.store.state.lastResult } /** - * Cancel without resetting _canLeadingExecute + * Processes the current pending execution immediately */ - private _cancel(): void { - if (this._timeoutId) { - clearTimeout(this._timeoutId) - this._timeoutId = null - } - if (this._abortController) { - this._abortController.abort() - this._abortController = null - } - if (this._resolvePreviousPromise) { - this._resolvePreviousPromise(this._lastResult) - this._resolvePreviousPromise = null + flush = (): void => { + if (this.store.state.isPending && this.store.state.lastArgs) { + this.#abortExecution() // abort any current execution + this.#clearTimeout() // clear any existing timeout + this.#execute(...this.store.state.lastArgs) } - this._lastArgs = undefined - this._isPending = false - this._isExecuting = false - } - - /** - * Cancels any pending execution or aborts any execution in progress - */ - cancel(): void { - this._canLeadingExecute = true - this._cancel() - } - - /** - * Returns the last result of the debounced function - */ - getLastResult(): ReturnType | undefined { - return this._lastResult } - /** - * Returns the number of times the function has been executed successfully - */ - getSuccessCount(): number { - return this._successCount + #clearTimeout = (): void => { + if (this.#timeoutId) { + clearTimeout(this.#timeoutId) + this.#timeoutId = null + } } - /** - * Returns the number of times the function has settled (completed or errored) - */ - getSettleCount(): number { - return this._settleCount + #cancelPendingExecution = (): void => { + this.#clearTimeout() + if (this.#resolvePreviousPromise) { + this.#resolvePreviousPromise(this.store.state.lastResult) + this.#resolvePreviousPromise = null + } + this.#setState({ + isPending: false, + isExecuting: false, + lastArgs: undefined, + }) } - /** - * Returns the number of times the function has errored - */ - getErrorCount(): number { - return this._errorCount + #abortExecution = (): void => { + if (this.#abortController) { + this.#abortController.abort() + this.#abortController = null + } } /** - * Returns `true` if there is a pending execution queued up for trailing execution + * Cancels any pending execution or aborts any execution in progress */ - getIsPending(): boolean { - return this.getEnabled() && this._isPending + cancel = (): void => { + this.#cancelPendingExecution() + this.#abortExecution() + this.#setState({ canLeadingExecute: true }) } /** - * Returns `true` if there is currently an execution in progress + * Resets the debouncer state to its default values */ - getIsExecuting(): boolean { - return this._isExecuting + reset = (): void => { + this.#setState(getDefaultAsyncDebouncerState()) } } @@ -320,6 +390,16 @@ export class AsyncDebouncer { * - The error state can be checked using the underlying AsyncDebouncer instance * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the async debouncer + * - Use `onSuccess` callback to react to successful function execution and implement custom logic + * - Use `onError` callback to react to function execution errors and implement custom error handling + * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic + * - The state includes canLeadingExecute, error count, execution status, and success/settle counts + * - State can be accessed via `asyncDebouncer.store.state` when using the class directly + * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state` + * * @example * ```ts * const debounced = asyncDebounce(async (value: string) => { @@ -343,5 +423,5 @@ export function asyncDebounce( initialOptions: AsyncDebouncerOptions, ) { const asyncDebouncer = new AsyncDebouncer(fn, initialOptions) - return asyncDebouncer.maybeExecute.bind(asyncDebouncer) + return asyncDebouncer.maybeExecute } diff --git a/packages/pacer/src/async-queuer.ts b/packages/pacer/src/async-queuer.ts index 478911792..b9cf97f73 100644 --- a/packages/pacer/src/async-queuer.ts +++ b/packages/pacer/src/async-queuer.ts @@ -1,7 +1,96 @@ +import { Store } from '@tanstack/store' import { parseFunctionOrValue } from './utils' import type { OptionalKeys } from './types' import type { QueuePosition } from './queuer' +export interface AsyncQueuerState { + /** + * Items currently being processed by the queuer + */ + activeItems: Array + /** + * Number of task executions that have resulted in errors + */ + errorCount: number + /** + * Number of items that have been removed from the queue due to expiration + */ + expirationCount: number + /** + * Whether the queuer has no items to process (items array is empty) + */ + isEmpty: boolean + /** + * Whether the queuer has reached its maximum capacity + */ + isFull: boolean + /** + * Whether the queuer is not currently processing any items + */ + isIdle: boolean + /** + * Whether the queuer is active and will process items automatically + */ + isRunning: boolean + /** + * Timestamps when items were added to the queue for expiration tracking + */ + itemTimestamps: Array + /** + * Array of items currently waiting to be processed + */ + items: Array + /** + * The result from the most recent task execution + */ + lastResult: any + /** + * Whether the queuer has a pending timeout for processing the next item + */ + pendingTick: boolean + /** + * Number of items that have been rejected from being added to the queue + */ + rejectionCount: number + /** + * Number of task executions that have completed (either successfully or with errors) + */ + settledCount: number + /** + * Number of items currently in the queue + */ + size: number + /** + * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused + */ + status: 'idle' | 'running' | 'stopped' + /** + * Number of task executions that have completed successfully + */ + successCount: number +} + +function getDefaultAsyncQueuerState(): AsyncQueuerState { + return structuredClone({ + activeItems: [], + errorCount: 0, + expirationCount: 0, + isEmpty: true, + isFull: false, + isIdle: true, + isRunning: true, + itemTimestamps: [], + items: [], + lastResult: null, + pendingTick: false, + rejectionCount: 0, + settledCount: 0, + size: 0, + status: 'idle', + successCount: 0, + }) +} + export interface AsyncQueuerOptions { /** * Default position to add items to the queuer @@ -39,6 +128,10 @@ export interface AsyncQueuerOptions { * Initial items to populate the queuer with */ initialItems?: Array + /** + * Initial state for the async queuer + */ + initialState?: Partial> /** * Maximum number of items allowed in the queuer */ @@ -53,10 +146,6 @@ export interface AsyncQueuerOptions { * Callback fired whenever an item expires in the queuer */ onExpire?: (item: TValue, queuer: AsyncQueuer) => void - /** - * Callback fired whenever the queuer's running state changes - */ - onIsRunningChange?: (queuer: AsyncQueuer) => void /** * Callback fired whenever an item is added or removed from the queuer */ @@ -93,12 +182,12 @@ export interface AsyncQueuerOptions { type AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys< Required>, + | 'initialState' | 'throwOnError' | 'onSuccess' | 'onSettled' | 'onReject' | 'onItemsChange' - | 'onIsRunningChange' | 'onExpire' | 'onError' > @@ -138,6 +227,19 @@ const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = { * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown * - The error state can be checked using the AsyncQueuer instance * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the async queuer + * - Use `onSuccess` callback to react to successful task execution and implement custom logic + * - Use `onError` callback to react to task execution errors and implement custom error handling + * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic + * - Use `onItemsChange` callback to react to items being added or removed from the queue + * - Use `onExpire` callback to react to items expiring and implement custom logic + * - Use `onReject` callback to react to items being rejected when the queue is full + * - The state includes error count, expiration count, rejection count, running status, and success/settle counts + * - State can be accessed via `asyncQueuer.store.state` when using the class directly + * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state` + * * Example usage: * ```ts * const asyncQueuer = new AsyncQueuer(async (item) => { @@ -155,148 +257,134 @@ const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = { * ``` */ export class AsyncQueuer { - private _options: AsyncQueuerOptionsWithOptionalCallbacks - private _activeItems: Set = new Set() - private _successCount = 0 - private _errorCount = 0 - private _settledCount = 0 - private _rejectionCount = 0 - private _expirationCount = 0 - private _items: Array = [] - private _itemTimestamps: Array = [] - private _pendingTick = false - private _running: boolean - private _lastResult: any + readonly store: Store>> = new Store< + AsyncQueuerState + >(getDefaultAsyncQueuerState()) + options: AsyncQueuerOptions + #timeoutIds: Set = new Set() constructor( - private fn: (value: TValue) => Promise, - initialOptions: AsyncQueuerOptions, + private fn: (item: TValue) => Promise, + initialOptions: AsyncQueuerOptions = {}, ) { - this._options = { + this.options = { ...defaultOptions, ...initialOptions, throwOnError: initialOptions.throwOnError ?? !initialOptions.onError, } - this._running = this._options.started - - for (let i = 0; i < this._options.initialItems.length; i++) { - const item = this._options.initialItems[i]! - const isLast = i === this._options.initialItems.length - 1 - this.addItem(item, this._options.addItemsTo, isLast) + const isInitiallyRunning = + this.options.initialState?.isRunning ?? this.options.started ?? true + this.#setState({ + ...this.options.initialState, + isRunning: isInitiallyRunning, + }) + + if (this.options.initialState?.items) { + if (this.store.state.isRunning) { + this.#tick() + } + } else { + for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) { + const item = this.options.initialItems![i]! + const isLast = i === (this.options.initialItems?.length ?? 0) - 1 + this.addItem(item, this.options.addItemsTo ?? 'back', isLast) + } } } /** * Updates the queuer options. New options are merged with existing options. */ - setOptions(newOptions: Partial>): void { - this._options = { ...this._options, ...newOptions } + setOptions = (newOptions: Partial>): void => { + this.options = { ...this.options, ...newOptions } } - /** - * Returns the current queuer options, including defaults and any overrides. - */ - getOptions(): AsyncQueuerOptions { - return this._options + #setState = (newState: Partial>): void => { + this.store.setState((state) => { + const combinedState = { + ...state, + ...newState, + } + + const { activeItems, items, isRunning } = combinedState + + const size = items.length + const isFull = size >= (this.options.maxSize ?? Infinity) + const isEmpty = size === 0 + const isIdle = isRunning && isEmpty && activeItems.length === 0 + + const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped' + + return { + ...combinedState, + isEmpty, + isFull, + isIdle, + size, + status, + } + }) } /** * Returns the current wait time (in milliseconds) between processing items. * If a function is provided, it is called with the queuer instance. */ - getWait(): number { - return parseFunctionOrValue(this._options.wait, this) + #getWait = (): number => { + return parseFunctionOrValue(this.options.wait ?? 0, this) } /** * Returns the current concurrency limit for processing items. * If a function is provided, it is called with the queuer instance. */ - getConcurrency(): number { - return parseFunctionOrValue(this._options.concurrency, this) + #getConcurrency = (): number => { + return parseFunctionOrValue(this.options.concurrency ?? 1, this) } /** * Processes items in the queue up to the concurrency limit. Internal use only. */ - private tick() { - if (!this._running) { - this._pendingTick = false + #tick = () => { + if (!this.store.state.isRunning) { + this.#setState({ pendingTick: false }) return } + this.#setState({ pendingTick: true }) // Check for expired items - this.checkExpiredItems() + this.#checkExpiredItems() // Process items concurrently up to the concurrency limit + const activeItems = this.store.state.activeItems while ( - this._activeItems.size < this.getConcurrency() && - !this.getIsEmpty() + activeItems.length < this.#getConcurrency() && + !this.store.state.isEmpty ) { const nextItem = this.peekNextItem() if (!nextItem) { break } - this._activeItems.add(nextItem) - this._options.onItemsChange?.(this) + activeItems.push(nextItem) + this.#setState({ + activeItems, + }) ;(async () => { - this._lastResult = await this.execute() + const result = await this.execute() + this.#setState({ lastResult: result }) - const wait = this.getWait() + const wait = this.#getWait() if (wait > 0) { - setTimeout(() => this.tick(), wait) + const timeoutId = setTimeout(() => this.#tick(), wait) + this.#timeoutIds.add(timeoutId) return } - this.tick() + this.#tick() })() } - this._pendingTick = false - } - - /** - * Starts processing items in the queue. If already running, does nothing. - */ - start(): void { - this._running = true - if (!this._pendingTick && !this.getIsEmpty()) { - this._pendingTick = true - this.tick() - } - this._options.onIsRunningChange?.(this) - } - - /** - * Stops processing items in the queue. Does not clear the queue. - */ - stop(): void { - this._running = false - this._pendingTick = false - this._options.onIsRunningChange?.(this) - } - - /** - * Removes all pending items from the queue. Does not affect active tasks. - */ - clear(): void { - this._items = [] - this._options.onItemsChange?.(this) - } - - /** - * Resets the queuer to its initial state. Optionally repopulates with initial items. - * Does not affect callbacks or options. - */ - reset(withInitialItems?: boolean): void { - this.clear() - this._successCount = 0 - this._errorCount = 0 - this._settledCount = 0 - if (withInitialItems) { - this._items = [...this._options.initialItems] - } - this._running = this._options.started + this.#setState({ pendingTick: false }) } /** @@ -309,60 +397,71 @@ export class AsyncQueuer { * queuer.addItem('task2', 'front'); * ``` */ - addItem( - item: TValue & { priority?: number }, - position: QueuePosition = this._options.addItemsTo, + addItem = ( + item: TValue, + position: QueuePosition = this.options.addItemsTo ?? 'back', runOnItemsChange: boolean = true, - ): void { - if (this.getIsFull()) { - this._rejectionCount++ - this._options.onReject?.(item, this) - return + ): boolean => { + if (this.store.state.isFull) { + this.#setState({ + rejectionCount: this.store.state.rejectionCount + 1, + }) + this.options.onReject?.(item, this) + return false } // Get priority either from the function or from getPriority option const priority = - this._options.getPriority !== defaultOptions.getPriority - ? this._options.getPriority(item) - : item.priority + this.options.getPriority !== defaultOptions.getPriority + ? this.options.getPriority!(item) + : (item as any).priority + + const items = this.store.state.items + const itemTimestamps = this.store.state.itemTimestamps if (priority !== undefined) { // Insert based on priority - higher priority items go to front - const insertIndex = this._items.findIndex((existing) => { + const insertIndex = items.findIndex((existing) => { const existingPriority = - this._options.getPriority !== defaultOptions.getPriority - ? this._options.getPriority(existing) + this.options.getPriority !== defaultOptions.getPriority + ? this.options.getPriority!(existing) : (existing as any).priority return existingPriority < priority }) if (insertIndex === -1) { - this._items.push(item) - this._itemTimestamps.push(Date.now()) + items.push(item) + itemTimestamps.push(Date.now()) } else { - this._items.splice(insertIndex, 0, item) - this._itemTimestamps.splice(insertIndex, 0, Date.now()) + items.splice(insertIndex, 0, item) + itemTimestamps.splice(insertIndex, 0, Date.now()) } } else { if (position === 'front') { // Default FIFO/LIFO behavior - this._items.unshift(item) - this._itemTimestamps.unshift(Date.now()) + items.unshift(item) + itemTimestamps.unshift(Date.now()) } else { // LIFO - this._items.push(item) - this._itemTimestamps.push(Date.now()) + items.push(item) + itemTimestamps.push(Date.now()) } } + this.#setState({ + items, + itemTimestamps, + }) + if (runOnItemsChange) { - this._options.onItemsChange?.(this) + this.options.onItemsChange?.(this) } - if (this._running && !this._pendingTick) { - this._pendingTick = true - this.tick() + if (this.store.state.isRunning && !this.store.state.pendingTick) { + this.#tick() } + + return true } /** @@ -377,21 +476,32 @@ export class AsyncQueuer { * queuer.getNextItem('back'); * ``` */ - getNextItem( - position: QueuePosition = this._options.getItemsFrom, - ): TValue | undefined { + getNextItem = ( + position: QueuePosition = this.options.getItemsFrom ?? 'front', + ): TValue | undefined => { + const { items, itemTimestamps } = this.store.state let item: TValue | undefined if (position === 'front') { - item = this._items.shift() - this._itemTimestamps.shift() + item = items[0] + if (item !== undefined) { + this.#setState({ + items: items.slice(1), + itemTimestamps: itemTimestamps.slice(1), + }) + } } else { - item = this._items.pop() - this._itemTimestamps.pop() + item = items[items.length - 1] + if (item !== undefined) { + this.#setState({ + items: items.slice(0, -1), + itemTimestamps: itemTimestamps.slice(0, -1), + }) + } } if (item !== undefined) { - this._options.onItemsChange?.(this) + this.options.onItemsChange?.(this) } return item @@ -407,55 +517,78 @@ export class AsyncQueuer { * queuer.execute('back'); * ``` */ - async execute(position?: QueuePosition): Promise { + execute = async (position?: QueuePosition): Promise => { const item = this.getNextItem(position) if (item !== undefined) { try { - this._lastResult = await this.fn(item) - this._successCount++ - this._options.onSuccess?.(this._lastResult, this) + const lastResult = await this.fn(item) + this.#setState({ + successCount: this.store.state.successCount + 1, + lastResult, + }) + this.options.onSuccess?.(lastResult, this) } catch (error) { - this._errorCount++ - this._options.onError?.(error, this) - if (this._options.throwOnError) { + this.#setState({ + errorCount: this.store.state.errorCount + 1, + }) + this.options.onError?.(error, this) + if (this.options.throwOnError) { throw error } } finally { - this._settledCount++ - this._activeItems.delete(item) - this._options.onItemsChange?.(this) - this._options.onSettled?.(this) + this.#setState({ + activeItems: this.store.state.activeItems.filter( + (activeItem) => activeItem !== item, + ), + settledCount: this.store.state.settledCount + 1, + }) + this.options.onSettled?.(this) } } return item } + /** + * Processes a specified number of items to execute immediately with no wait time + * If no numberOfItems is provided, all items will be processed + */ + flush = ( + numberOfItems: number = this.store.state.items.length, + position?: QueuePosition, + ): void => { + this.#clearTimeouts() // clear any pending timeouts + for (let i = 0; i < numberOfItems; i++) { + this.execute(position) + } + } + /** * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. * Internal use only. */ - private checkExpiredItems(): void { + #checkExpiredItems = (): void => { if ( - this._options.expirationDuration === Infinity && - this._options.getIsExpired === defaultOptions.getIsExpired - ) + (this.options.expirationDuration ?? Infinity) === Infinity && + this.options.getIsExpired === defaultOptions.getIsExpired + ) { return + } const now = Date.now() const expiredIndices: Array = [] // Find indices of expired items - for (let i = 0; i < this._items.length; i++) { - const timestamp = this._itemTimestamps[i] + for (let i = 0; i < this.store.state.size; i++) { + const timestamp = this.store.state.itemTimestamps[i] if (timestamp === undefined) continue - const item = this._items[i] + const item = this.store.state.items[i] if (item === undefined) continue const isExpired = - this._options.getIsExpired !== defaultOptions.getIsExpired - ? this._options.getIsExpired(item, timestamp) - : now - timestamp > this._options.expirationDuration + this.options.getIsExpired !== defaultOptions.getIsExpired + ? this.options.getIsExpired!(item, timestamp) + : now - timestamp > (this.options.expirationDuration ?? Infinity) if (isExpired) { expiredIndices.push(i) @@ -467,17 +600,23 @@ export class AsyncQueuer { const index = expiredIndices[i] if (index === undefined) continue - const expiredItem = this._items[index] + const expiredItem = this.store.state.items[index] if (expiredItem === undefined) continue - this._items.splice(index, 1) - this._itemTimestamps.splice(index, 1) - this._expirationCount++ - this._options.onExpire?.(expiredItem, this) + const newItems = [...this.store.state.items] + const newTimestamps = [...this.store.state.itemTimestamps] + newItems.splice(index, 1) + newTimestamps.splice(index, 1) + this.#setState({ + items: newItems, + itemTimestamps: newTimestamps, + expirationCount: this.store.state.expirationCount + 1, + }) + this.options.onExpire?.(expiredItem, this) } if (expiredIndices.length > 0) { - this._options.onItemsChange?.(this) + this.options.onItemsChange?.(this) } } @@ -490,102 +629,71 @@ export class AsyncQueuer { * queuer.peekNextItem('back'); // back * ``` */ - peekNextItem(position: QueuePosition = 'front'): TValue | undefined { + peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => { if (position === 'front') { - return this._items[0] + return this.store.state.items[0] } - return this._items[this._items.length - 1] - } - - /** - * Returns true if the queue is empty (no pending items). - */ - getIsEmpty(): boolean { - return this._items.length === 0 - } - - /** - * Returns true if the queue is full (reached maxSize). - */ - getIsFull(): boolean { - return this._items.length >= this._options.maxSize - } - - /** - * Returns the number of pending items in the queue. - */ - getSize(): number { - return this._items.length + return this.store.state.items[this.store.state.size - 1] } /** * Returns a copy of all items in the queue, including active and pending items. */ - peekAllItems(): Array { + peekAllItems = (): Array => { return [...this.peekActiveItems(), ...this.peekPendingItems()] } /** * Returns the items currently being processed (active tasks). */ - peekActiveItems(): Array { - return Array.from(this._activeItems) + peekActiveItems = (): Array => { + return [...this.store.state.activeItems] } /** * Returns the items waiting to be processed (pending tasks). */ - peekPendingItems(): Array { - return [...this._items] + peekPendingItems = (): Array => { + return [...this.store.state.items] } /** - * Returns the number of items that have been successfully processed. - */ - getSuccessCount(): number { - return this._successCount - } - - /** - * Returns the number of items that have failed processing. - */ - getErrorCount(): number { - return this._errorCount - } - - /** - * Returns the number of items that have completed processing (success or error). + * Starts processing items in the queue. If already running, does nothing. */ - getSettledCount(): number { - return this._settledCount + start = (): void => { + this.#setState({ isRunning: true }) + if (!this.store.state.pendingTick && !this.store.state.isEmpty) { + this.#tick() + } } /** - * Returns the number of items that have been rejected from being added to the queue. + * Stops processing items in the queue. Does not clear the queue. */ - getRejectionCount(): number { - return this._rejectionCount + stop = (): void => { + this.#clearTimeouts() + this.#setState({ isRunning: false, pendingTick: false }) } - /** - * Returns true if the queuer is currently running (processing items). - */ - getIsRunning(): boolean { - return this._running + #clearTimeouts = (): void => { + this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId)) + this.#timeoutIds.clear() } /** - * Returns true if the queuer is running but has no items to process and no active tasks. + * Removes all pending items from the queue. Does not affect active tasks. */ - getIsIdle(): boolean { - return this._running && this.getIsEmpty() && this._activeItems.size === 0 + clear = (): void => { + this.#setState({ items: [], itemTimestamps: [] }) + this.options.onItemsChange?.(this) } /** - * Returns the number of items that have expired and been removed from the queue. + * Resets the queuer state to its default values */ - getExpirationCount(): number { - return this._expirationCount + reset = (): void => { + this.#setState(getDefaultAsyncQueuerState()) + this.options.onItemsChange?.(this) } } @@ -600,6 +708,19 @@ export class AsyncQueuer { * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown * - The error state can be checked using the underlying AsyncQueuer instance * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the async queuer + * - Use `onSuccess` callback to react to successful task execution and implement custom logic + * - Use `onError` callback to react to task execution errors and implement custom error handling + * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic + * - Use `onItemsChange` callback to react to items being added or removed from the queue + * - Use `onExpire` callback to react to items expiring and implement custom logic + * - Use `onReject` callback to react to items being rejected when the queue is full + * - The state includes error count, expiration count, rejection count, running status, and success/settle counts + * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property + * - When using framework adapters (React/Solid), state is accessed from the hook's state property + * * Example usage: * ```ts * const enqueue = asyncQueue(async (item) => { @@ -614,5 +735,5 @@ export function asyncQueue( initialOptions: AsyncQueuerOptions, ) { const asyncQueuer = new AsyncQueuer(fn, initialOptions) - return asyncQueuer.addItem.bind(asyncQueuer) + return asyncQueuer.addItem } diff --git a/packages/pacer/src/async-rate-limiter.ts b/packages/pacer/src/async-rate-limiter.ts index 6d0d46a69..80c0f6a40 100644 --- a/packages/pacer/src/async-rate-limiter.ts +++ b/packages/pacer/src/async-rate-limiter.ts @@ -1,5 +1,51 @@ +import { Store } from '@tanstack/store' import { parseFunctionOrValue } from './utils' -import type { AnyAsyncFunction, OptionalKeys } from './types' +import type { AnyAsyncFunction } from './types' + +export interface AsyncRateLimiterState { + /** + * Number of function executions that have resulted in errors + */ + errorCount: number + /** + * Array of timestamps when executions occurred for rate limiting calculations + */ + executionTimes: Array + /** + * Whether the rate-limited function is currently executing asynchronously + */ + isExecuting: boolean + /** + * The result from the most recent successful function execution + */ + lastResult: ReturnType | undefined + /** + * Number of function executions that have been rejected due to rate limiting + */ + rejectionCount: number + /** + * Number of function executions that have completed (either successfully or with errors) + */ + settleCount: number + /** + * Number of function executions that have completed successfully + */ + successCount: number +} + +function getDefaultAsyncRateLimiterState< + TFn extends AnyAsyncFunction, +>(): AsyncRateLimiterState { + return { + errorCount: 0, + executionTimes: [], + isExecuting: false, + lastResult: undefined, + rejectionCount: 0, + settleCount: 0, + successCount: 0, + } +} /** * Options for configuring an async rate-limited function @@ -11,6 +57,10 @@ export interface AsyncRateLimiterOptions { * Defaults to true. */ enabled?: boolean | ((rateLimiter: AsyncRateLimiter) => boolean) + /** + * Initial state for the rate limiter + */ + initialState?: Partial> /** * Maximum number of executions allowed within the time window. * Can be a number or a function that returns a number. @@ -57,17 +107,15 @@ export interface AsyncRateLimiterOptions { windowType?: 'fixed' | 'sliding' } -type AsyncRateLimiterOptionsWithOptionalCallbacks = OptionalKeys< - AsyncRateLimiterOptions, - 'onError' | 'onReject' | 'onSettled' | 'onSuccess' -> - const defaultOptions: Omit< - AsyncRateLimiterOptionsWithOptionalCallbacks, - 'limit' | 'window' + Required>, + 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess' > = { enabled: true, + limit: 1, + window: 0, windowType: 'fixed', + throwOnError: true, } /** @@ -94,6 +142,18 @@ const defaultOptions: Omit< * Rate limiting is best used for hard API limits or resource constraints. For UI updates or * smoothing out frequent events, throttling or debouncing usually provide better user experience. * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the rate limiter + * - `initialState` can be a partial state object + * - Use `onSuccess` callback to react to successful function execution and implement custom logic + * - Use `onError` callback to react to function execution errors and implement custom error handling + * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic + * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded + * - The state includes execution times, success/error counts, and current execution status + * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly + * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state` + * * Error Handling: * - If an `onError` handler is provided, it will be called with the error and rate limiter instance * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown @@ -125,75 +185,71 @@ const defaultOptions: Omit< * ``` */ export class AsyncRateLimiter { - private _options: AsyncRateLimiterOptionsWithOptionalCallbacks - private _errorCount = 0 - private _executionTimes: Array = [] - private _lastResult: ReturnType | undefined - private _rejectionCount = 0 - private _settleCount = 0 - private _successCount = 0 - private _isExecuting = false + readonly store: Store>> = new Store< + AsyncRateLimiterState + >(getDefaultAsyncRateLimiterState()) + options: AsyncRateLimiterOptions constructor( private fn: TFn, initialOptions: AsyncRateLimiterOptions, ) { - this._options = { + this.options = { ...defaultOptions, ...initialOptions, throwOnError: initialOptions.throwOnError ?? !initialOptions.onError, } + this.#setState(this.options.initialState ?? {}) } /** - * Updates the rate limiter options + * Updates the async rate limiter options */ - setOptions(newOptions: Partial>): void { - this._options = { ...this._options, ...newOptions } + setOptions = (newOptions: Partial>): void => { + this.options = { ...this.options, ...newOptions } } - /** - * Returns the current rate limiter options - */ - getOptions(): AsyncRateLimiterOptions { - return this._options + #setState = (newState: Partial>): void => { + this.store.setState((state) => { + const combinedState = { + ...state, + ...newState, + } + return combinedState + }) } /** - * Returns the current enabled state of the rate limiter + * Returns the current enabled state of the async rate limiter */ - getEnabled(): boolean { - return !!parseFunctionOrValue(this._options.enabled, this) + #getEnabled = (): boolean => { + return !!parseFunctionOrValue(this.options.enabled, this) } /** * Returns the current limit of executions allowed within the time window */ - getLimit(): number { - return parseFunctionOrValue(this._options.limit, this) + #getLimit = (): number => { + return parseFunctionOrValue(this.options.limit, this) } /** * Returns the current time window in milliseconds */ - getWindow(): number { - return parseFunctionOrValue(this._options.window, this) + #getWindow = (): number => { + return parseFunctionOrValue(this.options.window, this) } /** * Attempts to execute the rate-limited function if within the configured limits. * Will reject execution if the number of calls in the current window exceeds the limit. - * If execution is allowed, waits for any previous execution to complete before proceeding. * * Error Handling: * - If the rate-limited function throws and no `onError` handler is configured, * the error will be thrown from this method. * - If an `onError` handler is configured, errors will be caught and passed to the handler, * and this method will return undefined. - * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler - * will be called if configured. * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. - * - Rate limit rejections can be tracked using `getRejectionCount()`. * * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError * @throws The error from the rate-limited function if no onError handler is configured @@ -202,93 +258,104 @@ export class AsyncRateLimiter { * ```ts * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 }); * - * // First 5 calls will execute - * await rateLimiter.maybeExecute('arg1', 'arg2'); + * // First 5 calls will return a promise that resolves with the result + * const result = await rateLimiter.maybeExecute('arg1', 'arg2'); * - * // Additional calls within the window will be rejected - * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected + * // Additional calls within the window will return undefined + * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined * ``` */ - async maybeExecute( + maybeExecute = async ( ...args: Parameters - ): Promise | undefined> { - this.cleanupOldExecutions() + ): Promise | undefined> => { + this.#cleanupOldExecutions() - const limit = this.getLimit() - const window = this.getWindow() + const relevantExecutionTimes = this.#getRelevantExecutionTimes() - if (this._options.windowType === 'sliding') { - // For sliding window, we can execute if we have capacity in the current window - if (this._executionTimes.length < limit) { - await this.execute(...args) - return this._lastResult - } - } else { - // For fixed window, we need to check if we're in a new window - const now = Date.now() - const oldestExecution = Math.min(...this._executionTimes) - const isNewWindow = oldestExecution + window <= now - - if (isNewWindow || this._executionTimes.length < limit) { - await this.execute(...args) - return this._lastResult - } + if (relevantExecutionTimes.length < this.#getLimit()) { + await this.#execute(...args) + return this.store.state.lastResult } - this.rejectFunction() + this.#setState({ + rejectionCount: this.store.state.rejectionCount + 1, + }) + this.options.onReject?.(this) return undefined } - private async execute( + #execute = async ( ...args: Parameters - ): Promise | undefined> { - if (!this.getEnabled()) return - this._isExecuting = true + ): Promise | undefined> => { + if (!this.#getEnabled()) return + const now = Date.now() - this._executionTimes.push(now) + const executionTimes = [...this.store.state.executionTimes, now] + this.#setState({ + isExecuting: true, + executionTimes, + }) try { - this._lastResult = await this.fn(...args) - this._successCount++ - this._options.onSuccess?.(this._lastResult!, this) + const result = await this.fn(...args) + this.#setState({ + successCount: this.store.state.successCount + 1, + lastResult: result, + }) + this.options.onSuccess?.(result, this) } catch (error) { - this._errorCount++ - this._options.onError?.(error, this) - if (this._options.throwOnError) { + this.#setState({ + errorCount: this.store.state.errorCount + 1, + }) + this.options.onError?.(error, this) + if (this.options.throwOnError) { throw error - } else { - console.error(error) } } finally { - this._isExecuting = false - this._settleCount++ - this._options.onSettled?.(this) + this.#setState({ + isExecuting: false, + settleCount: this.store.state.settleCount + 1, + }) + this.options.onSettled?.(this) } - return this._lastResult + return this.store.state.lastResult } - private rejectFunction(): void { - this._rejectionCount++ - if (this._options.onReject) { - this._options.onReject(this) + #getRelevantExecutionTimes = (): Array => { + if (this.options.windowType === 'sliding') { + // For sliding window, return all executions within the current window + return this.store.state.executionTimes.filter( + (time) => time > Date.now() - this.#getWindow(), + ) + } else { + // For fixed window, return all executions in the current window + // The window starts from the oldest execution time + const oldestExecution = Math.min(...this.store.state.executionTimes) + const windowStart = oldestExecution + return this.store.state.executionTimes.filter( + (time) => + time >= windowStart && time <= windowStart + this.#getWindow(), + ) } } - private cleanupOldExecutions(): void { + #cleanupOldExecutions = (): void => { const now = Date.now() - const windowStart = now - this.getWindow() - this._executionTimes = this._executionTimes.filter( - (time) => time > windowStart, - ) + const windowStart = now - this.#getWindow() + this.#setState({ + executionTimes: this.store.state.executionTimes.filter( + (time) => time > windowStart, + ), + }) } /** * Returns the number of remaining executions allowed in the current window */ - getRemainingInWindow(): number { - this.cleanupOldExecutions() - return Math.max(0, this.getLimit() - this._executionTimes.length) + getRemainingInWindow = (): number => { + const relevantExecutionTimes = this.#getRelevantExecutionTimes() + return Math.max(0, this.#getLimit() - relevantExecutionTimes.length) } /** @@ -296,58 +363,19 @@ export class AsyncRateLimiter { * For fixed windows, this is the time until the current window resets * For sliding windows, this is the time until the oldest execution expires */ - getMsUntilNextWindow(): number { + getMsUntilNextWindow = (): number => { if (this.getRemainingInWindow() > 0) { return 0 } - const oldestExecution = Math.min(...this._executionTimes) - return oldestExecution + this.getWindow() - Date.now() - } - - /** - * Returns the number of times the function has been executed - */ - getSuccessCount(): number { - return this._successCount - } - - /** - * Returns the number of times the function has been settled - */ - getSettleCount(): number { - return this._settleCount - } - - /** - * Returns the number of times the function has errored - */ - getErrorCount(): number { - return this._errorCount - } - - /** - * Returns the number of times the function has been rejected - */ - getRejectionCount(): number { - return this._rejectionCount - } - - /** - * Returns whether the function is currently executing - */ - getIsExecuting(): boolean { - return this._isExecuting + const oldestExecution = this.store.state.executionTimes[0] ?? Infinity + return oldestExecution + this.#getWindow() - Date.now() } /** * Resets the rate limiter state */ - reset(): void { - this._executionTimes = [] - this._successCount = 0 - this._errorCount = 0 - this._rejectionCount = 0 - this._settleCount = 0 + reset = (): void => { + this.#setState(getDefaultAsyncRateLimiterState()) } } @@ -369,6 +397,18 @@ export class AsyncRateLimiter { * - A throttler ensures even spacing between executions, which can be better for consistent performance * - A debouncer collapses multiple calls into one, which is better for handling bursts of events * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the rate limiter + * - `initialState` can be a partial state object + * - Use `onSuccess` callback to react to successful function execution and implement custom logic + * - Use `onError` callback to react to function execution errors and implement custom error handling + * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic + * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded + * - The state includes execution times, success/error counts, and current execution status + * - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property + * - When using framework adapters (React/Solid), state is accessed from the hook's state property + * * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically * need to enforce a hard limit on the number of executions within a time period. * @@ -409,5 +449,5 @@ export function asyncRateLimit( initialOptions: AsyncRateLimiterOptions, ) { const rateLimiter = new AsyncRateLimiter(fn, initialOptions) - return rateLimiter.maybeExecute.bind(rateLimiter) + return rateLimiter.maybeExecute } diff --git a/packages/pacer/src/async-throttler.ts b/packages/pacer/src/async-throttler.ts index 839f78fab..a387e73b4 100644 --- a/packages/pacer/src/async-throttler.ts +++ b/packages/pacer/src/async-throttler.ts @@ -1,6 +1,67 @@ +import { Store } from '@tanstack/store' import { parseFunctionOrValue } from './utils' import type { AnyAsyncFunction, OptionalKeys } from './types' +export interface AsyncThrottlerState { + /** + * Number of function executions that have resulted in errors + */ + errorCount: number + /** + * Whether the throttled function is currently executing asynchronously + */ + isExecuting: boolean + /** + * Whether the throttler is waiting for the timeout to trigger execution + */ + isPending: boolean + /** + * The arguments from the most recent call to maybeExecute + */ + lastArgs: Parameters | undefined + /** + * Timestamp of the last function execution in milliseconds + */ + lastExecutionTime: number + /** + * The result from the most recent successful function execution + */ + lastResult: ReturnType | undefined + /** + * Timestamp when the next execution can occur in milliseconds + */ + nextExecutionTime: number + /** + * Number of function executions that have completed (either successfully or with errors) + */ + settleCount: number + /** + * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed + */ + status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled' + /** + * Number of function executions that have completed successfully + */ + successCount: number +} + +function getDefaultAsyncThrottlerState< + TFn extends AnyAsyncFunction, +>(): AsyncThrottlerState { + return structuredClone({ + errorCount: 0, + isExecuting: false, + isPending: false, + lastArgs: undefined, + lastExecutionTime: 0, + lastResult: undefined, + nextExecutionTime: 0, + settleCount: 0, + status: 'idle', + successCount: 0, + }) +} + /** * Options for configuring an async throttled function */ @@ -11,6 +72,10 @@ export interface AsyncThrottlerOptions { * Defaults to true. */ enabled?: boolean | ((throttler: AsyncThrottler) => boolean) + /** + * Initial state for the async throttler + */ + initialState?: Partial> /** * Whether to execute the function immediately when called * Defaults to true @@ -54,7 +119,7 @@ export interface AsyncThrottlerOptions { type AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys< AsyncThrottlerOptions, - 'onError' | 'onSettled' | 'onSuccess' + 'initialState' | 'onError' | 'onSettled' | 'onSuccess' > const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = { @@ -85,6 +150,16 @@ const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = { * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown * - The error state can be checked using the underlying AsyncThrottler instance * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the async throttler + * - Use `onSuccess` callback to react to successful function execution and implement custom logic + * - Use `onError` callback to react to function execution errors and implement custom error handling + * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic + * - The state includes error count, execution status, last execution time, and success/settle counts + * - State can be accessed via `asyncThrottler.store.state` when using the class directly + * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state` + * * @example * ```ts * const throttler = new AsyncThrottler(async (value: string) => { @@ -103,18 +178,13 @@ const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = { * ``` */ export class AsyncThrottler { - private _options: AsyncThrottlerOptionsWithOptionalCallbacks - private _abortController: AbortController | null = null - private _errorCount = 0 - private _isExecuting = false - private _lastArgs: Parameters | undefined - private _lastExecutionTime = 0 - private _lastResult: ReturnType | undefined - private _nextExecutionTime = 0 - private _settleCount = 0 - private _successCount = 0 - private _timeoutId: NodeJS.Timeout | null = null - private _resolvePreviousPromise: + readonly store: Store>> = new Store< + AsyncThrottlerState + >(getDefaultAsyncThrottlerState()) + options: AsyncThrottlerOptions + #abortController: AbortController | null = null + #timeoutId: NodeJS.Timeout | null = null + #resolvePreviousPromise: | ((value?: ReturnType | undefined) => void) | null = null @@ -122,208 +192,222 @@ export class AsyncThrottler { private fn: TFn, initialOptions: AsyncThrottlerOptions, ) { - this._options = { + this.options = { ...defaultOptions, ...initialOptions, throwOnError: initialOptions.throwOnError ?? !initialOptions.onError, } + this.#setState(this.options.initialState ?? {}) } /** - * Updates the throttler options + * Updates the async throttler options */ - setOptions(newOptions: Partial>): void { - this._options = { ...this._options, ...newOptions } + setOptions = (newOptions: Partial>): void => { + this.options = { ...this.options, ...newOptions } - // End the pending state if the debouncer is disabled - if (!this._options.enabled) { + // End the pending state if the throttler is disabled + if (!this.#getEnabled()) { this.cancel() } } - /** - * Returns the current options - */ - getOptions(): AsyncThrottlerOptions { - return this._options + #setState = (newState: Partial>): void => { + this.store.setState((state) => { + const combinedState = { + ...state, + ...newState, + } + const { isPending, isExecuting, settleCount } = combinedState + return { + ...combinedState, + status: !this.#getEnabled() + ? 'disabled' + : isPending + ? 'pending' + : isExecuting + ? 'executing' + : settleCount > 0 + ? 'settled' + : 'idle', + } + }) } /** - * Returns the current enabled state of the throttler + * Returns the current enabled state of the async throttler */ - getEnabled(): boolean { - return !!parseFunctionOrValue(this._options.enabled, this) + #getEnabled = (): boolean => { + return !!parseFunctionOrValue(this.options.enabled, this) } /** * Returns the current wait time in milliseconds */ - getWait(): number { - return parseFunctionOrValue(this._options.wait, this) + #getWait = (): number => { + return parseFunctionOrValue(this.options.wait, this) } /** - * Attempts to execute the throttled function. - * If a call is already in progress, it may be blocked or queued depending on the `wait` option. + * Attempts to execute the throttled function. The execution behavior depends on the throttler options: + * + * - If enough time has passed since the last execution (>= wait period): + * - With leading=true: Executes immediately + * - With leading=false: Waits for the next trailing execution + * + * - If within the wait period: + * - With trailing=true: Schedules execution for end of wait period + * - With trailing=false: Drops the execution * - * Error Handling: - * - If the throttled function throws and no `onError` handler is configured, - * the error will be thrown from this method. - * - If an `onError` handler is configured, errors will be caught and passed to the handler, - * and this method will return undefined. - * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. + * @example + * ```ts + * const throttled = new AsyncThrottler(fn, { wait: 1000 }); * - * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError - * @throws The error from the throttled function if no onError handler is configured + * // First call executes immediately + * await throttled.maybeExecute('a', 'b'); + * + * // Call during wait period - gets throttled + * await throttled.maybeExecute('c', 'd'); + * ``` */ - async maybeExecute( + maybeExecute = async ( ...args: Parameters - ): Promise | undefined> { + ): Promise | undefined> => { + if (!this.#getEnabled()) return undefined const now = Date.now() - const timeSinceLastExecution = now - this._lastExecutionTime - const wait = this.getWait() + const timeSinceLastExecution = now - this.store.state.lastExecutionTime + const wait = this.#getWait() + // Store the most recent arguments for potential trailing execution + this.#setState({ lastArgs: args }) - this.resolvePreviousPromise() + this.#resolvePreviousPromiseInternal() // Handle leading execution - if (this._options.leading && timeSinceLastExecution >= wait) { - await this.execute(...args) - return this._lastResult + if (this.options.leading && timeSinceLastExecution >= wait) { + await this.#execute(...args) + return this.store.state.lastResult } else { - // Store the most recent arguments for potential trailing execution - this._lastArgs = args - return new Promise((resolve) => { - this._resolvePreviousPromise = resolve + this.#resolvePreviousPromise = resolve // Clear any existing timeout to ensure we use the latest arguments - if (this._timeoutId) { - clearTimeout(this._timeoutId) - } + this.#clearTimeout() // Set up trailing execution if enabled - if (this._options.trailing) { - const _timeSinceLastExecution = this._lastExecutionTime - ? now - this._lastExecutionTime + if (this.options.trailing) { + const _timeSinceLastExecution = this.store.state.lastExecutionTime + ? now - this.store.state.lastExecutionTime : 0 const timeoutDuration = wait - _timeSinceLastExecution - this._timeoutId = setTimeout(async () => { - if (this._lastArgs !== undefined) { - await this.execute(...this._lastArgs) + this.#setState({ isPending: true }) + this.#timeoutId = setTimeout(async () => { + if (this.store.state.lastArgs !== undefined) { + await this.#execute(...this.store.state.lastArgs) } - this._resolvePreviousPromise = null - resolve(this._lastResult) + this.#resolvePreviousPromise = null + resolve(this.store.state.lastResult) }, timeoutDuration) } }) } } - private async execute( + #execute = async ( ...args: Parameters - ): Promise | undefined> { - if (!this.getEnabled() || this._isExecuting) return undefined - this._abortController = new AbortController() + ): Promise | undefined> => { + if (!this.#getEnabled() || this.store.state.isExecuting) return undefined + this.#abortController = new AbortController() try { - this._isExecuting = true - this._lastResult = await this.fn(...args) // EXECUTE! - this._successCount++ - this._options.onSuccess?.(this._lastResult!, this) + this.#setState({ isExecuting: true }) + const result = await this.fn(...args) // EXECUTE! + this.#setState({ + lastResult: result, + successCount: this.store.state.successCount + 1, + }) + this.options.onSuccess?.(result, this) } catch (error) { - this._errorCount++ - this._options.onError?.(error, this) - if (this._options.throwOnError) { + this.#setState({ + errorCount: this.store.state.errorCount + 1, + }) + this.options.onError?.(error, this) + if (this.options.throwOnError) { throw error } else { console.error(error) } } finally { - this._isExecuting = false - this._settleCount++ - this._abortController = null - this._lastExecutionTime = Date.now() - this._nextExecutionTime = this._lastExecutionTime + this.getWait() - this._options.onSettled?.(this) - } - return this._lastResult - } - - private resolvePreviousPromise(): void { - if (this._resolvePreviousPromise) { - this._resolvePreviousPromise(this._lastResult) - this._resolvePreviousPromise = null + const lastExecutionTime = Date.now() + const nextExecutionTime = lastExecutionTime + this.#getWait() + this.#setState({ + isExecuting: false, + isPending: false, + settleCount: this.store.state.settleCount + 1, + lastExecutionTime, + nextExecutionTime, + }) + this.#abortController = null + this.options.onSettled?.(this) } + return this.store.state.lastResult } /** - * Cancels any pending execution or aborts any execution in progress + * Processes the current pending execution immediately */ - cancel(): void { - if (this._timeoutId) { - clearTimeout(this._timeoutId) - this._timeoutId = null - } - if (this._abortController) { - this._abortController.abort() - this._abortController = null + flush = (): void => { + if (this.store.state.isPending && this.store.state.lastArgs) { + this.#abortExecution() // abort any current execution + this.#clearTimeout() // clear any existing timeout + this.#execute(...this.store.state.lastArgs) } - this.resolvePreviousPromise() - this._lastArgs = undefined - } - - /** - * Returns the last execution time - */ - getLastExecutionTime(): number { - return this._lastExecutionTime } - /** - * Returns the next execution time - */ - getNextExecutionTime(): number { - return this._nextExecutionTime - } - - /** - * Returns the last result of the debounced function - */ - getLastResult(): ReturnType | undefined { - return this._lastResult + #resolvePreviousPromiseInternal = (): void => { + if (this.#resolvePreviousPromise) { + this.#resolvePreviousPromise(this.store.state.lastResult) + this.#resolvePreviousPromise = null + } } - /** - * Returns the number of times the function has been executed successfully - */ - getSuccessCount(): number { - return this._successCount + #clearTimeout = (): void => { + if (this.#timeoutId) { + clearTimeout(this.#timeoutId) + this.#timeoutId = null + } } - /** - * Returns the number of times the function has settled (completed or errored) - */ - getSettleCount(): number { - return this._settleCount + #cancelPendingExecution = (): void => { + this.#clearTimeout() + if (this.#resolvePreviousPromise) { + this.#resolvePreviousPromise(this.store.state.lastResult) + this.#resolvePreviousPromise = null + } + this.#setState({ + isPending: false, + isExecuting: false, + lastArgs: undefined, + }) } - /** - * Returns the number of times the function has errored - */ - getErrorCount(): number { - return this._errorCount + #abortExecution = (): void => { + if (this.#abortController) { + this.#abortController.abort() + this.#abortController = null + } } /** - * Returns the current pending state + * Cancels any pending execution or aborts any execution in progress */ - getIsPending(): boolean { - return this.getEnabled() && !!this._timeoutId + cancel = (): void => { + this.#cancelPendingExecution() + this.#abortExecution() } /** - * Returns the current executing state + * Resets the debouncer state to its default values */ - getIsExecuting(): boolean { - return this._isExecuting + reset = (): void => { + this.#setState(getDefaultAsyncThrottlerState()) } } @@ -343,6 +427,16 @@ export class AsyncThrottler { * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown * - The error state can be checked using the underlying AsyncThrottler instance * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the async throttler + * - Use `onSuccess` callback to react to successful function execution and implement custom logic + * - Use `onError` callback to react to function execution errors and implement custom error handling + * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic + * - The state includes error count, execution status, last execution time, and success/settle counts + * - State can be accessed via the underlying AsyncThrottler instance's `store.state` property + * - When using framework adapters (React/Solid), state is accessed from the hook's state property + * * @example * ```ts * const throttled = asyncThrottle(async (value: string) => { @@ -365,5 +459,5 @@ export function asyncThrottle( initialOptions: AsyncThrottlerOptions, ) { const asyncThrottler = new AsyncThrottler(fn, initialOptions) - return asyncThrottler.maybeExecute.bind(asyncThrottler) + return asyncThrottler.maybeExecute } diff --git a/packages/pacer/src/batcher.ts b/packages/pacer/src/batcher.ts index de3a35166..2a79c9c35 100644 --- a/packages/pacer/src/batcher.ts +++ b/packages/pacer/src/batcher.ts @@ -1,5 +1,55 @@ +import { Store } from '@tanstack/store' +import { parseFunctionOrValue } from './utils' import type { OptionalKeys } from './types' +export interface BatcherState { + /** + * Number of batch executions that have been completed + */ + executionCount: number + /** + * Whether the batcher has no items to process (items array is empty) + */ + isEmpty: boolean + /** + * Whether the batcher is waiting for the timeout to trigger batch processing + */ + isPending: boolean + /** + * Whether the batcher is active and will process items automatically + */ + isRunning: boolean + /** + * Total number of items that have been processed across all batches + */ + totalItemsProcessed: number + /** + * Array of items currently queued for batch processing + */ + items: Array + /** + * Number of items currently in the batch queue + */ + size: number + /** + * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout + */ + status: 'idle' | 'pending' +} + +function getDefaultBatcherState(): BatcherState { + return { + executionCount: 0, + isEmpty: true, + isPending: false, + isRunning: true, + totalItemsProcessed: 0, + items: [], + size: 0, + status: 'idle', + } +} + /** * Options for configuring a Batcher instance */ @@ -9,6 +59,10 @@ export interface BatcherOptions { * Return true to process the batch immediately */ getShouldExecute?: (items: Array, batcher: Batcher) => boolean + /** + * Initial state for the batcher + */ + initialState?: Partial> /** * Maximum number of items in a batch * @default Infinity @@ -18,10 +72,6 @@ export interface BatcherOptions { * Callback fired after a batch is processed */ onExecute?: (batcher: Batcher) => void - /** - * Callback fired when the batcher's running state changes - */ - onIsRunningChange?: (batcher: Batcher) => void /** * Callback fired after items are added to the batcher */ @@ -37,12 +87,12 @@ export interface BatcherOptions { * If not provided, the batch will not be triggered by a timeout. * @default Infinity */ - wait?: number + wait?: number | ((batcher: Batcher) => number) } type BatcherOptionsWithOptionalCallbacks = OptionalKeys< Required>, - 'onExecute' | 'onItemsChange' | 'onIsRunningChange' + 'initialState' | 'onExecute' | 'onItemsChange' > const defaultOptions: BatcherOptionsWithOptionalCallbacks = { @@ -63,6 +113,15 @@ const defaultOptions: BatcherOptionsWithOptionalCallbacks = { * - Custom batch processing logic via getShouldExecute * - Event callbacks for monitoring batch operations * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the batcher + * - Use `onExecute` callback to react to batch execution and implement custom logic + * - Use `onItemsChange` callback to react to items being added or removed from the batcher + * - The state includes batch execution count, total items processed, items, and running status + * - State can be accessed via `batcher.store.state` when using the class directly + * - When using framework adapters (React/Solid), state is accessed from `batcher.state` + * * @example * ```ts * const batcher = new Batcher( @@ -70,7 +129,7 @@ const defaultOptions: BatcherOptionsWithOptionalCallbacks = { * { * maxSize: 5, * wait: 2000, - * onExecuteBatch: (items) => console.log('Batch executed:', items) + * onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems()) * } * ); * @@ -78,59 +137,76 @@ const defaultOptions: BatcherOptionsWithOptionalCallbacks = { * batcher.addItem(2); * // After 2 seconds or when 5 items are added, whichever comes first, * // the batch will be processed - * // batcher.execute() // manually trigger a batch + * // batcher.flush() // manually trigger a batch * ``` */ export class Batcher { - private _options: BatcherOptionsWithOptionalCallbacks - private _batchExecutionCount = 0 - private _itemExecutionCount = 0 - private _items: Array = [] - private _running: boolean - private _timeoutId: NodeJS.Timeout | null = null + readonly store: Store>> = new Store( + getDefaultBatcherState(), + ) + options: BatcherOptionsWithOptionalCallbacks + #timeoutId: NodeJS.Timeout | null = null constructor( private fn: (items: Array) => void, initialOptions: BatcherOptions, ) { - this._options = { ...defaultOptions, ...initialOptions } - this._running = this._options.started + this.options = { + ...defaultOptions, + ...initialOptions, + } + this.#setState(this.options.initialState ?? {}) } /** * Updates the batcher options */ - setOptions(newOptions: Partial>): void { - this._options = { ...this._options, ...newOptions } + setOptions = (newOptions: Partial>): void => { + this.options = { ...this.options, ...newOptions } } - /** - * Returns the current batcher options - */ - getOptions(): BatcherOptions { - return this._options + #setState = (newState: Partial>): void => { + this.store.setState((state) => { + const combinedState = { + ...state, + ...newState, + } + const { isPending, items } = combinedState + const size = items.length + const isEmpty = size === 0 + return { + ...combinedState, + isEmpty, + size, + status: isPending ? 'pending' : 'idle', + } + }) + } + + #getWait = (): number => { + return parseFunctionOrValue(this.options.wait, this) } /** * Adds an item to the batcher * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed */ - addItem(item: TValue): void { - this._items.push(item) - this._options.onItemsChange?.(this) + addItem = (item: TValue): void => { + this.#setState({ + items: [...this.store.state.items, item], + isPending: this.options.wait !== Infinity, + }) + this.options.onItemsChange?.(this) const shouldProcess = - this._items.length >= this._options.maxSize || - this._options.getShouldExecute(this._items, this) + this.store.state.items.length >= this.options.maxSize || + this.options.getShouldExecute(this.store.state.items, this) if (shouldProcess) { - this.execute() - } else if ( - this._running && - !this._timeoutId && - this._options.wait !== Infinity - ) { - this._timeoutId = setTimeout(() => this.execute(), this._options.wait) + this.#execute() + } else if (this.store.state.isRunning && this.options.wait !== Infinity) { + this.#clearTimeout() // clear any pending timeout to replace it with a new one + this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()) } } @@ -143,89 +219,76 @@ export class Batcher { * * You can also call this method manually to process the current batch at any time. */ - execute(): void { - if (this._timeoutId) { - clearTimeout(this._timeoutId) - this._timeoutId = null - } - - if (this._items.length === 0) { + #execute = (): void => { + if (this.store.state.items.length === 0) { return } const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions) - this._items = [] // Clear items before processing to prevent race conditions - this._options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed + this.clear() // Clear items before processing to prevent race conditions + this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed - this.fn(batch) - this._batchExecutionCount++ - this._itemExecutionCount += batch.length - this._options.onExecute?.(this) + this.fn(batch) // EXECUTE + this.#setState({ + executionCount: this.store.state.executionCount + 1, + totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length, + }) + this.options.onExecute?.(this) } /** - * Stops the batcher from processing batches + * Processes the current batch of items immediately */ - stop(): void { - this._running = false - this._options.onIsRunningChange?.(this) - if (this._timeoutId) { - clearTimeout(this._timeoutId) - this._timeoutId = null - } + flush = (): void => { + this.#clearTimeout() // clear any pending timeout + this.#execute() // execute immediately } /** - * Starts the batcher and processes any pending items - */ - start(): void { - this._running = true - this._options.onIsRunningChange?.(this) - if (this._items.length > 0 && !this._timeoutId) { - this._timeoutId = setTimeout(() => this.execute(), this._options.wait) - } - } - - /** - * Returns the current number of items in the batcher + * Stops the batcher from processing batches */ - getSize(): number { - return this._items.length + stop = (): void => { + this.#setState({ isRunning: false }) + this.#clearTimeout() } /** - * Returns true if the batcher is empty + * Starts the batcher and processes any pending items */ - getIsEmpty(): boolean { - return this._items.length === 0 + start = (): void => { + this.#setState({ isRunning: true }) + if (this.store.state.items.length > 0) { + this.#execute() + } } /** - * Returns true if the batcher is running + * Returns a copy of all items in the batcher */ - getIsRunning(): boolean { - return this._running + peekAllItems = (): Array => { + return [...this.store.state.items] } - /** - * Returns a copy of all items currently in the batcher - */ - peekAllItems(): Array { - return [...this._items] + #clearTimeout = (): void => { + if (this.#timeoutId) { + clearTimeout(this.#timeoutId) + this.#timeoutId = null + } } /** - * Returns the number of times batches have been processed + * Removes all items from the batcher */ - getBatchExecutionCount(): number { - return this._batchExecutionCount + clear = (): void => { + this.#setState({ items: [], isPending: false }) } /** - * Returns the total number of individual items that have been processed + * Resets the batcher state to its default values */ - getItemExecutionCount(): number { - return this._itemExecutionCount + reset = (): void => { + this.#setState(getDefaultBatcherState()) + this.options.onItemsChange?.(this) } } @@ -234,10 +297,13 @@ export class Batcher { * * @example * ```ts - * const batchItems = batch({ - * batchSize: 3, - * processBatch: (items) => console.log('Processing:', items) - * }); + * const batchItems = batch( + * (items) => console.log('Processing:', items), + * { + * maxSize: 3, + * onExecute: (batcher) => console.log('Batch executed') + * } + * ); * * batchItems(1); * batchItems(2); @@ -249,5 +315,5 @@ export function batch( options: BatcherOptions, ) { const batcher = new Batcher(fn, options) - return batcher.addItem.bind(batcher) + return batcher.addItem } diff --git a/packages/pacer/src/debouncer.ts b/packages/pacer/src/debouncer.ts index e420a4dfd..6ad7c6b80 100644 --- a/packages/pacer/src/debouncer.ts +++ b/packages/pacer/src/debouncer.ts @@ -1,6 +1,42 @@ +import { Store } from '@tanstack/store' import { parseFunctionOrValue } from './utils' import type { AnyFunction } from './types' +export interface DebouncerState { + /** + * Whether the debouncer can execute on the leading edge of the timeout + */ + canLeadingExecute: boolean + /** + * Number of function executions that have been completed + */ + executionCount: number + /** + * Whether the debouncer is waiting for the timeout to trigger execution + */ + isPending: boolean + /** + * The arguments from the most recent call to maybeExecute + */ + lastArgs: Parameters | undefined + /** + * Current execution status - 'idle' when not active, 'pending' when waiting for timeout + */ + status: 'disabled' | 'idle' | 'pending' +} + +function getDefaultDebouncerState< + TFn extends AnyFunction, +>(): DebouncerState { + return structuredClone({ + canLeadingExecute: true, + executionCount: 0, + isPending: false, + lastArgs: undefined, + status: 'idle', + }) +} + /** * Options for configuring a debounced function */ @@ -11,6 +47,10 @@ export interface DebouncerOptions { * Defaults to true. */ enabled?: boolean | ((debouncer: Debouncer) => boolean) + /** + * Initial state for the debouncer + */ + initialState?: Partial> /** * Whether to execute on the leading edge of the timeout. * The first call will execute immediately and the rest will wait the delay. @@ -34,10 +74,12 @@ export interface DebouncerOptions { wait: number | ((debouncer: Debouncer) => number) } -const defaultOptions: Required> = { +const defaultOptions: Omit< + Required>, + 'initialState' | 'onExecute' +> = { enabled: true, leading: false, - onExecute: () => {}, trailing: true, wait: 0, } @@ -53,6 +95,14 @@ const defaultOptions: Required> = { * (leading edge) or at the end (trailing edge, default). Each new call during the wait period * will reset the timer. * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the debouncer + * - Use `onExecute` callback to react to function execution and implement custom logic + * - The state includes canLeadingExecute, execution count, and isPending status + * - State can be accessed via `debouncer.store.state` when using the class directly + * - When using framework adapters (React/Solid), state is accessed from `debouncer.state` + * * @example * ```ts * const debouncer = new Debouncer((value: string) => { @@ -66,117 +116,142 @@ const defaultOptions: Required> = { * ``` */ export class Debouncer { - private _canLeadingExecute = true - private _executionCount = 0 - private _isPending = false - private _options: Required> - private _timeoutId: NodeJS.Timeout | undefined + readonly store: Store>> = new Store( + getDefaultDebouncerState(), + ) + options: DebouncerOptions + #timeoutId: NodeJS.Timeout | undefined constructor( private fn: TFn, initialOptions: DebouncerOptions, ) { - this._options = { + this.options = { ...defaultOptions, ...initialOptions, } + this.#setState(this.options.initialState ?? {}) } /** * Updates the debouncer options */ - setOptions(newOptions: Partial>): void { - this._options = { ...this._options, ...newOptions } + setOptions = (newOptions: Partial>): void => { + this.options = { ...this.options, ...newOptions } - // End the pending state if the debouncer is disabled - if (!this._options.enabled) { - this._isPending = false + // Cancel pending execution if the debouncer is disabled + if (!this.#getEnabled()) { + this.cancel() } } - /** - * Returns the current debouncer options - */ - getOptions(): Required> { - return this._options + #setState = (newState: Partial>): void => { + this.store.setState((state) => { + const combinedState = { + ...state, + ...newState, + } + const { isPending } = combinedState + return { + ...combinedState, + status: !this.#getEnabled() + ? 'disabled' + : isPending + ? 'pending' + : 'idle', + } + }) } /** * Returns the current enabled state of the debouncer */ - getEnabled(): boolean { - return parseFunctionOrValue(this._options.enabled, this) + #getEnabled = (): boolean => { + return !!parseFunctionOrValue(this.options.enabled, this) } /** * Returns the current wait time in milliseconds */ - getWait(): number { - return parseFunctionOrValue(this._options.wait, this) + #getWait = (): number => { + return parseFunctionOrValue(this.options.wait, this) } /** * Attempts to execute the debounced function * If a call is already in progress, it will be queued */ - maybeExecute(...args: Parameters): void { + maybeExecute = (...args: Parameters): void => { + if (!this.#getEnabled()) return undefined let _didLeadingExecute = false // Handle leading execution - if (this._options.leading && this._canLeadingExecute) { - this._canLeadingExecute = false + if (this.options.leading && this.store.state.canLeadingExecute) { + this.#setState({ canLeadingExecute: false }) _didLeadingExecute = true - this.execute(...args) + this.#execute(...args) } // Start pending state to indicate that the debouncer is waiting for the trailing edge - if (this._options.trailing) { - this._isPending = true + if (this.options.trailing) { + this.#setState({ isPending: true, lastArgs: args }) } // Clear any existing timeout - if (this._timeoutId) clearTimeout(this._timeoutId) + if (this.#timeoutId) clearTimeout(this.#timeoutId) // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading - this._timeoutId = setTimeout(() => { - this._canLeadingExecute = true - if (this._options.trailing && !_didLeadingExecute) { - this.execute(...args) + this.#timeoutId = setTimeout(() => { + this.#setState({ canLeadingExecute: true }) + if (this.options.trailing && !_didLeadingExecute) { + this.#execute(...args) } - }, this.getWait()) + }, this.#getWait()) } - private execute(...args: Parameters): void { - if (!this.getEnabled()) return undefined + #execute = (...args: Parameters): void => { + if (!this.#getEnabled()) return undefined this.fn(...args) // EXECUTE! - this._isPending = false - this._executionCount++ - this._options.onExecute(this) + this.#setState({ + isPending: false, + executionCount: this.store.state.executionCount + 1, + }) + this.options.onExecute?.(this) } /** - * Cancels any pending execution + * Processes the current pending execution immediately */ - cancel(): void { - if (this._timeoutId) { - clearTimeout(this._timeoutId) - this._canLeadingExecute = true - this._isPending = false + flush = (): void => { + if (this.store.state.isPending && this.store.state.lastArgs) { + this.#clearTimeout() // clear any pending timeout + this.#execute(...this.store.state.lastArgs) // execute immediately + } + } + + #clearTimeout = (): void => { + if (this.#timeoutId) { + clearTimeout(this.#timeoutId) + this.#timeoutId = undefined } } /** - * Returns the number of times the function has been executed + * Cancels any pending execution */ - getExecutionCount(): number { - return this._executionCount + cancel = (): void => { + this.#clearTimeout() + this.#setState({ + canLeadingExecute: true, + isPending: false, + }) } /** - * Returns `true` if debouncing + * Resets the debouncer state to its default values */ - getIsPending(): boolean { - return this.getEnabled() && this._isPending + reset = (): void => { + this.#setState(getDefaultDebouncerState()) } } @@ -190,6 +265,14 @@ export class Debouncer { * If leading option is true, the function will execute immediately on the first call, then wait the delay * before allowing another execution. * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the debouncer + * - Use `onExecute` callback to react to function execution and implement custom logic + * - The state includes canLeadingExecute, execution count, and isPending status + * - State can be accessed via the underlying Debouncer instance's `store.state` property + * - When using framework adapters (React/Solid), state is accessed from the hook's state property + * * @example * ```ts * const debounced = debounce(() => { @@ -205,5 +288,5 @@ export function debounce( initialOptions: DebouncerOptions, ): (...args: Parameters) => void { const debouncer = new Debouncer(fn, initialOptions) - return debouncer.maybeExecute.bind(debouncer) + return debouncer.maybeExecute } diff --git a/packages/pacer/src/index.ts b/packages/pacer/src/index.ts index 480f0d443..194ea6f7f 100644 --- a/packages/pacer/src/index.ts +++ b/packages/pacer/src/index.ts @@ -1,9 +1,9 @@ +export * from './async-batcher' export * from './async-debouncer' export * from './async-queuer' export * from './async-rate-limiter' export * from './async-throttler' export * from './batcher' -export * from './compare' export * from './debouncer' export * from './queuer' export * from './rate-limiter' diff --git a/packages/pacer/src/queuer.ts b/packages/pacer/src/queuer.ts index e0fdb5474..445a9420a 100644 --- a/packages/pacer/src/queuer.ts +++ b/packages/pacer/src/queuer.ts @@ -1,5 +1,74 @@ +import { Store } from '@tanstack/store' import { parseFunctionOrValue } from './utils' +export interface QueuerState { + /** + * Number of items that have been processed by the queuer + */ + executionCount: number + /** + * Number of items that have been removed from the queue due to expiration + */ + expirationCount: number + /** + * Whether the queuer has no items to process (items array is empty) + */ + isEmpty: boolean + /** + * Whether the queuer has reached its maximum capacity + */ + isFull: boolean + /** + * Whether the queuer is not currently processing any items + */ + isIdle: boolean + /** + * Whether the queuer is active and will process items automatically + */ + isRunning: boolean + /** + * Timestamps when items were added to the queue for expiration tracking + */ + itemTimestamps: Array + /** + * Array of items currently waiting to be processed + */ + items: Array + /** + * Whether the queuer has a pending timeout for processing the next item + */ + pendingTick: boolean + /** + * Number of items that have been rejected from being added to the queue + */ + rejectionCount: number + /** + * Number of items currently in the queue + */ + size: number + /** + * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused + */ + status: 'idle' | 'running' | 'stopped' +} + +function getDefaultQueuerState(): QueuerState { + return { + executionCount: 0, + expirationCount: 0, + isEmpty: true, + isFull: false, + isIdle: true, + isRunning: true, + itemTimestamps: [], + items: [], + pendingTick: false, + rejectionCount: 0, + size: 0, + status: 'idle', + } +} + /** * Options for configuring a Queuer instance. * @@ -35,6 +104,10 @@ export interface QueuerOptions { * Initial items to populate the queuer with */ initialItems?: Array + /** + * Initial state for the queuer + */ + initialState?: Partial> /** * Maximum number of items allowed in the queuer */ @@ -47,10 +120,6 @@ export interface QueuerOptions { * Callback fired whenever an item is removed from the queuer */ onExecute?: (item: TValue, queuer: Queuer) => void - /** - * Callback fired whenever the queuer's running state changes - */ - onIsRunningChange?: (queuer: Queuer) => void /** * Callback fired whenever an item is added or removed from the queuer */ @@ -71,7 +140,15 @@ export interface QueuerOptions { wait?: number | ((queuer: Queuer) => number) } -const defaultOptions: Required> = { +const defaultOptions: Omit< + Required>, + | 'initialState' + | 'onExecute' + | 'onIsRunningChange' + | 'onItemsChange' + | 'onReject' + | 'onExpire' +> = { addItemsTo: 'back', getItemsFrom: 'front', getPriority: (item) => item?.priority ?? 0, @@ -79,11 +156,6 @@ const defaultOptions: Required> = { expirationDuration: Infinity, initialItems: [], maxSize: Infinity, - onExecute: () => {}, - onIsRunningChange: () => {}, - onItemsChange: () => {}, - onReject: () => {}, - onExpire: () => {}, started: true, wait: 0, } @@ -107,7 +179,7 @@ export type QueuePosition = 'front' | 'back' * - Callbacks for queue state changes, execution, rejection, and expiration * * Running behavior: - * - `start()`: Begins automatically processing items in the queue (defaults to running) + * - `start()`: Begins automatically processing items in the queue (defaults to isRunning) * - `stop()`: Pauses processing but maintains queue state * - `wait`: Configurable delay between processing items * - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state @@ -136,6 +208,17 @@ export type QueuePosition = 'front' | 'back' * - `getIsExpired`: Function to override default expiration * - `onExpire`: Callback for expired items * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the queuer + * - Use `onExecute` callback to react to item execution and implement custom logic + * - Use `onItemsChange` callback to react to items being added or removed from the queue + * - Use `onExpire` callback to react to items expiring and implement custom logic + * - Use `onReject` callback to react to items being rejected when the queue is full + * - The state includes execution count, expiration count, rejection count, and isRunning status + * - State can be accessed via `queuer.store.state` when using the class directly + * - When using framework adapters (React/Solid), state is accessed from `queuer.state` + * * Example usage: * ```ts * // Auto-processing queue with wait time @@ -158,174 +241,112 @@ export type QueuePosition = 'front' | 'back' * ``` */ export class Queuer { - private _options: Required> - private _items: Array = [] - private _itemTimestamps: Array = [] - private _executionCount = 0 - private _rejectionCount = 0 - private _expirationCount = 0 - private _onItemsChanges: Array<(item: TValue) => void> = [] - private _running: boolean - private _pendingTick = false + readonly store: Store>> = new Store( + getDefaultQueuerState(), + ) + options: QueuerOptions + #timeoutId: NodeJS.Timeout | null = null constructor( private fn: (item: TValue) => void, initialOptions: QueuerOptions = {}, ) { - this._options = { ...defaultOptions, ...initialOptions } - this._running = this._options.started - - for (let i = 0; i < this._options.initialItems.length; i++) { - const item = this._options.initialItems[i]! - const isLast = i === this._options.initialItems.length - 1 - this.addItem(item, this._options.addItemsTo, isLast) + this.options = { + ...defaultOptions, + ...initialOptions, + } + const isInitiallyRunning = + this.options.initialState?.isRunning ?? this.options.started ?? true + this.#setState({ + ...this.options.initialState, + isRunning: isInitiallyRunning, + }) + + if (this.options.initialState?.items) { + if (this.store.state.isRunning) { + this.#tick() + } + } else { + for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) { + const item = this.options.initialItems![i]! + const isLast = i === (this.options.initialItems?.length ?? 0) - 1 + this.addItem(item, this.options.addItemsTo ?? 'back', isLast) + } } } /** * Updates the queuer options. New options are merged with existing options. */ - setOptions(newOptions: Partial>): void { - this._options = { ...this._options, ...newOptions } + setOptions = (newOptions: Partial>): void => { + this.options = { ...this.options, ...newOptions } } - /** - * Returns the current queuer options, including defaults and any overrides. - */ - getOptions(): Required> { - return this._options + #setState = (newState: Partial>): void => { + this.store.setState((state) => { + const combinedState = { + ...state, + ...newState, + } + + const { items, isRunning } = combinedState + + const size = items.length + const isFull = size >= (this.options.maxSize ?? Infinity) + const isEmpty = size === 0 + const isIdle = isRunning && isEmpty + + const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped' + + return { + ...combinedState, + isEmpty, + isFull, + isIdle, + size, + status, + } + }) } /** * Returns the current wait time (in milliseconds) between processing items. * If a function is provided, it is called with the queuer instance. */ - getWait(): number { - return parseFunctionOrValue(this._options.wait, this) + #getWait = (): number => { + return parseFunctionOrValue(this.options.wait ?? 0, this) } /** * Processes items in the queue up to the wait interval. Internal use only. */ - private tick() { - if (!this._running) { - this._pendingTick = false + #tick = () => { + if (!this.store.state.isRunning) { + this.#setState({ pendingTick: false }) return } + this.#setState({ pendingTick: true }) + // Check for expired items - this.checkExpiredItems() + this.#checkExpiredItems() - while (!this.getIsEmpty()) { - const nextItem = this.execute(this._options.getItemsFrom) + while (!this.store.state.isEmpty) { + const nextItem = this.execute(this.options.getItemsFrom ?? 'front') if (nextItem === undefined) { break } - this._onItemsChanges.forEach((cb) => cb(nextItem)) - const wait = this.getWait() + const wait = this.#getWait() if (wait > 0) { // Use setTimeout to wait before processing next item - setTimeout(() => this.tick(), wait) + this.#timeoutId = setTimeout(() => this.#tick(), wait) return } - this.tick() - } - this._pendingTick = false - } - - /** - * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. - * Internal use only. - */ - private checkExpiredItems() { - if ( - this._options.expirationDuration === Infinity && - this._options.getIsExpired === defaultOptions.getIsExpired - ) - return - - const now = Date.now() - const expiredIndices: Array = [] - - // Find indices of expired items - for (let i = 0; i < this._items.length; i++) { - const timestamp = this._itemTimestamps[i] - if (timestamp === undefined) continue - - const item = this._items[i] - if (item === undefined) continue - - const isExpired = - this._options.getIsExpired !== defaultOptions.getIsExpired - ? this._options.getIsExpired(item, timestamp) - : now - timestamp > this._options.expirationDuration - - if (isExpired) { - expiredIndices.push(i) - } - } - - // Remove expired items from back to front to maintain indices - for (let i = expiredIndices.length - 1; i >= 0; i--) { - const index = expiredIndices[i] - if (index === undefined) continue - - const expiredItem = this._items[index] - if (expiredItem === undefined) continue - - this._items.splice(index, 1) - this._itemTimestamps.splice(index, 1) - this._expirationCount++ - this._options.onExpire(expiredItem, this) - } - - if (expiredIndices.length > 0) { - this._options.onItemsChange(this) - } - } - - /** - * Stops processing items in the queue. Does not clear the queue. - */ - stop() { - this._running = false - this._pendingTick = false - this._options.onIsRunningChange(this) - } - - /** - * Starts processing items in the queue. If already running, does nothing. - */ - start() { - this._running = true - if (!this._pendingTick && !this.getIsEmpty()) { - this._pendingTick = true - this.tick() + this.#tick() } - this._options.onIsRunningChange(this) - } - - /** - * Removes all pending items from the queue. Does not affect items being processed. - */ - clear(): void { - this._items = [] - this._options.onItemsChange(this) - } - - /** - * Resets the queuer to its initial state. Optionally repopulates with initial items. - * Does not affect callbacks or options. - */ - reset(withInitialItems?: boolean): void { - this.clear() - this._executionCount = 0 - if (withInitialItems) { - this._items = [...this._options.initialItems] - } - this._running = this._options.started + this.#setState({ pendingTick: false }) } /** @@ -340,49 +361,71 @@ export class Queuer { * queuer.addItem('task2', 'front'); * ``` */ - addItem( + addItem = ( item: TValue, - position: QueuePosition = this._options.addItemsTo, - runOnUpdate: boolean = true, - ): boolean { - if (this.getIsFull()) { - this._rejectionCount++ - this._options.onReject(item, this) + position: QueuePosition = this.options.addItemsTo ?? 'back', + runOnItemsChange: boolean = true, + ): boolean => { + if (this.store.state.isFull) { + this.#setState({ + rejectionCount: this.store.state.rejectionCount + 1, + }) + this.options.onReject?.(item, this) return false } - if (this._options.getPriority !== defaultOptions.getPriority) { - // If custom priority function is provided, insert based on priority - const priority = this._options.getPriority(item) - const insertIndex = this._items.findIndex( - (existing) => this._options.getPriority(existing) < priority, - ) + // Get priority either from the function or from getPriority option + const priority = + this.options.getPriority !== defaultOptions.getPriority + ? this.options.getPriority!(item) + : (item as any).priority + + const items = this.store.state.items + const itemTimestamps = this.store.state.itemTimestamps + + if (priority !== undefined) { + // Insert based on priority - higher priority items go to front + const insertIndex = items.findIndex((existing) => { + const existingPriority: number = + this.options.getPriority !== defaultOptions.getPriority + ? this.options.getPriority!(existing) + : (existing as any).priority + return existingPriority < priority + }) if (insertIndex === -1) { - this._items.push(item) - this._itemTimestamps.push(Date.now()) + items.push(item) + itemTimestamps.push(Date.now()) } else { - this._items.splice(insertIndex, 0, item) - this._itemTimestamps.splice(insertIndex, 0, Date.now()) + items.splice(insertIndex, 0, item) + itemTimestamps.splice(insertIndex, 0, Date.now()) } } else { - // Default FIFO/LIFO behavior if (position === 'front') { - this._items.unshift(item) - this._itemTimestamps.unshift(Date.now()) + // Default FIFO/LIFO behavior + items.unshift(item) + itemTimestamps.unshift(Date.now()) } else { - this._items.push(item) - this._itemTimestamps.push(Date.now()) + // LIFO + items.push(item) + itemTimestamps.push(Date.now()) } } - if (this._running && !this._pendingTick) { - this._pendingTick = true - this.tick() + this.#setState({ + items, + itemTimestamps, + }) + + if (runOnItemsChange) { + this.options.onItemsChange?.(this) } - if (runOnUpdate) { - this._options.onItemsChange(this) + + if (this.store.state.isRunning && !this.store.state.pendingTick) { + this.#setState({ pendingTick: true }) + this.#tick() } + return true } @@ -398,21 +441,32 @@ export class Queuer { * queuer.getNextItem('back'); * ``` */ - getNextItem( - position: QueuePosition = this._options.getItemsFrom, - ): TValue | undefined { + getNextItem = ( + position: QueuePosition = this.options.getItemsFrom ?? 'front', + ): TValue | undefined => { + const { items, itemTimestamps } = this.store.state let item: TValue | undefined if (position === 'front') { - item = this._items.shift() - this._itemTimestamps.shift() + item = items[0] + if (item !== undefined) { + this.#setState({ + items: items.slice(1), + itemTimestamps: itemTimestamps.slice(1), + }) + } } else { - item = this._items.pop() - this._itemTimestamps.pop() + item = items[items.length - 1] + if (item !== undefined) { + this.#setState({ + items: items.slice(0, -1), + itemTimestamps: itemTimestamps.slice(0, -1), + }) + } } if (item !== undefined) { - this._options.onItemsChange(this) + this.options.onItemsChange?.(this) } return item @@ -428,95 +482,152 @@ export class Queuer { * queuer.execute('back'); * ``` */ - execute(position?: QueuePosition): TValue | undefined { + execute = (position?: QueuePosition): TValue | undefined => { const item = this.getNextItem(position) if (item !== undefined) { this.fn(item) - this._executionCount++ - this._options.onExecute(item, this) + this.#setState({ + executionCount: this.store.state.executionCount + 1, + }) + this.options.onExecute?.(item, this) } return item } /** - * Returns the next item in the queue without removing it. - * - * Example usage: - * ```ts - * queuer.peekNextItem(); // front - * queuer.peekNextItem('back'); // back - * ``` + * Processes a specified number of items to execute immediately with no wait time + * If no numberOfItems is provided, all items will be processed */ - peekNextItem( - position: QueuePosition = this._options.getItemsFrom, - ): TValue | undefined { - if (position === 'front') { - return this._items[0] + flush = ( + numberOfItems: number = this.store.state.items.length, + position?: QueuePosition, + ): void => { + this.#clearTimeout() // clear any pending timeout + for (let i = 0; i < numberOfItems; i++) { + this.execute(position) } - return this._items[this._items.length - 1] } /** - * Returns true if the queue is empty (no pending items). + * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. + * Internal use only. */ - getIsEmpty(): boolean { - return this._items.length === 0 - } + #checkExpiredItems = (): void => { + if ( + (this.options.expirationDuration ?? Infinity) === Infinity && + this.options.getIsExpired === defaultOptions.getIsExpired + ) { + return + } - /** - * Returns true if the queue is full (reached maxSize). - */ - getIsFull(): boolean { - return this._items.length >= this._options.maxSize + const now = Date.now() + const expiredIndices: Array = [] + + // Find indices of expired items + for (let i = 0; i < this.store.state.items.length; i++) { + const timestamp = this.store.state.itemTimestamps[i] + if (timestamp === undefined) continue + + const item = this.store.state.items[i] + if (item === undefined) continue + + const isExpired = + this.options.getIsExpired !== defaultOptions.getIsExpired + ? this.options.getIsExpired!(item, timestamp) + : now - timestamp > (this.options.expirationDuration ?? Infinity) + + if (isExpired) { + expiredIndices.push(i) + } + } + + // Remove expired items from back to front to maintain indices + for (let i = expiredIndices.length - 1; i >= 0; i--) { + const index = expiredIndices[i] + if (index === undefined) continue + + const expiredItem = this.store.state.items[index] + if (expiredItem === undefined) continue + + const newItems = [...this.store.state.items] + const newTimestamps = [...this.store.state.itemTimestamps] + newItems.splice(index, 1) + newTimestamps.splice(index, 1) + this.#setState({ + items: newItems, + itemTimestamps: newTimestamps, + expirationCount: this.store.state.expirationCount + 1, + }) + this.options.onExpire?.(expiredItem, this) + } + + if (expiredIndices.length > 0) { + this.options.onItemsChange?.(this) + } } /** - * Returns the number of pending items in the queue. + * Returns the next item in the queue without removing it. + * + * Example usage: + * ```ts + * queuer.peekNextItem(); // front + * queuer.peekNextItem('back'); // back + * ``` */ - getSize(): number { - return this._items.length + peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => { + if (position === 'front') { + return this.store.state.items[0] + } + return this.store.state.items[this.store.state.size - 1] } /** * Returns a copy of all items in the queue. */ - peekAllItems(): Array { - return [...this._items] + peekAllItems = (): Array => { + return [...this.store.state.items] } /** - * Returns the number of items that have been processed and removed from the queue. + * Starts processing items in the queue. If already isRunning, does nothing. */ - getExecutionCount(): number { - return this._executionCount + start = () => { + this.#setState({ isRunning: true }) + if (!this.store.state.pendingTick && !this.store.state.isEmpty) { + this.#tick() + } } /** - * Returns the number of items that have been rejected from being added to the queue. + * Stops processing items in the queue. Does not clear the queue. */ - getRejectionCount(): number { - return this._rejectionCount + stop = () => { + this.#clearTimeout() + this.#setState({ isRunning: false, pendingTick: false }) } - /** - * Returns the number of items that have expired and been removed from the queue. - */ - getExpirationCount(): number { - return this._expirationCount + #clearTimeout = (): void => { + if (this.#timeoutId) { + clearTimeout(this.#timeoutId) + this.#timeoutId = null + } } /** - * Returns true if the queuer is currently running (processing items). + * Removes all pending items from the queue. Does not affect items being processed. */ - getIsRunning() { - return this._running + clear = (): void => { + this.#setState({ items: [], itemTimestamps: [] }) + this.options.onItemsChange?.(this) } /** - * Returns true if the queuer is running but has no items to process. + * Resets the queuer state to its default values */ - getIsIdle() { - return this._running && this.getIsEmpty() + reset = (): void => { + this.#setState(getDefaultQueuerState()) + this.options.onItemsChange?.(this) } } @@ -525,9 +636,20 @@ export class Queuer { * Items are processed sequentially in FIFO order by default. * * This is a simplified wrapper around the Queuer class that only exposes the - * `addItem` method. The queue is always running and will process items as they are added. + * `addItem` method. The queue is always isRunning and will process items as they are added. * For more control over queue processing, use the Queuer class directly. * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the queuer + * - Use `onExecute` callback to react to item execution and implement custom logic + * - Use `onItemsChange` callback to react to items being added or removed from the queue + * - Use `onExpire` callback to react to items expiring and implement custom logic + * - Use `onReject` callback to react to items being rejected when the queue is full + * - The state includes execution count, expiration count, rejection count, and isRunning status + * - State can be accessed via the underlying Queuer instance's `store.state` property + * - When using framework adapters (React/Solid), state is accessed from the hook's state property + * * Example usage: * ```ts * // Basic sequential processing @@ -548,8 +670,8 @@ export class Queuer { */ export function queue( fn: (item: TValue) => void, - options: QueuerOptions, + initialOptions: QueuerOptions, ) { - const queuer = new Queuer(fn, options) - return queuer.addItem.bind(queuer) + const queuer = new Queuer(fn, initialOptions) + return queuer.addItem } diff --git a/packages/pacer/src/rate-limiter.ts b/packages/pacer/src/rate-limiter.ts index e7d17c3e1..e0e86dfdd 100644 --- a/packages/pacer/src/rate-limiter.ts +++ b/packages/pacer/src/rate-limiter.ts @@ -1,6 +1,30 @@ +import { Store } from '@tanstack/store' import { parseFunctionOrValue } from './utils' import type { AnyFunction } from './types' +export interface RateLimiterState { + /** + * Number of function executions that have been completed + */ + executionCount: number + /** + * Array of timestamps when executions occurred for rate limiting calculations + */ + executionTimes: Array + /** + * Number of function executions that have been rejected due to rate limiting + */ + rejectionCount: number +} + +function getDefaultRateLimiterState(): RateLimiterState { + return structuredClone({ + executionCount: 0, + executionTimes: [], + rejectionCount: 0, + }) +} + /** * Options for configuring a rate-limited function */ @@ -10,6 +34,10 @@ export interface RateLimiterOptions { * Defaults to true. */ enabled?: boolean | ((rateLimiter: RateLimiter) => boolean) + /** + * Initial state for the rate limiter + */ + initialState?: Partial /** * Maximum number of executions allowed within the time window. * Can be a number or a callback function that receives the rate limiter instance and returns a number. @@ -37,11 +65,12 @@ export interface RateLimiterOptions { windowType?: 'fixed' | 'sliding' } -const defaultOptions: Required> = { +const defaultOptions: Omit< + Required>, + 'initialState' | 'onExecute' | 'onReject' +> = { enabled: true, limit: 1, - onExecute: () => {}, - onReject: () => {}, window: 0, windowType: 'fixed', } @@ -66,11 +95,24 @@ const defaultOptions: Required> = { * Rate limiting is best used for hard API limits or resource constraints. For UI updates or * smoothing out frequent events, throttling or debouncing usually provide better user experience. * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the rate limiter + * - Use `onExecute` callback to react to function execution and implement custom logic + * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded + * - The state includes execution count, execution times, and rejection count + * - State can be accessed via `rateLimiter.store.state` when using the class directly + * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state` + * * @example * ```ts * const rateLimiter = new RateLimiter( * (id: string) => api.getData(id), - * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window + * { + * limit: 5, + * window: 1000, + * windowType: 'sliding', + * } * ); * * // Will execute immediately until limit reached, then block @@ -78,54 +120,57 @@ const defaultOptions: Required> = { * ``` */ export class RateLimiter { - private _executionCount = 0 - private _rejectionCount = 0 - private _executionTimes: Array = [] - private _options: RateLimiterOptions + readonly store: Store> = + new Store(getDefaultRateLimiterState()) + options: RateLimiterOptions constructor( private fn: TFn, initialOptions: RateLimiterOptions, ) { - this._options = { + this.options = { ...defaultOptions, ...initialOptions, } + this.#setState(this.options.initialState ?? {}) } /** * Updates the rate limiter options */ - setOptions(newOptions: Partial>): void { - this._options = { ...this._options, ...newOptions } + setOptions = (newOptions: Partial>): void => { + this.options = { ...this.options, ...newOptions } } - /** - * Returns the current rate limiter options - */ - getOptions(): Required> { - return this._options as Required> + #setState = (newState: Partial): void => { + this.store.setState((state) => { + const combinedState = { + ...state, + ...newState, + } + return combinedState + }) } /** * Returns the current enabled state of the rate limiter */ - getEnabled(): boolean { - return parseFunctionOrValue(this._options.enabled, this)! + #getEnabled = (): boolean => { + return !!parseFunctionOrValue(this.options.enabled, this) } /** * Returns the current limit of executions allowed within the time window */ - getLimit(): number { - return parseFunctionOrValue(this._options.limit, this) + #getLimit = (): number => { + return parseFunctionOrValue(this.options.limit, this) } /** * Returns the current time window in milliseconds */ - getWindow(): number { - return parseFunctionOrValue(this._options.window, this) + #getWindow = (): number => { + return parseFunctionOrValue(this.options.window, this) } /** @@ -143,95 +188,86 @@ export class RateLimiter { * rateLimiter.maybeExecute('arg1', 'arg2'); // false * ``` */ - maybeExecute(...args: Parameters): boolean { - this.cleanupOldExecutions() + maybeExecute = (...args: Parameters): boolean => { + this.#cleanupOldExecutions() - if (this._options.windowType === 'sliding') { - // For sliding window, we can execute if we have capacity in the current window - if (this._executionTimes.length < this.getLimit()) { - this.execute(...args) - return true - } - } else { - // For fixed window, we need to check if we're in a new window - const now = Date.now() - const oldestExecution = Math.min(...this._executionTimes) - const isNewWindow = oldestExecution + this.getWindow() <= now + const relevantExecutionTimes = this.#getRelevantExecutionTimes() - if (isNewWindow || this._executionTimes.length < this.getLimit()) { - this.execute(...args) - return true - } + if (relevantExecutionTimes.length < this.#getLimit()) { + this.#execute(...args) + return true } - this.rejectFunction() + this.#setState({ + rejectionCount: this.store.state.rejectionCount + 1, + }) + this.options.onReject?.(this) return false } - private execute(...args: Parameters): void { - if (!this.getEnabled()) return + #execute = (...args: Parameters): void => { + if (!this.#getEnabled()) return const now = Date.now() - this._executionCount++ - this._executionTimes.push(now) - this.fn(...args) // execute the function - this._options.onExecute?.(this) + this.fn(...args) // EXECUTE! + this.store.state.executionTimes.push(now) // mutate state directly for performance + this.#setState({ + executionCount: this.store.state.executionCount + 1, + }) + this.options.onExecute?.(this) } - private rejectFunction(): void { - this._rejectionCount++ - if (this._options.onReject) { - this._options.onReject(this) + #getRelevantExecutionTimes = (): Array => { + if (this.options.windowType === 'sliding') { + // For sliding window, return all executions within the current window + return this.store.state.executionTimes.filter( + (time) => time > Date.now() - this.#getWindow(), + ) + } else { + // For fixed window, return all executions in the current window + // The window starts from the oldest execution time + const oldestExecution = Math.min(...this.store.state.executionTimes) + const windowStart = oldestExecution + return this.store.state.executionTimes.filter( + (time) => + time >= windowStart && time <= windowStart + this.#getWindow(), + ) } } - private cleanupOldExecutions(): void { + #cleanupOldExecutions = (): void => { const now = Date.now() - const windowStart = now - this.getWindow() - this._executionTimes = this._executionTimes.filter( - (time) => time > windowStart, - ) - } - - /** - * Returns the number of times the function has been executed - */ - getExecutionCount(): number { - return this._executionCount - } - - /** - * Returns the number of times the function has been rejected - */ - getRejectionCount(): number { - return this._rejectionCount + const windowStart = now - this.#getWindow() + this.#setState({ + executionTimes: this.store.state.executionTimes.filter( + (time) => time > windowStart, + ), + }) } /** * Returns the number of remaining executions allowed in the current window */ - getRemainingInWindow(): number { - this.cleanupOldExecutions() - return Math.max(0, this.getLimit() - this._executionTimes.length) + getRemainingInWindow = (): number => { + const relevantExecutionTimes = this.#getRelevantExecutionTimes() + return Math.max(0, this.#getLimit() - relevantExecutionTimes.length) } /** * Returns the number of milliseconds until the next execution will be possible */ - getMsUntilNextWindow(): number { + getMsUntilNextWindow = (): number => { if (this.getRemainingInWindow() > 0) { return 0 } - const oldestExecution = Math.min(...this._executionTimes) - return oldestExecution + this.getWindow() - Date.now() + const oldestExecution = this.store.state.executionTimes[0] ?? Infinity + return oldestExecution + this.#getWindow() - Date.now() } /** * Resets the rate limiter state */ - reset(): void { - this._executionTimes = [] - this._executionCount = 0 - this._rejectionCount = 0 + reset = (): void => { + this.#setState(getDefaultRateLimiterState()) } } @@ -249,6 +285,15 @@ export class RateLimiter { * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more * consistent rate of execution over time. * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the rate limiter + * - Use `onExecute` callback to react to function execution and implement custom logic + * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded + * - The state includes execution count, execution times, and rejection count + * - State can be accessed via the underlying RateLimiter instance's `store.state` property + * - When using framework adapters (React/Solid), state is accessed from the hook's state property + * * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically * need to enforce a hard limit on the number of executions within a time period. * @@ -277,5 +322,5 @@ export function rateLimit( initialOptions: RateLimiterOptions, ) { const rateLimiter = new RateLimiter(fn, initialOptions) - return rateLimiter.maybeExecute.bind(rateLimiter) + return rateLimiter.maybeExecute } diff --git a/packages/pacer/src/throttler.ts b/packages/pacer/src/throttler.ts index bc4552b88..2cd3d233e 100644 --- a/packages/pacer/src/throttler.ts +++ b/packages/pacer/src/throttler.ts @@ -1,6 +1,47 @@ +import { Store } from '@tanstack/store' import { parseFunctionOrValue } from './utils' import type { AnyFunction } from './types' +export interface ThrottlerState { + /** + * Number of function executions that have been completed + */ + executionCount: number + /** + * The arguments from the most recent call to maybeExecute + */ + lastArgs: Parameters | undefined + /** + * Timestamp of the last function execution in milliseconds + */ + lastExecutionTime: number + /** + * Timestamp when the next execution can occur in milliseconds + */ + nextExecutionTime: number + /** + * Whether the throttler is waiting for the timeout to trigger execution + */ + isPending: boolean + /** + * Current execution status - 'idle' when not active, 'pending' when waiting for timeout + */ + status: 'disabled' | 'idle' | 'pending' +} + +function getDefaultThrottlerState< + TFn extends AnyFunction, +>(): ThrottlerState { + return structuredClone({ + executionCount: 0, + isPending: false, + lastArgs: undefined, + lastExecutionTime: 0, + nextExecutionTime: 0, + status: 'idle', + }) +} + /** * Options for configuring a throttled function */ @@ -11,6 +52,10 @@ export interface ThrottlerOptions { * Defaults to true. */ enabled?: boolean | ((throttler: Throttler) => boolean) + /** + * Initial state for the throttler + */ + initialState?: Partial> /** * Whether to execute on the leading edge of the timeout. * Defaults to true. @@ -33,10 +78,12 @@ export interface ThrottlerOptions { wait: number | ((throttler: Throttler) => number) } -const defaultOptions: Required> = { +const defaultOptions: Omit< + Required>, + 'initialState' | 'onExecute' +> = { enabled: true, leading: true, - onExecute: () => {}, trailing: true, wait: 0, } @@ -54,6 +101,14 @@ const defaultOptions: Required> = { * * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer. * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the throttler + * - Use `onExecute` callback to react to function execution and implement custom logic + * - The state includes execution count, last execution time, pending status, and more + * - State can be accessed via `throttler.store.state` when using the class directly + * - When using framework adapters (React/Solid), state is accessed from `throttler.state` + * * @example * ```ts * const throttler = new Throttler( @@ -69,53 +124,59 @@ const defaultOptions: Required> = { * ``` */ export class Throttler { - private _executionCount = 0 - private _lastArgs: Parameters | undefined - private _lastExecutionTime = 0 - private _options: Required> - private _timeoutId: NodeJS.Timeout | undefined + readonly store: Store>> = new Store( + getDefaultThrottlerState(), + ) + options: ThrottlerOptions + #timeoutId: NodeJS.Timeout | undefined constructor( private fn: TFn, initialOptions: ThrottlerOptions, ) { - this._options = { + this.options = { ...defaultOptions, ...initialOptions, } + this.#setState(this.options.initialState ?? {}) } /** * Updates the throttler options */ - setOptions(newOptions: Partial>): void { - this._options = { ...this._options, ...newOptions } + setOptions = (newOptions: Partial>): void => { + this.options = { ...this.options, ...newOptions } - // End the pending state if the debouncer is disabled - if (!this._options.enabled) { + // Cancel pending execution if the throttler is disabled + if (!this.#getEnabled()) { this.cancel() } } - /** - * Returns the current throttler options - */ - getOptions(): Required> { - return this._options + #setState = (newState: Partial>): void => { + this.store.setState((state) => { + const combinedState = { + ...state, + ...newState, + } + const { isPending } = combinedState + return { + ...combinedState, + status: !this.#getEnabled() + ? 'disabled' + : isPending + ? 'pending' + : 'idle', + } + }) } - /** - * Returns the current enabled state of the throttler - */ - getEnabled(): boolean { - return parseFunctionOrValue(this._options.enabled, this) + #getEnabled = (): boolean => { + return !!parseFunctionOrValue(this.options.enabled, this) } - /** - * Returns the current wait time in milliseconds - */ - getWait(): number { - return parseFunctionOrValue(this._options.wait, this) + #getWait = (): number => { + return parseFunctionOrValue(this.options.wait, this) } /** @@ -140,86 +201,91 @@ export class Throttler { * throttled.maybeExecute('c', 'd'); * ``` */ - maybeExecute(...args: Parameters): void { + maybeExecute = (...args: Parameters): void => { const now = Date.now() - const timeSinceLastExecution = now - this._lastExecutionTime - const wait = this.getWait() + const timeSinceLastExecution = now - this.store.state.lastExecutionTime + const wait = this.#getWait() // Handle leading execution - if (this._options.leading && timeSinceLastExecution >= wait) { - this.execute(...args) + if (this.options.leading && timeSinceLastExecution >= wait) { + this.#execute(...args) } else { // Store the most recent arguments for potential trailing execution - this._lastArgs = args - + this.#setState({ + lastArgs: args, + }) // Set up trailing execution if not already scheduled - if (!this._timeoutId && this._options.trailing) { - const _timeSinceLastExecution = this._lastExecutionTime - ? now - this._lastExecutionTime + if (!this.#timeoutId && this.options.trailing) { + // prevent large number if lastExecutionTime is undefined + const _timeSinceLastExecution = this.store.state.lastExecutionTime + ? now - this.store.state.lastExecutionTime : 0 const timeoutDuration = wait - _timeSinceLastExecution - this._timeoutId = setTimeout(() => { - if (this._lastArgs !== undefined) { - this.execute(...this._lastArgs) + this.#setState({ isPending: true }) + this.#timeoutId = setTimeout(() => { + const { lastArgs } = this.store.state + if (lastArgs !== undefined) { + this.#execute(...lastArgs) } }, timeoutDuration) } } } - private execute(...args: Parameters): void { - if (!this.getEnabled()) return + #execute = (...args: Parameters): void => { + if (!this.#getEnabled()) return this.fn(...args) // EXECUTE! - this._executionCount++ - this._lastExecutionTime = Date.now() - this._timeoutId = undefined - this._lastArgs = undefined - this._options.onExecute(this) + const lastExecutionTime = Date.now() + const nextExecutionTime = lastExecutionTime + this.#getWait() + this.#clearTimeout() + this.#setState({ + executionCount: this.store.state.executionCount + 1, + lastExecutionTime, + nextExecutionTime, + isPending: false, + lastArgs: undefined, + }) + this.options.onExecute?.(this) } /** - * Cancels any pending trailing execution and clears internal state. - * - * If a trailing execution is scheduled (due to throttling with trailing=true), - * this will prevent that execution from occurring. The internal timeout and - * stored arguments will be cleared. - * - * Has no effect if there is no pending execution. + * Processes the current pending execution immediately */ - cancel(): void { - if (this._timeoutId) { - clearTimeout(this._timeoutId) - this._timeoutId = undefined - this._lastArgs = undefined + flush = (): void => { + if (this.store.state.isPending && this.store.state.lastArgs) { + this.#execute(...this.store.state.lastArgs) } } - /** - * Returns the last execution time - */ - getLastExecutionTime(): number { - return this._lastExecutionTime - } - - /** - * Returns the next execution time - */ - getNextExecutionTime(): number { - return this._lastExecutionTime + this.getWait() + #clearTimeout = (): void => { + if (this.#timeoutId) { + clearTimeout(this.#timeoutId) + this.#timeoutId = undefined + } } /** - * Returns the number of times the function has been executed + * Cancels any pending trailing execution and clears internal state. + * + * If a trailing execution is scheduled (due to throttling with trailing=true), + * this will prevent that execution from occurring. The internal timeout and + * stored arguments will be cleared. + * + * Has no effect if there is no pending execution. */ - getExecutionCount(): number { - return this._executionCount + cancel = (): void => { + this.#clearTimeout() + this.#setState({ + lastArgs: undefined, + isPending: false, + }) } /** - * Returns `true` if there is a pending execution + * Resets the throttler state to its default values */ - getIsPending(): boolean { - return this.getEnabled() && !!this._timeoutId + reset = (): void => { + this.#setState(getDefaultThrottlerState()) } } @@ -236,6 +302,14 @@ export class Throttler { * For handling bursts of events, consider using debounce() instead. For hard execution * limits, consider using rateLimit(). * + * State Management: + * - Uses TanStack Store for reactive state management + * - Use `initialState` to provide initial state values when creating the throttler + * - Use `onExecute` callback to react to function execution and implement custom logic + * - The state includes execution count, last execution time, pending status, and more + * - State can be accessed via the underlying Throttler instance's `store.state` property + * - When using framework adapters (React/Solid), state is accessed from the hook's state property + * * @example * ```ts * // Basic throttling - max once per second @@ -254,5 +328,5 @@ export function throttle( initialOptions: ThrottlerOptions, ) { const throttler = new Throttler(fn, initialOptions) - return throttler.maybeExecute.bind(throttler) + return throttler.maybeExecute } diff --git a/packages/pacer/src/utils.ts b/packages/pacer/src/utils.ts index 9026e0570..3a510d8ec 100644 --- a/packages/pacer/src/utils.ts +++ b/packages/pacer/src/utils.ts @@ -10,18 +10,3 @@ export function parseFunctionOrValue>( ): T { return isFunction(value) ? value(...args) : value } - -export function bindInstanceMethods>( - instance: T, -): T { - return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce( - (acc: any, key) => { - const method = instance[key as keyof T] - if (isFunction(method)) { - acc[key] = method.bind(instance) - } - return acc - }, - instance, - ) -} diff --git a/packages/pacer/tests/async-debouncer.test.ts b/packages/pacer/tests/async-debouncer.test.ts index 934753fb1..39cd890a5 100644 --- a/packages/pacer/tests/async-debouncer.test.ts +++ b/packages/pacer/tests/async-debouncer.test.ts @@ -345,9 +345,9 @@ describe('AsyncDebouncer', () => { expect(onError).toBeCalledWith(error, debouncer) expect(onSettled).toBeCalledWith(debouncer) - expect(debouncer.getErrorCount()).toBe(1) - expect(debouncer.getSettleCount()).toBe(1) - expect(debouncer.getSuccessCount()).toBe(0) + expect(debouncer.store.state.errorCount).toBe(1) + expect(debouncer.store.state.settleCount).toBe(1) + expect(debouncer.store.state.successCount).toBe(0) }) it('should not break debouncing chain on error', async () => { @@ -367,9 +367,9 @@ describe('AsyncDebouncer', () => { vi.advanceTimersByTime(1000) await promise1 expect(onError).toBeCalledWith(error, debouncer) - expect(debouncer.getErrorCount()).toBe(1) - expect(debouncer.getSettleCount()).toBe(1) - expect(debouncer.getSuccessCount()).toBe(0) + expect(debouncer.store.state.errorCount).toBe(1) + expect(debouncer.store.state.settleCount).toBe(1) + expect(debouncer.store.state.successCount).toBe(0) // Second call - should succeed const promise2 = debouncer.maybeExecute() @@ -377,9 +377,9 @@ describe('AsyncDebouncer', () => { const result = await promise2 expect(result).toBe('success') expect(mockFn).toBeCalledTimes(2) - expect(debouncer.getErrorCount()).toBe(1) - expect(debouncer.getSettleCount()).toBe(2) - expect(debouncer.getSuccessCount()).toBe(1) + expect(debouncer.store.state.errorCount).toBe(1) + expect(debouncer.store.state.settleCount).toBe(2) + expect(debouncer.store.state.successCount).toBe(1) }) it('should handle multiple errors in sequence', async () => { @@ -400,18 +400,18 @@ describe('AsyncDebouncer', () => { vi.advanceTimersByTime(1000) await promise1 expect(onError).toHaveBeenNthCalledWith(1, error1, debouncer) - expect(debouncer.getErrorCount()).toBe(1) - expect(debouncer.getSettleCount()).toBe(1) - expect(debouncer.getSuccessCount()).toBe(0) + expect(debouncer.store.state.errorCount).toBe(1) + expect(debouncer.store.state.settleCount).toBe(1) + expect(debouncer.store.state.successCount).toBe(0) // Second call - should error again const promise2 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise2 expect(onError).toHaveBeenNthCalledWith(2, error2, debouncer) - expect(debouncer.getErrorCount()).toBe(2) - expect(debouncer.getSettleCount()).toBe(2) - expect(debouncer.getSuccessCount()).toBe(0) + expect(debouncer.store.state.errorCount).toBe(2) + expect(debouncer.store.state.settleCount).toBe(2) + expect(debouncer.store.state.successCount).toBe(0) }) it('should handle errors during leading execution', async () => { @@ -431,9 +431,9 @@ describe('AsyncDebouncer', () => { await promise expect(onError).toBeCalledWith(error, debouncer) expect(onSettled).toBeCalledWith(debouncer) - expect(debouncer.getErrorCount()).toBe(1) - expect(debouncer.getSettleCount()).toBe(1) - expect(debouncer.getSuccessCount()).toBe(0) + expect(debouncer.store.state.errorCount).toBe(1) + expect(debouncer.store.state.settleCount).toBe(1) + expect(debouncer.store.state.successCount).toBe(0) }) it('should handle errors during trailing execution', async () => { @@ -453,9 +453,9 @@ describe('AsyncDebouncer', () => { await promise expect(onError).toBeCalledWith(error, debouncer) expect(onSettled).toBeCalledWith(debouncer) - expect(debouncer.getErrorCount()).toBe(1) - expect(debouncer.getSettleCount()).toBe(1) - expect(debouncer.getSuccessCount()).toBe(0) + expect(debouncer.store.state.errorCount).toBe(1) + expect(debouncer.store.state.settleCount).toBe(1) + expect(debouncer.store.state.successCount).toBe(0) }) it('should maintain state after error', async () => { @@ -473,10 +473,10 @@ describe('AsyncDebouncer', () => { await promise1 // Verify state is maintained - expect(debouncer.getErrorCount()).toBe(1) - expect(debouncer.getSettleCount()).toBe(1) - expect(debouncer.getSuccessCount()).toBe(0) - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.errorCount).toBe(1) + expect(debouncer.store.state.settleCount).toBe(1) + expect(debouncer.store.state.successCount).toBe(0) + expect(debouncer.store.state.isPending).toBe(false) }) }) @@ -591,11 +591,11 @@ describe('AsyncDebouncer', () => { // Start execution debouncer.maybeExecute() - expect(debouncer.getIsPending()).toBe(true) + expect(debouncer.store.state.isPending).toBe(true) // Cancel before wait period ends debouncer.cancel() - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) // Advance time and verify no execution vi.advanceTimersByTime(1000) @@ -616,7 +616,7 @@ describe('AsyncDebouncer', () => { // Cancel and verify canLeadingExecute is reset debouncer.cancel() - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) // Next call should execute immediately again const promise2 = debouncer.maybeExecute('second') @@ -640,7 +640,7 @@ describe('AsyncDebouncer', () => { // Cancel during leading execution debouncer.cancel() - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) // Next call should execute immediately again const promise2 = debouncer.maybeExecute('second') @@ -660,7 +660,7 @@ describe('AsyncDebouncer', () => { vi.advanceTimersByTime(1000) await promise - expect(debouncer.getLastResult()).toBe('result') + expect(debouncer.store.state.lastResult).toBe('result') }) it('should return last result when execution is pending', async () => { @@ -671,11 +671,11 @@ describe('AsyncDebouncer', () => { const promise1 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise1 - expect(debouncer.getLastResult()).toBe('result') + expect(debouncer.store.state.lastResult).toBe('result') // Second execution - should still return last result while pending const promise2 = debouncer.maybeExecute() - expect(debouncer.getLastResult()).toBe('result') + expect(debouncer.store.state.lastResult).toBe('result') vi.advanceTimersByTime(1000) await promise2 }) @@ -688,18 +688,18 @@ describe('AsyncDebouncer', () => { const promise1 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise1 - expect(debouncer.getLastResult()).toBe('result') + expect(debouncer.store.state.lastResult).toBe('result') // Second execution with cancellation debouncer.maybeExecute() debouncer.cancel() - expect(debouncer.getLastResult()).toBe('result') // Should still have last result + expect(debouncer.store.state.lastResult).toBe('result') // Should still have last result // Third execution const promise3 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise3 - expect(debouncer.getLastResult()).toBe('result') + expect(debouncer.store.state.lastResult).toBe('result') }) it('should maintain last result across multiple executions', async () => { @@ -714,19 +714,19 @@ describe('AsyncDebouncer', () => { const promise1 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise1 - expect(debouncer.getLastResult()).toBe('first') + expect(debouncer.store.state.lastResult).toBe('first') // Second execution const promise2 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise2 - expect(debouncer.getLastResult()).toBe('second') + expect(debouncer.store.state.lastResult).toBe('second') // Third execution const promise3 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise3 - expect(debouncer.getLastResult()).toBe('third') + expect(debouncer.store.state.lastResult).toBe('third') }) it('should handle undefined/null results', async () => { @@ -740,13 +740,13 @@ describe('AsyncDebouncer', () => { const promise1 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise1 - expect(debouncer.getLastResult()).toBeUndefined() + expect(debouncer.store.state.lastResult).toBeUndefined() // Test null result const promise2 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise2 - expect(debouncer.getLastResult()).toBeNull() + expect(debouncer.store.state.lastResult).toBeNull() }) it('should maintain last result after error', async () => { @@ -763,13 +763,13 @@ describe('AsyncDebouncer', () => { const promise1 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise1 - expect(debouncer.getLastResult()).toBe('success') + expect(debouncer.store.state.lastResult).toBe('success') // Second execution - error const promise2 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise2 - expect(debouncer.getLastResult()).toBe('success') // Should maintain last successful result + expect(debouncer.store.state.lastResult).toBe('success') // Should maintain last successful result }) }) @@ -783,8 +783,8 @@ describe('AsyncDebouncer', () => { // Try to execute while disabled const promise = debouncer.maybeExecute() - expect(debouncer.getIsPending()).toBe(false) - expect(debouncer.getIsExecuting()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) + expect(debouncer.store.state.isExecuting).toBe(false) // Advance time and verify no execution vi.advanceTimersByTime(1000) @@ -802,8 +802,8 @@ describe('AsyncDebouncer', () => { // Try to execute while disabled const promise = debouncer.maybeExecute() - expect(debouncer.getIsPending()).toBe(false) - expect(debouncer.getIsExecuting()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) + expect(debouncer.store.state.isExecuting).toBe(false) // Advance time and verify no execution vi.advanceTimersByTime(1000) @@ -817,7 +817,7 @@ describe('AsyncDebouncer', () => { // Should execute by default const promise = debouncer.maybeExecute() - expect(debouncer.getIsPending()).toBe(true) + expect(debouncer.store.state.isPending).toBe(true) // Advance time and verify execution vi.advanceTimersByTime(1000) @@ -831,11 +831,11 @@ describe('AsyncDebouncer', () => { // Start execution const promise = debouncer.maybeExecute() - expect(debouncer.getIsPending()).toBe(true) + expect(debouncer.store.state.isPending).toBe(true) // Disable during wait debouncer.setOptions({ enabled: false }) - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) // Advance time and verify no execution vi.advanceTimersByTime(1000) @@ -858,7 +858,7 @@ describe('AsyncDebouncer', () => { const promise = debouncer.maybeExecute() // Should only have one pending execution - expect(debouncer.getIsPending()).toBe(true) + expect(debouncer.store.state.isPending).toBe(true) // Advance time and verify single execution vi.advanceTimersByTime(1000) @@ -874,17 +874,17 @@ describe('AsyncDebouncer', () => { const promise1 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise1 - expect(debouncer.getLastResult()).toBe('result') + expect(debouncer.store.state.lastResult).toBe('result') // Disable and verify state is maintained debouncer.setOptions({ enabled: false }) - expect(debouncer.getLastResult()).toBe('result') - expect(debouncer.getIsPending()).toBe(false) - expect(debouncer.getIsExecuting()).toBe(false) + expect(debouncer.store.state.lastResult).toBe('result') + expect(debouncer.store.state.isPending).toBe(false) + expect(debouncer.store.state.isExecuting).toBe(false) // Re-enable and verify state is still maintained debouncer.setOptions({ enabled: true }) - expect(debouncer.getLastResult()).toBe('result') + expect(debouncer.store.state.lastResult).toBe('result') }) }) @@ -924,7 +924,7 @@ describe('AsyncDebouncer', () => { // Start execution const promise = debouncer.maybeExecute() - expect(debouncer.getIsPending()).toBe(true) + expect(debouncer.store.state.isPending).toBe(true) // Change options during wait debouncer.setOptions({ onSuccess }) @@ -944,22 +944,22 @@ describe('AsyncDebouncer', () => { const promise1 = debouncer.maybeExecute() vi.advanceTimersByTime(1000) await promise1 - expect(debouncer.getLastResult()).toBe('result') - expect(debouncer.getSuccessCount()).toBe(1) + expect(debouncer.store.state.lastResult).toBe('result') + expect(debouncer.store.state.successCount).toBe(1) // Change options debouncer.setOptions({ wait: 500, leading: true }) // Verify state is maintained - expect(debouncer.getLastResult()).toBe('result') - expect(debouncer.getSuccessCount()).toBe(1) + expect(debouncer.store.state.lastResult).toBe('result') + expect(debouncer.store.state.successCount).toBe(1) // Second execution with new options const promise2 = debouncer.maybeExecute() expect(mockFn).toBeCalledTimes(2) // Leading execution vi.advanceTimersByTime(500) await promise2 - expect(debouncer.getSuccessCount()).toBe(2) + expect(debouncer.store.state.successCount).toBe(2) }) it('should handle callback option changes', async () => { @@ -1018,7 +1018,7 @@ describe('AsyncDebouncer', () => { // Start execution const promise = debouncer.maybeExecute() - expect(debouncer.getIsPending()).toBe(true) + expect(debouncer.store.state.isPending).toBe(true) // Change callbacks during wait debouncer.setOptions({ diff --git a/packages/pacer/tests/async-queuer.test.ts b/packages/pacer/tests/async-queuer.test.ts index a950194d1..572558d0d 100644 --- a/packages/pacer/tests/async-queuer.test.ts +++ b/packages/pacer/tests/async-queuer.test.ts @@ -11,17 +11,17 @@ describe('AsyncQueuer', () => { it('should create an empty async queuer and be started by default', () => { const asyncQueuer = new AsyncQueuer((item) => Promise.resolve(item), {}) - expect(asyncQueuer.getIsRunning()).toBe(true) - expect(asyncQueuer.getIsIdle()).toBe(true) - expect(asyncQueuer.getSize()).toBe(0) + expect(asyncQueuer.store.state.isRunning).toBe(true) + expect(asyncQueuer.store.state.isIdle).toBe(true) + expect(asyncQueuer.store.state.items.length).toBe(0) }) it('should respect started option', () => { const asyncQueuer = new AsyncQueuer((item) => Promise.resolve(item), { started: false, }) - expect(asyncQueuer.getIsRunning()).toBe(false) - expect(asyncQueuer.getIsIdle()).toBe(false) + expect(asyncQueuer.store.state.isRunning).toBe(false) + expect(asyncQueuer.store.state.isIdle).toBe(false) }) it('should respect maxSize option', () => { @@ -32,10 +32,10 @@ describe('AsyncQueuer', () => { started: false, }, ) - expect(asyncQueuer.getSize()).toBe(0) + expect(asyncQueuer.store.state.items.length).toBe(0) asyncQueuer.addItem('test') - expect(asyncQueuer.getSize()).toBe(1) - expect(asyncQueuer.getIsFull()).toBe(true) + expect(asyncQueuer.store.state.items.length).toBe(1) + expect(asyncQueuer.store.state.isFull).toBe(true) }) describe('addItem', () => { @@ -47,7 +47,7 @@ describe('AsyncQueuer', () => { }, ) asyncQueuer.addItem('test') - expect(asyncQueuer.getSize()).toBe(1) + expect(asyncQueuer.store.state.items.length).toBe(1) }) it('should reject items when full and call onReject', () => { const onReject = vi.fn() @@ -60,11 +60,11 @@ describe('AsyncQueuer', () => { }, ) asyncQueuer.addItem('test') - expect(asyncQueuer.getSize()).toBe(1) - expect(asyncQueuer.getIsFull()).toBe(true) + expect(asyncQueuer.store.state.items.length).toBe(1) + expect(asyncQueuer.store.state.isFull).toBe(true) asyncQueuer.addItem('test2') expect(onReject).toHaveBeenCalledTimes(1) - expect(asyncQueuer.getSize()).toBe(1) + expect(asyncQueuer.store.state.items.length).toBe(1) }) }) @@ -163,7 +163,7 @@ describe('AsyncQueuer', () => { started: false, }, ) - expect(asyncQueuer.getIsEmpty()).toBe(true) + expect(asyncQueuer.store.state.isEmpty).toBe(true) }) it('should return false when queuer has items', () => { const asyncQueuer = new AsyncQueuer( @@ -173,7 +173,7 @@ describe('AsyncQueuer', () => { }, ) asyncQueuer.addItem('test') - expect(asyncQueuer.getIsEmpty()).toBe(false) + expect(asyncQueuer.store.state.isEmpty).toBe(false) }) }) @@ -187,7 +187,7 @@ describe('AsyncQueuer', () => { }, ) asyncQueuer.addItem('test') - expect(asyncQueuer.getIsFull()).toBe(true) + expect(asyncQueuer.store.state.isFull).toBe(true) }) it('should return false when queuer is not full', () => { const asyncQueuer = new AsyncQueuer( @@ -197,7 +197,7 @@ describe('AsyncQueuer', () => { }, ) asyncQueuer.addItem('test') - expect(asyncQueuer.getIsFull()).toBe(false) + expect(asyncQueuer.store.state.isFull).toBe(false) }) }) @@ -211,7 +211,7 @@ describe('AsyncQueuer', () => { ) asyncQueuer.addItem('test') asyncQueuer.clear() - expect(asyncQueuer.getSize()).toBe(0) + expect(asyncQueuer.store.state.items.length).toBe(0) }) }) @@ -225,7 +225,7 @@ describe('AsyncQueuer', () => { initialItems: ['test'], }, ) - expect(asyncQueuer.getSize()).toBe(1) + expect(asyncQueuer.store.state.items.length).toBe(1) expect(asyncQueuer.getNextItem()).toBe('test') }) @@ -249,7 +249,7 @@ describe('AsyncQueuer', () => { }, }, ) - expect(asyncQueuer.getSize()).toBe(3) + expect(asyncQueuer.store.state.items.length).toBe(3) expect(asyncQueuer.getNextItem()).toBe('high') expect(asyncQueuer.getNextItem()).toBe('medium') expect(asyncQueuer.getNextItem()).toBe('low') @@ -263,8 +263,8 @@ describe('AsyncQueuer', () => { initialItems: [], }, ) - expect(asyncQueuer.getSize()).toBe(0) - expect(asyncQueuer.getIsEmpty()).toBe(true) + expect(asyncQueuer.store.state.items.length).toBe(0) + expect(asyncQueuer.store.state.isEmpty).toBe(true) }) }) @@ -491,85 +491,6 @@ describe('AsyncQueuer', () => { expect(asyncQueuer.getNextItem('front')).toBeUndefined() }) - describe('setOptions', () => { - it('should update queuer options', () => { - const asyncQueuer = new AsyncQueuer( - (item) => Promise.resolve(item), - { - started: false, - concurrency: 1, - }, - ) - asyncQueuer.setOptions({ concurrency: 2, started: true }) - expect(asyncQueuer.getOptions()).toEqual( - expect.objectContaining({ - concurrency: 2, - started: true, - }), - ) - }) - }) - - describe('getOptions', () => { - it('should return current queuer options', () => { - const options = { - started: false, - concurrency: 2, - maxSize: 10, - } - const asyncQueuer = new AsyncQueuer( - (item) => Promise.resolve(item), - options, - ) - expect(asyncQueuer.getOptions()).toEqual(expect.objectContaining(options)) - }) - }) - - describe('getWait', () => { - it('should return the current wait time', () => { - const asyncQueuer = new AsyncQueuer( - (item) => Promise.resolve(item), - { - wait: 100, - }, - ) - expect(asyncQueuer.getWait()).toBe(100) - - // Test with function - const asyncQueuer2 = new AsyncQueuer( - (item) => Promise.resolve(item), - { - wait: () => 200, - }, - ) - expect(asyncQueuer2.getWait()).toBe(200) - }) - }) - - describe('reset', () => { - it('should reset the queuer to its initial state', () => { - const asyncQueuer = new AsyncQueuer( - (item) => Promise.resolve(item), - { - started: false, - initialItems: ['initial'], - }, - ) - asyncQueuer.addItem('added') - expect(asyncQueuer.getSize()).toBe(2) - - asyncQueuer.reset() - expect(asyncQueuer.getSize()).toBe(0) - expect(asyncQueuer.getSuccessCount()).toBe(0) - expect(asyncQueuer.getErrorCount()).toBe(0) - expect(asyncQueuer.getSettledCount()).toBe(0) - - asyncQueuer.reset(true) - expect(asyncQueuer.getSize()).toBe(1) - expect(asyncQueuer.getNextItem()).toBe('initial') - }) - }) - describe('start', () => { it('should start the queuer', () => { const asyncQueuer = new AsyncQueuer( @@ -578,9 +499,9 @@ describe('AsyncQueuer', () => { started: false, }, ) - expect(asyncQueuer.getIsRunning()).toBe(false) + expect(asyncQueuer.store.state.isRunning).toBe(false) asyncQueuer.start() - expect(asyncQueuer.getIsRunning()).toBe(true) + expect(asyncQueuer.store.state.isRunning).toBe(true) }) }) @@ -592,9 +513,9 @@ describe('AsyncQueuer', () => { started: true, }, ) - expect(asyncQueuer.getIsRunning()).toBe(true) + expect(asyncQueuer.store.state.isRunning).toBe(true) asyncQueuer.stop() - expect(asyncQueuer.getIsRunning()).toBe(false) + expect(asyncQueuer.store.state.isRunning).toBe(false) }) }) @@ -608,7 +529,7 @@ describe('AsyncQueuer', () => { asyncQueuer.addItem('test2') await asyncQueuer.execute() await asyncQueuer.execute() - expect(asyncQueuer.getSuccessCount()).toBe(2) + expect(asyncQueuer.store.state.successCount).toBe(2) }) }) @@ -622,7 +543,7 @@ describe('AsyncQueuer', () => { asyncQueuer.addItem('test2') await asyncQueuer.execute() await asyncQueuer.execute() - expect(asyncQueuer.getErrorCount()).toBe(2) + expect(asyncQueuer.store.state.errorCount).toBe(2) }) }) @@ -636,7 +557,7 @@ describe('AsyncQueuer', () => { asyncQueuer.addItem('test2') await asyncQueuer.execute() await asyncQueuer.execute() - expect(asyncQueuer.getSettledCount()).toBe(2) + expect(asyncQueuer.store.state.settledCount).toBe(2) }) }) @@ -649,7 +570,7 @@ describe('AsyncQueuer', () => { asyncQueuer.addItem('test1') asyncQueuer.addItem('test2') asyncQueuer.addItem('test3') - expect(asyncQueuer.getRejectionCount()).toBe(2) + expect(asyncQueuer.store.state.rejectionCount).toBe(2) }) }) @@ -668,7 +589,7 @@ describe('AsyncQueuer', () => { await vi.advanceTimersByTimeAsync(200) asyncQueuer.start() await vi.advanceTimersByTimeAsync(0) - expect(asyncQueuer.getExpirationCount()).toBe(2) + expect(asyncQueuer.store.state.expirationCount).toBe(2) }) }) @@ -725,18 +646,6 @@ describe('AsyncQueuer', () => { expect(onExpire).toHaveBeenCalledWith('test', asyncQueuer) }) - it('should call onIsRunningChange when running state changes', () => { - const onIsRunningChange = vi.fn() - const asyncQueuer = new AsyncQueuer( - (item) => Promise.resolve(item), - { started: false, onIsRunningChange }, - ) - asyncQueuer.start() - expect(onIsRunningChange).toHaveBeenCalledWith(asyncQueuer) - asyncQueuer.stop() - expect(onIsRunningChange).toHaveBeenCalledWith(asyncQueuer) - }) - it('should call onReject when an item is rejected', () => { const onReject = vi.fn() const asyncQueuer = new AsyncQueuer( @@ -814,7 +723,8 @@ describe('AsyncQueuer', () => { return item }, { - concurrency: (queuer) => (queuer.getSize() > 1 ? 2 : 1), + concurrency: (queuer) => + queuer.store.state.items.length > 1 ? 2 : 1, started: false, }, ) @@ -875,7 +785,7 @@ describe('AsyncQueuer', () => { return item }, { - wait: (queuer) => (queuer.getSize() > 1 ? 100 : 50), + wait: (queuer) => (queuer.store.state.items.length > 1 ? 100 : 50), started: false, }, ) diff --git a/packages/pacer/tests/async-rate-limiter.test.ts b/packages/pacer/tests/async-rate-limiter.test.ts index 727a4a77e..d112e5c22 100644 --- a/packages/pacer/tests/async-rate-limiter.test.ts +++ b/packages/pacer/tests/async-rate-limiter.test.ts @@ -58,16 +58,16 @@ describe('AsyncRateLimiter', () => { }) await rateLimiter.maybeExecute() - expect(rateLimiter.getSuccessCount()).toBe(1) - expect(rateLimiter.getErrorCount()).toBe(0) + expect(rateLimiter.store.state.successCount).toBe(1) + expect(rateLimiter.store.state.errorCount).toBe(0) await rateLimiter.maybeExecute().catch(() => {}) - expect(rateLimiter.getSuccessCount()).toBe(1) - expect(rateLimiter.getErrorCount()).toBe(1) + expect(rateLimiter.store.state.successCount).toBe(1) + expect(rateLimiter.store.state.errorCount).toBe(1) await rateLimiter.maybeExecute() - expect(rateLimiter.getSuccessCount()).toBe(2) - expect(rateLimiter.getErrorCount()).toBe(1) + expect(rateLimiter.store.state.successCount).toBe(2) + expect(rateLimiter.store.state.errorCount).toBe(1) }) it('should track remaining executions', async () => { diff --git a/packages/pacer/tests/async-throttler.test.ts b/packages/pacer/tests/async-throttler.test.ts index 8f1bec8ac..0625269a1 100644 --- a/packages/pacer/tests/async-throttler.test.ts +++ b/packages/pacer/tests/async-throttler.test.ts @@ -92,14 +92,14 @@ describe('AsyncThrottler', () => { const throttler = new AsyncThrottler(mockFn, { wait: 100 }) await throttler.maybeExecute() - expect(throttler.getSuccessCount()).toBe(1) + expect(throttler.store.state.successCount).toBe(1) const promise = throttler.maybeExecute() - expect(throttler.getSuccessCount()).toBe(1) + expect(throttler.store.state.successCount).toBe(1) vi.advanceTimersByTime(100) await promise - expect(throttler.getSuccessCount()).toBe(2) + expect(throttler.store.state.successCount).toBe(2) }) it('should handle errors with onError callback', async () => { @@ -166,12 +166,12 @@ describe('AsyncThrottler', () => { const now = Date.now() await throttler.maybeExecute() - expect(throttler.getNextExecutionTime()).toBe(now + 100) + expect(throttler.store.state.nextExecutionTime).toBe(now + 100) vi.advanceTimersByTime(100) await throttler.maybeExecute() - expect(throttler.getNextExecutionTime()).toBe(now + 200) + expect(throttler.store.state.nextExecutionTime).toBe(now + 200) }) it('should cancel pending execution', async () => { @@ -254,28 +254,27 @@ describe('AsyncThrottler', () => { expect(mockFn).toHaveBeenLastCalledWith('first') }) - it('should let first call through but cancel subsequent calls', async () => { + it('should cancel pending calls when cancel is called', async () => { const mockFn = vi.fn().mockResolvedValue(undefined) const throttler = new AsyncThrottler(mockFn, { wait: 100 }) - // First call should go through - throttler.maybeExecute('first') - vi.advanceTimersByTime(100) + // First call executes immediately + const promise1 = throttler.maybeExecute('first') expect(mockFn).toHaveBeenCalledTimes(1) expect(mockFn).toHaveBeenLastCalledWith('first') + await promise1 - // Second call should be cancelled - throttler.maybeExecute('second') + // Second call is queued but then cancelled + const promise2 = throttler.maybeExecute('second') throttler.cancel() - vi.advanceTimersByTime(100) - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenLastCalledWith('first') + await promise2 - // Third call should also be cancelled - const promise2 = throttler.maybeExecute('third') + // Third call is also queued and cancelled + const promise3 = throttler.maybeExecute('third') throttler.cancel() - vi.advanceTimersByTime(100) - await promise2 + await promise3 + + // Only the first call should have executed expect(mockFn).toHaveBeenCalledTimes(1) expect(mockFn).toHaveBeenLastCalledWith('first') }) diff --git a/packages/pacer/tests/debouncer.test.ts b/packages/pacer/tests/debouncer.test.ts index e1a8aeec3..5f6cccb05 100644 --- a/packages/pacer/tests/debouncer.test.ts +++ b/packages/pacer/tests/debouncer.test.ts @@ -308,15 +308,15 @@ describe('Debouncer', () => { const mockFn = vi.fn() const debouncer = new Debouncer(mockFn, { wait: 1000 }) - expect(debouncer.getExecutionCount()).toBe(0) + expect(debouncer.store.state.executionCount).toBe(0) debouncer.maybeExecute('test') vi.advanceTimersByTime(1000) - expect(debouncer.getExecutionCount()).toBe(1) + expect(debouncer.store.state.executionCount).toBe(1) debouncer.maybeExecute('test') vi.advanceTimersByTime(1000) - expect(debouncer.getExecutionCount()).toBe(2) + expect(debouncer.store.state.executionCount).toBe(2) }) it('should track execution count with leading and trailing', () => { @@ -326,14 +326,14 @@ describe('Debouncer', () => { leading: true, }) - expect(debouncer.getExecutionCount()).toBe(0) + expect(debouncer.store.state.executionCount).toBe(0) debouncer.maybeExecute('test') debouncer.maybeExecute('test2') - expect(debouncer.getExecutionCount()).toBe(1) // Leading execution + expect(debouncer.store.state.executionCount).toBe(1) // Leading execution vi.advanceTimersByTime(1000) - expect(debouncer.getExecutionCount()).toBe(2) // Trailing execution + expect(debouncer.store.state.executionCount).toBe(2) // Trailing execution }) it('should not increment count when execution is cancelled', () => { @@ -344,7 +344,7 @@ describe('Debouncer', () => { debouncer.cancel() vi.advanceTimersByTime(1000) - expect(debouncer.getExecutionCount()).toBe(0) + expect(debouncer.store.state.executionCount).toBe(0) }) }) @@ -358,7 +358,7 @@ describe('Debouncer', () => { }) debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(true) + expect(debouncer.store.state.isPending).toBe(true) // Call again before wait expires vi.advanceTimersByTime(500) @@ -366,10 +366,10 @@ describe('Debouncer', () => { // Time is almost up vi.advanceTimersByTime(900) - expect(debouncer.getIsPending()).toBe(true) // Still pending + expect(debouncer.store.state.isPending).toBe(true) // Still pending vi.advanceTimersByTime(100) - expect(debouncer.getIsPending()).toBe(false) // Now it's done + expect(debouncer.store.state.isPending).toBe(false) // Now it's done }) it('should never be pending when trailing is false', () => { @@ -381,7 +381,7 @@ describe('Debouncer', () => { }) debouncer.maybeExecute('test1') - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) // Call again before wait expires vi.advanceTimersByTime(500) @@ -389,10 +389,10 @@ describe('Debouncer', () => { // Time is almost up vi.advanceTimersByTime(900) - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) vi.advanceTimersByTime(100) - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) }) it('should not be pending when leading and trailing are both false', () => { @@ -404,10 +404,10 @@ describe('Debouncer', () => { }) debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) vi.advanceTimersByTime(1000) - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) }) it('should not be pending when disabled', () => { @@ -415,10 +415,10 @@ describe('Debouncer', () => { const debouncer = new Debouncer(mockFn, { wait: 1000, enabled: false }) debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) vi.advanceTimersByTime(1000) - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) }) it('should update pending when enabling/disabling', () => { @@ -426,15 +426,15 @@ describe('Debouncer', () => { const debouncer = new Debouncer(mockFn, { wait: 1000 }) debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(true) + expect(debouncer.store.state.isPending).toBe(true) // Disable while there is a pending execution debouncer.setOptions({ enabled: false }) - expect(debouncer.getIsPending()).toBe(false) // Should be false now + expect(debouncer.store.state.isPending).toBe(false) // Should be false now // Re-enable debouncer.setOptions({ enabled: true }) - expect(debouncer.getIsPending()).toBe(false) // Should still be false + expect(debouncer.store.state.isPending).toBe(false) // Should still be false }) it('should set pending to false when canceled', () => { @@ -442,10 +442,10 @@ describe('Debouncer', () => { const debouncer = new Debouncer(mockFn, { wait: 1000 }) debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(true) + expect(debouncer.store.state.isPending).toBe(true) debouncer.cancel() - expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.store.state.isPending).toBe(false) }) }) @@ -632,24 +632,6 @@ describe('Debouncer', () => { vi.advanceTimersByTime(1000) expect(mockFn).toBeCalledTimes(1) }) - - it('should handle rapid enable/disable cycles', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) - - // Start execution - debouncer.maybeExecute() - - // Rapidly enable/disable - debouncer.setOptions({ enabled: false }) - debouncer.setOptions({ enabled: true }) - debouncer.setOptions({ enabled: false }) - debouncer.setOptions({ enabled: true }) - - // Should execute if last state was enabled - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) - }) }) }) diff --git a/packages/pacer/tests/queuer.test.ts b/packages/pacer/tests/queuer.test.ts index 65036e474..02902986e 100644 --- a/packages/pacer/tests/queuer.test.ts +++ b/packages/pacer/tests/queuer.test.ts @@ -5,14 +5,14 @@ describe('Queuer', () => { it('should create an empty queuer', () => { const fn = vi.fn() const queuer = new Queuer(fn, { started: false }) - expect(queuer.getIsEmpty()).toBe(true) - expect(queuer.getSize()).toBe(0) + expect(queuer.store.state.isEmpty).toBe(true) + expect(queuer.store.state.size).toBe(0) }) it('should start with started: true by default', () => { const fn = vi.fn() const queuer = new Queuer(fn, {}) - expect(queuer.getIsRunning()).toBe(true) + expect(queuer.store.state.isRunning).toBe(true) }) it('should respect maxSize option', () => { @@ -21,7 +21,7 @@ describe('Queuer', () => { expect(queuer.addItem(1)).toBe(true) expect(queuer.addItem(2)).toBe(true) expect(queuer.addItem(3)).toBe(false) - expect(queuer.getSize()).toBe(2) + expect(queuer.store.state.size).toBe(2) }) describe('addItem', () => { @@ -29,7 +29,7 @@ describe('Queuer', () => { const fn = vi.fn() const queuer = new Queuer(fn, { started: false }) expect(queuer.addItem(1)).toBe(true) - expect(queuer.getSize()).toBe(1) + expect(queuer.store.state.size).toBe(1) expect(queuer.peekNextItem()).toBe(1) }) }) @@ -63,7 +63,7 @@ describe('Queuer', () => { queuer.addItem(2) expect(queuer.peekNextItem()).toBe(1) - expect(queuer.getSize()).toBe(2) + expect(queuer.store.state.size).toBe(2) }) it('should return undefined when queuer is empty', () => { @@ -77,14 +77,14 @@ describe('Queuer', () => { it('should return true when queuer is empty', () => { const fn = vi.fn() const queuer = new Queuer(fn, { started: false }) - expect(queuer.getIsEmpty()).toBe(true) + expect(queuer.store.state.isEmpty).toBe(true) }) it('should return false when queuer has items', () => { const fn = vi.fn() const queuer = new Queuer(fn, { started: false }) queuer.addItem(1) - expect(queuer.getIsEmpty()).toBe(false) + expect(queuer.store.state.isEmpty).toBe(false) }) }) @@ -94,14 +94,14 @@ describe('Queuer', () => { const queuer = new Queuer(fn, { maxSize: 2, started: false }) queuer.addItem(1) queuer.addItem(2) - expect(queuer.getIsFull()).toBe(true) + expect(queuer.store.state.isFull).toBe(true) }) it('should return false when queuer is not full', () => { const fn = vi.fn() const queuer = new Queuer(fn, { maxSize: 2, started: false }) queuer.addItem(1) - expect(queuer.getIsFull()).toBe(false) + expect(queuer.store.state.isFull).toBe(false) }) }) @@ -113,8 +113,8 @@ describe('Queuer', () => { queuer.addItem(2) queuer.clear() - expect(queuer.getIsEmpty()).toBe(true) - expect(queuer.getSize()).toBe(0) + expect(queuer.store.state.isEmpty).toBe(true) + expect(queuer.store.state.size).toBe(0) expect(queuer.peekNextItem()).toBeUndefined() }) }) @@ -127,7 +127,7 @@ describe('Queuer', () => { initialItems: [1, 2, 3], started: false, }) - expect(queuer.getSize()).toBe(3) + expect(queuer.store.state.size).toBe(3) expect(queuer.peekAllItems()).toEqual([1, 2, 3]) }) @@ -153,7 +153,7 @@ describe('Queuer', () => { it('should handle empty initialItems array', () => { const fn = vi.fn() const queuer = new Queuer(fn, { initialItems: [], started: false }) - expect(queuer.getIsEmpty()).toBe(true) + expect(queuer.store.state.isEmpty).toBe(true) }) }) @@ -305,7 +305,7 @@ describe('Queuer', () => { expect(queuer.peekNextItem('front')).toBe(2) expect(queuer.peekNextItem('back')).toBe(3) - expect(queuer.getSize()).toBe(3) + expect(queuer.store.state.size).toBe(3) // Remove from both ends expect(queuer.execute('front')).toBe(2) @@ -313,41 +313,11 @@ describe('Queuer', () => { expect(queuer.execute('front')).toBe(1) }) - describe('setOptions', () => { - it('should update queuer options', () => { - const fn = vi.fn() - const queuer = new Queuer(fn, { wait: 100 }) - queuer.setOptions({ wait: 200 }) - expect(queuer.getWait()).toBe(200) - }) - }) - - describe('getOptions', () => { - it('should return current queuer options', () => { - const fn = vi.fn() - const options = { wait: 123, maxSize: 5 } - const queuer = new Queuer(fn, options) - const result = queuer.getOptions() - expect(result.wait).toBe(123) - expect(result.maxSize).toBe(5) - }) - }) - - describe('getWait', () => { - it('should return the current wait time', () => { - const fn = vi.fn() - const queuer = new Queuer(fn, { wait: 42 }) - expect(queuer.getWait()).toBe(42) - }) - }) - describe('reset', () => { it('should reset the queuer to its initial state', () => { const fn = vi.fn() const queuer = new Queuer(fn, { initialItems: [1, 2], started: false }) queuer.addItem(3) - queuer.reset(true) - expect(queuer.peekAllItems()).toEqual([1, 2]) queuer.reset() expect(queuer.peekAllItems()).toEqual([]) }) @@ -357,9 +327,9 @@ describe('Queuer', () => { it('should start the queuer', () => { const fn = vi.fn() const queuer = new Queuer(fn, { started: false }) - expect(queuer.getIsRunning()).toBe(false) + expect(queuer.store.state.isRunning).toBe(false) queuer.start() - expect(queuer.getIsRunning()).toBe(true) + expect(queuer.store.state.isRunning).toBe(true) }) }) @@ -367,9 +337,9 @@ describe('Queuer', () => { it('should stop the queuer', () => { const fn = vi.fn() const queuer = new Queuer(fn, { started: true }) - expect(queuer.getIsRunning()).toBe(true) + expect(queuer.store.state.isRunning).toBe(true) queuer.stop() - expect(queuer.getIsRunning()).toBe(false) + expect(queuer.store.state.isRunning).toBe(false) }) }) @@ -381,7 +351,7 @@ describe('Queuer', () => { queuer.addItem(2) queuer.execute() queuer.execute() - expect(queuer.getExecutionCount()).toBe(2) + expect(queuer.store.state.executionCount).toBe(2) }) }) @@ -391,7 +361,7 @@ describe('Queuer', () => { const queuer = new Queuer(fn, { maxSize: 1, started: false }) queuer.addItem(1) queuer.addItem(2) - expect(queuer.getRejectionCount()).toBe(1) + expect(queuer.store.state.rejectionCount).toBe(1) }) }) @@ -404,18 +374,6 @@ describe('Queuer', () => { queuer.addItem(2) expect(onReject).toHaveBeenCalledWith(2, queuer) }) - it('should call onExpire when an item expires', () => { - const onExpire = vi.fn() - const fn = vi.fn() - const queuer = new Queuer(fn, { - expirationDuration: 0, - onExpire, - started: false, - }) - queuer.addItem(1) - queuer['checkExpiredItems']() - expect(onExpire).not.toHaveBeenCalled() - }) it('should call onExecute when an item is executed', () => { const onExecute = vi.fn() const fn = vi.fn() @@ -424,14 +382,5 @@ describe('Queuer', () => { queuer.execute() expect(onExecute).toHaveBeenCalledWith(1, queuer) }) - it('should call onIsRunningChange when running state changes', () => { - const onIsRunningChange = vi.fn() - const fn = vi.fn() - const queuer = new Queuer(fn, { onIsRunningChange, started: false }) - queuer.start() - queuer.stop() - expect(onIsRunningChange).toHaveBeenCalledTimes(2) - expect(onIsRunningChange).toHaveBeenCalledWith(queuer) - }) }) }) diff --git a/packages/pacer/tests/rate-limiter.test.ts b/packages/pacer/tests/rate-limiter.test.ts index 55ac29900..e689acc98 100644 --- a/packages/pacer/tests/rate-limiter.test.ts +++ b/packages/pacer/tests/rate-limiter.test.ts @@ -45,14 +45,14 @@ describe('RateLimiter', () => { const rateLimiter = new RateLimiter(mockFn, { limit: 2, window: 1000 }) rateLimiter.maybeExecute() - expect(rateLimiter.getExecutionCount()).toBe(1) + expect(rateLimiter.store.state.executionCount).toBe(1) rateLimiter.maybeExecute() - expect(rateLimiter.getExecutionCount()).toBe(2) + expect(rateLimiter.store.state.executionCount).toBe(2) // This should not increment the count rateLimiter.maybeExecute() - expect(rateLimiter.getExecutionCount()).toBe(2) + expect(rateLimiter.store.state.executionCount).toBe(2) }) it('should track remaining executions', () => { diff --git a/packages/pacer/tests/throttler.test.ts b/packages/pacer/tests/throttler.test.ts index 6d266bac0..d0b06aa95 100644 --- a/packages/pacer/tests/throttler.test.ts +++ b/packages/pacer/tests/throttler.test.ts @@ -89,13 +89,13 @@ describe('Throttler', () => { const throttler = new Throttler(mockFn, { wait: 100 }) throttler.maybeExecute() - expect(throttler.getExecutionCount()).toBe(1) + expect(throttler.store.state.executionCount).toBe(1) throttler.maybeExecute() - expect(throttler.getExecutionCount()).toBe(1) + expect(throttler.store.state.executionCount).toBe(1) vi.advanceTimersByTime(100) - expect(throttler.getExecutionCount()).toBe(2) + expect(throttler.store.state.executionCount).toBe(2) }) it('should cancel pending trailing execution', () => { @@ -146,17 +146,17 @@ describe('Throttler', () => { const throttler = new Throttler(mockFn, { wait: 100 }) // Initial state - expect(throttler.getLastExecutionTime()).toBe(0) + expect(throttler.store.state.lastExecutionTime).toBe(0) // First execution throttler.maybeExecute('first') - const firstExecutionTime = throttler.getLastExecutionTime() + const firstExecutionTime = throttler.store.state.lastExecutionTime expect(firstExecutionTime).toBeGreaterThan(0) // Wait and execute again vi.advanceTimersByTime(100) throttler.maybeExecute('second') - const secondExecutionTime = throttler.getLastExecutionTime() + const secondExecutionTime = throttler.store.state.lastExecutionTime expect(secondExecutionTime).toBeGreaterThan(firstExecutionTime) }) diff --git a/packages/pacer/tests/utils.test.ts b/packages/pacer/tests/utils.test.ts index f9ec23c20..0450e5e13 100644 --- a/packages/pacer/tests/utils.test.ts +++ b/packages/pacer/tests/utils.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from 'vitest' -import { - bindInstanceMethods, - isFunction, - parseFunctionOrValue, -} from '../src/utils' +import { isFunction, parseFunctionOrValue } from '../src/utils' describe('isFunction', () => { it('should return true for function declarations', () => { @@ -60,61 +56,3 @@ describe('parseFunctionValue', () => { expect(await parseFunctionOrValue(asyncFn)).toBe(42) }) }) - -describe('bindInstanceMethods', () => { - it('should bind all instance methods', () => { - class TestClass { - value = 42 - method1() { - return this.value - } - method2() { - return this.value * 2 - } - } - - const instance = new TestClass() - const bound = bindInstanceMethods(instance) - - // Test that methods are bound by calling them without the instance context - const { method1, method2 } = bound - expect(method1()).toBe(42) - expect(method2()).toBe(84) - }) - - it('should not bind non-method properties', () => { - class TestClass { - value = 42 - method() { - return this.value - } - } - - const instance = new TestClass() - const bound = bindInstanceMethods(instance) - - expect(bound.value).toBe(42) - expect(bound.method()).toBe(42) - }) - - it('should work with inheritance', () => { - class Parent { - parentMethod() { - return 'parent' - } - } - - class Child extends Parent { - childMethod() { - return 'child' - } - } - - const instance = new Child() - const bound = bindInstanceMethods(instance) - - const { parentMethod, childMethod } = bound - expect(parentMethod()).toBe('parent') - expect(childMethod()).toBe('child') - }) -}) diff --git a/packages/pacer/tsconfig.json b/packages/pacer/tsconfig.json index 44533f71c..34449d762 100644 --- a/packages/pacer/tsconfig.json +++ b/packages/pacer/tsconfig.json @@ -1,4 +1,10 @@ { "extends": "../../tsconfig.json", - "include": ["src", "eslint.config.js", "vite.config.ts", "tests"] + "include": [ + "src", + "eslint.config.js", + "vite.config.ts", + "tests", + "../persister/src/compare.ts" + ] } diff --git a/packages/persister/CHANGELOG.md b/packages/persister/CHANGELOG.md new file mode 100644 index 000000000..827695e12 --- /dev/null +++ b/packages/persister/CHANGELOG.md @@ -0,0 +1 @@ +# @tanstack/persister diff --git a/packages/persister/eslint.config.js b/packages/persister/eslint.config.js new file mode 100644 index 000000000..e472c69e7 --- /dev/null +++ b/packages/persister/eslint.config.js @@ -0,0 +1,10 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + rules: {}, + }, +] diff --git a/packages/persister/package.json b/packages/persister/package.json new file mode 100644 index 000000000..eda886cfe --- /dev/null +++ b/packages/persister/package.json @@ -0,0 +1,113 @@ +{ + "name": "@tanstack/persister", + "version": "0.0.0", + "description": "Utilities for persisting state to local storage, session storage, indexedDB, and more.", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/persister.git", + "directory": "packages/persister" + }, + "homepage": "https://tanstack.com/persister", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "state", + "persister", + "local-storage", + "session-storage", + "indexed-db", + "cache" + ], + "type": "module", + "types": "dist/esm/index.d.ts", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./async-persister": { + "import": { + "types": "./dist/esm/async-persister.d.ts", + "default": "./dist/esm/async-persister.js" + }, + "require": { + "types": "./dist/cjs/async-persister.d.cts", + "default": "./dist/cjs/async-persister.cjs" + } + }, + "./compare": { + "import": { + "types": "./dist/esm/compare.d.ts", + "default": "./dist/esm/compare.js" + }, + "require": { + "types": "./dist/cjs/compare.d.cts", + "default": "./dist/cjs/compare.cjs" + } + }, + "./persister": { + "import": { + "types": "./dist/esm/persister.d.ts", + "default": "./dist/esm/persister.js" + }, + "require": { + "types": "./dist/cjs/persister.d.cts", + "default": "./dist/cjs/persister.cjs" + } + }, + "./storage-persister": { + "import": { + "types": "./dist/esm/storage-persister.d.ts", + "default": "./dist/esm/storage-persister.js" + }, + "require": { + "types": "./dist/cjs/storage-persister.d.cts", + "default": "./dist/cjs/storage-persister.cjs" + } + }, + "./types": { + "types": "./dist/esm/types.d.ts" + }, + "./utils": { + "import": { + "types": "./dist/esm/utils.d.ts", + "default": "./dist/esm/utils.js" + }, + "require": { + "types": "./dist/cjs/utils.d.cts", + "default": "./dist/cjs/utils.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "files": [ + "dist/", + "src" + ], + "scripts": { + "clean": "premove ./build ./dist", + "lint:fix": "eslint ./src --fix", + "test:eslint": "eslint ./src", + "test:lib": "vitest", + "test:lib:dev": "pnpm test:lib --watch", + "test:types": "tsc", + "test:build": "publint --strict", + "build": "vite build" + } +} diff --git a/packages/persister/src/async-persister.ts b/packages/persister/src/async-persister.ts new file mode 100644 index 000000000..14410afd5 --- /dev/null +++ b/packages/persister/src/async-persister.ts @@ -0,0 +1,20 @@ +/** + * Abstract class that defines the contract for an asynchronous state persister. + * An async persister is responsible for saving and loading state to a storage medium using asynchronous operations. + * + * This is useful for scenarios where persistence involves asynchronous APIs, such as IndexedDB, remote storage, or custom async backends. + * + * Implementations should provide methods to load, save, and clear persisted state for a given key. + * + * See also: `Persister` for synchronous persister implementations. + */ +export abstract class AsyncPersister< + TState, + TSelected extends Partial = TState, +> { + constructor(public readonly key: string) {} + + abstract loadState: () => TSelected | undefined + abstract saveState: (state: TState) => void + abstract clearState: (useDefaultState?: boolean) => void +} diff --git a/packages/pacer/src/compare.ts b/packages/persister/src/compare.ts similarity index 100% rename from packages/pacer/src/compare.ts rename to packages/persister/src/compare.ts diff --git a/packages/persister/src/index.ts b/packages/persister/src/index.ts new file mode 100644 index 000000000..572726e66 --- /dev/null +++ b/packages/persister/src/index.ts @@ -0,0 +1,6 @@ +export * from './async-persister' +export * from './compare' +export * from './persister' +export * from './storage-persister' +export * from './types' +export * from './utils' diff --git a/packages/persister/src/persister.ts b/packages/persister/src/persister.ts new file mode 100644 index 000000000..1250ab345 --- /dev/null +++ b/packages/persister/src/persister.ts @@ -0,0 +1,36 @@ +/** + * Abstract class that defines the contract for a state persister implementation. + * A persister is responsible for loading and saving state to a storage medium. + * + * @example + * ```ts + * class MyPersister extends Persister { + * constructor() { + * super(key) + * } + * + * loadState(): MyState | undefined { + * // Load state from storage + * return state + * } + * + * saveState(, state: MyState): void { + * // Save state to storage + * } + * + * clearState(useDefaultState?: boolean): void { + * // Clear state from storage or set the default state if provided and specified to be used + * } + * } + * ``` + */ +export abstract class Persister< + TState, + TSelected extends Partial = TState, +> { + constructor(public readonly key: string) {} + + abstract loadState: () => TSelected | undefined + abstract saveState: (state: TState) => void + abstract clearState: (useDefaultState?: boolean) => void +} diff --git a/packages/persister/src/storage-persister.ts b/packages/persister/src/storage-persister.ts new file mode 100644 index 000000000..27376e674 --- /dev/null +++ b/packages/persister/src/storage-persister.ts @@ -0,0 +1,265 @@ +import { Persister } from './persister' +import type { RequiredKeys } from './types' + +export interface PersistedStorage< + TState, + TSelected extends Partial = TState, +> { + buster?: string + state: TSelected | undefined + timestamp: number +} + +/** + * Configuration options for creating a browser-based state persister. + * + * The persister can use either localStorage (persists across browser sessions) or + * sessionStorage (cleared when browser tab/window closes) to store serialized state. + */ +export interface StoragePersisterOptions< + TState, + TSelected extends Partial = TState, +> { + /** + * A version string used to invalidate cached state. When changed, any existing + * stored state will be considered invalid and cleared. + */ + buster?: string + /** + * The default state to use if no state is found in storage. + */ + defaultState?: TState + /** + * Optional function to customize how state is deserialized after loading from storage. + * By default, JSON.parse is used. + * + * Optionally, consider using SuperJSON for better deserialization of complex objects. See https://github.com/flightcontrolhq/superjson + */ + deserializer?: (state: string) => PersistedStorage + /** + * Unique identifier used as the storage key for persisting state. + */ + key: string + /** + * Maximum age in milliseconds before stored state is considered expired. + * When exceeded, the state will be cleared and treated as if it doesn't exist. + */ + maxAge?: number + /** + * Optional callback that runs after state is successfully loaded. + */ + onLoadState?: ( + state: TSelected | undefined, + storagePersister: StoragePersister, + ) => void + /** + * Optional callback that runs after state is unable to be loaded. + */ + onLoadStateError?: ( + error: Error, + storagePersister: StoragePersister, + ) => void + /** + * Optional callback that runs after state is successfully saved. + */ + onSaveState?: ( + state: TSelected, + storagePersister: StoragePersister, + ) => void + /** + * Optional callback that runs after state is unable to be saved. + * For example, if the storage is full (localStorage >= 5MB) + */ + onSaveStateError?: ( + error: Error, + storagePersister: StoragePersister, + ) => void + /** + * Optional function to customize how state is serialized before saving to storage. + * By default, JSON.stringify is used. + * + * Optionally, consider using SuperJSON for better serialization of complex objects. See https://github.com/flightcontrolhq/superjson + */ + serializer?: (state: PersistedStorage) => string + /** + * Optional function to filter which parts of the state are persisted and loaded. + * When provided, only the filtered state will be saved to storage and returned when loading. + * This is useful for excluding sensitive or temporary data from persistence. + * + * Note: Don't use this to replace the serialization. Use the `serializer` option instead for that. + */ + select?: (state: TState) => TSelected + /** + * The browser storage implementation to use for persisting state. + * Typically window.localStorage or window.sessionStorage. + * + * Defaults to window.localStorage. + */ + storage?: Storage | null +} + +type DefaultOptions = RequiredKeys< + Partial>, + 'deserializer' | 'serializer' | 'storage' +> + +const defaultOptions: DefaultOptions = { + deserializer: JSON.parse, + serializer: JSON.stringify, + storage: typeof window !== 'undefined' ? window.localStorage : null, +} + +/** + * A persister that saves state to browser local/session storage. + * + * The persister can use either localStorage (persists across browser sessions) or + * sessionStorage (cleared when browser tab/window closes). State is automatically + * serialized to JSON when saving and deserialized when loading. + * + * Optionally, a `buster` string can be provided to force cache busting by storing it in the value. + * Optionally, a `maxAge` (in ms) can be provided to expire the stored state after a certain duration. + * Optionally, callbacks can be provided to run after state is saved or loaded. + * + * @example + * ```ts + * const persister = new StoragePersister({ + * key: 'my-rate-limiter', // required + * storage: window.localStorage, + * buster: 'v2', + * maxAge: 1000 * 60 * 60, // 1 hour + * stateTransform: (state) => ({ + * // Only persist specific parts of the state + * count: state.count, + * lastReset: state.lastReset, + * // Exclude sensitive or temporary data + * }), + * onSaveState: (key, state) => console.log('State saved:', key, state), + * onLoadState: (key, state) => console.log('State loaded:', key, state), + * onLoadStateError: (key, error) => console.error('Error loading state:', key, error), + * onSaveStateError: (key, error) => console.error('Error saving state:', key, error) + * }) + * ``` + */ +export class StoragePersister< + TState, + TSelected extends Partial = TState, +> extends Persister { + options: StoragePersisterOptions & DefaultOptions + + constructor(initialOptions: StoragePersisterOptions) { + super(initialOptions.key) + this.options = { + ...defaultOptions, + ...initialOptions, + } + } + + /** + * Updates the persister options + */ + setOptions = ( + newOptions: Partial>, + ): void => { + this.options = { ...this.options, ...newOptions } + } + + /** + * Saves the state to storage + */ + saveState = (state: TState | TSelected): void => { + try { + const stateToSave = this.options.select + ? this.options.select(state) + : state + + this.options.storage?.setItem( + this.key, + this.options.serializer({ + buster: this.options.buster, + state: stateToSave, + timestamp: Date.now(), + }), + ) + this.options.onSaveState?.(state, this) + } catch (error) { + console.error(error) + this.options.onSaveStateError?.(error as Error, this) + } + } + + /** + * Loads the state from storage + */ + loadState = (): TSelected | undefined => { + const stored = this.options.storage?.getItem(this.key) + if (!stored) { + return undefined + } + + try { + const parsed = this.options.deserializer(stored) + + const isValidVersion = + !this.options.buster || parsed.buster === this.options.buster + + const isNotExpired = + !this.options.maxAge || + !parsed.timestamp || + Date.now() - parsed.timestamp <= this.options.maxAge + + if (!isValidVersion || !isNotExpired) { + // clear the item from storage + this.options.storage?.removeItem(this.key) + return undefined + } + + const state = parsed.state + this.options.onLoadState?.(state, this) + return state + } catch (error) { + console.error(error) + this.options.onLoadStateError?.(error as Error, this) + return undefined + } + } + + /** + * Handles the storage event and ignores events triggered by the current tab. + * + * The storage event is only fired in other tabs/windows, not the one that made the change. + * However, some browsers or environments may fire it in the same tab, so we check if the event's storageArea + * matches the persister's storage and ignore if not. + */ + private handleStorageChange = (e: StorageEvent) => { + // Ignore events not from the same storage area or not matching our key + if (e.storageArea !== this.options.storage) { + return + } + // Ignore events triggered by the current tab (storage event is not fired in the same tab in most browsers) + // But this check is defensive in case of non-standard environments + if (e.key === this.key && e.newValue) { + this.loadState() + } + } + + subscribeToStorage = (): void => { + typeof window !== 'undefined' && + window.addEventListener('storage', this.handleStorageChange) + } + + unsubscribeFromStorage = (): void => { + typeof window !== 'undefined' && + window.removeEventListener('storage', this.handleStorageChange) + } + + /** + * Clears the state from storage or sets the default state if provided and specified to be used + */ + clearState = (useDefaultState: boolean = false): void => { + if (useDefaultState) { + this.saveState(this.options.defaultState) + } else { + this.options.storage?.removeItem(this.key) + } + } +} diff --git a/packages/persister/src/types.ts b/packages/persister/src/types.ts new file mode 100644 index 000000000..6e779c0b3 --- /dev/null +++ b/packages/persister/src/types.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/** + * Represents a function that can be called with any arguments and returns any value. + */ +export type AnyFunction = (...args: Array) => any + +/** + * Represents an asynchronous function that can be called with any arguments and returns a promise. + */ +export type AnyAsyncFunction = (...args: Array) => Promise + +export type OptionalKeys = Omit & + Partial> + +export type RequiredKeys = Required< + Pick +> & + Omit diff --git a/packages/persister/src/utils.ts b/packages/persister/src/utils.ts new file mode 100644 index 000000000..3a510d8ec --- /dev/null +++ b/packages/persister/src/utils.ts @@ -0,0 +1,12 @@ +import type { AnyFunction } from './types' + +export function isFunction(value: any): value is T { + return typeof value === 'function' +} + +export function parseFunctionOrValue>( + value: T | ((...args: TArgs) => T), + ...args: TArgs +): T { + return isFunction(value) ? value(...args) : value +} diff --git a/packages/pacer/tests/compare.test.ts b/packages/persister/tests/compare.test.ts similarity index 100% rename from packages/pacer/tests/compare.test.ts rename to packages/persister/tests/compare.test.ts diff --git a/packages/persister/tests/persister.test.ts b/packages/persister/tests/persister.test.ts new file mode 100644 index 000000000..c11f9c444 --- /dev/null +++ b/packages/persister/tests/persister.test.ts @@ -0,0 +1,132 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { StoragePersister } from '../src/storage-persister' + +describe('createStoragePersister', () => { + let storage: Storage + let persister: StoragePersister + + beforeEach(() => { + storage = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), + length: 0, + key: vi.fn(), + } + persister = new StoragePersister({ + key: 'test-key', + storage, + }) + }) + + it('should load state from storage', () => { + const state = { count: 1 } + const timestamp = Date.now() + ;(storage.getItem as any).mockReturnValue( + JSON.stringify({ state, timestamp }), + ) + const result = persister.loadState() + expect(result).toEqual(state) + }) + + it('should return undefined when no state exists', () => { + ;(storage.getItem as any).mockReturnValue(null) + const result = persister.loadState() + expect(result).toBeUndefined() + }) + + it('should handle storage errors when saving', () => { + const error = new Error('Storage error') + ;(storage.setItem as any).mockImplementation(() => { + throw error + }) + const onSaveStateError = vi.fn() + persister = new StoragePersister({ + key: 'test-key', + storage, + onSaveStateError, + }) + persister.saveState({ count: 1 }) + expect(onSaveStateError).toHaveBeenCalledWith(error, persister) + }) + + it('should call onSaveState callback when state is saved', () => { + const onSaveState = vi.fn() + persister = new StoragePersister({ + key: 'test-key', + storage, + onSaveState, + }) + const state = { count: 1 } + persister.saveState(state) + expect(onSaveState).toHaveBeenCalledWith(state, persister) + }) + + it('should call onLoadState callback when state is loaded', () => { + const onLoadState = vi.fn() + persister = new StoragePersister({ + key: 'test-key', + storage, + onLoadState, + }) + const state = { count: 1 } + ;(storage.getItem as any).mockReturnValue( + JSON.stringify({ state, timestamp: Date.now() }), + ) + persister.loadState() + expect(onLoadState).toHaveBeenCalledWith(state, persister) + }) + + it('should respect buster string when loading state', () => { + persister = new StoragePersister({ + key: 'test-key', + storage, + buster: 'v2', + }) + ;(storage.getItem as any).mockReturnValue( + JSON.stringify({ + state: { count: 1 }, + timestamp: Date.now(), + buster: 'v1', + }), + ) + const result = persister.loadState() + expect(result).toBeUndefined() + }) + + it('should respect maxAge when loading state', () => { + persister = new StoragePersister({ + key: 'test-key', + storage, + maxAge: 1000, + }) + ;(storage.getItem as any).mockReturnValue( + JSON.stringify({ + state: { count: 1 }, + timestamp: Date.now() - 2000, + }), + ) + const result = persister.loadState() + expect(result).toBeUndefined() + }) + + it('should use custom serializer and deserializer', () => { + const serializer = vi.fn((data) => JSON.stringify(data)) + const deserializer = vi.fn((data) => JSON.parse(data)) + persister = new StoragePersister({ + key: 'test-key', + storage, + serializer, + deserializer, + }) + const state = { count: 1 } + persister.saveState(state) + expect(serializer).toHaveBeenCalled() + ;(storage.getItem as any).mockReturnValue( + JSON.stringify({ state, timestamp: Date.now() }), + ) + persister.loadState() + expect(deserializer).toHaveBeenCalled() + }) +}) diff --git a/packages/persister/tests/test-setup.ts b/packages/persister/tests/test-setup.ts new file mode 100644 index 000000000..a9d0dd31a --- /dev/null +++ b/packages/persister/tests/test-setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest' diff --git a/packages/persister/tsconfig.docs.json b/packages/persister/tsconfig.docs.json new file mode 100644 index 000000000..2880b4dfa --- /dev/null +++ b/packages/persister/tsconfig.docs.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["tests", "src"] +} diff --git a/packages/persister/tsconfig.json b/packages/persister/tsconfig.json new file mode 100644 index 000000000..44533f71c --- /dev/null +++ b/packages/persister/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "eslint.config.js", "vite.config.ts", "tests"] +} diff --git a/packages/persister/vite.config.ts b/packages/persister/vite.config.ts new file mode 100644 index 000000000..ed5d11383 --- /dev/null +++ b/packages/persister/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import packageJson from './package.json' + +const config = defineConfig({ + test: { + name: packageJson.name, + dir: './', + watch: false, + environment: 'jsdom', + setupFiles: ['./tests/test-setup.ts'], + globals: true, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: ['./src/index.ts'], + srcDir: './src', + }), +) diff --git a/packages/react-pacer/package.json b/packages/react-pacer/package.json index b55562472..dfc0897af 100644 --- a/packages/react-pacer/package.json +++ b/packages/react-pacer/package.json @@ -37,6 +37,16 @@ "default": "./dist/cjs/index.cjs" } }, + "./async-batcher": { + "import": { + "types": "./dist/esm/async-batcher/index.d.ts", + "default": "./dist/esm/async-batcher/index.js" + }, + "require": { + "types": "./dist/cjs/async-batcher/index.d.cts", + "default": "./dist/cjs/async-batcher/index.cjs" + } + }, "./async-debouncer": { "import": { "types": "./dist/esm/async-debouncer/index.d.ts", @@ -161,12 +171,13 @@ "build": "vite build" }, "dependencies": { - "@tanstack/pacer": "workspace:*" + "@tanstack/pacer": "workspace:*", + "@tanstack/react-store": "^0.7.3" }, "devDependencies": { - "@eslint-react/eslint-plugin": "^1.51.2", - "@types/react": "^19.1.6", - "@vitejs/plugin-react": "^4.5.1", + "@eslint-react/eslint-plugin": "^1.52.2", + "@types/react": "^19.1.8", + "@vitejs/plugin-react": "^4.6.0", "eslint-plugin-react-compiler": "19.1.0-rc.2", "eslint-plugin-react-hooks": "^5.2.0", "react": "^19.1.0" diff --git a/packages/react-pacer/src/async-batcher/index.ts b/packages/react-pacer/src/async-batcher/index.ts new file mode 100644 index 000000000..f0fe9c2be --- /dev/null +++ b/packages/react-pacer/src/async-batcher/index.ts @@ -0,0 +1,3 @@ +export * from '@tanstack/pacer/async-batcher' + +export * from './useAsyncBatcher' diff --git a/packages/react-pacer/src/async-batcher/useAsyncBatcher.ts b/packages/react-pacer/src/async-batcher/useAsyncBatcher.ts new file mode 100644 index 000000000..93c0cfc59 --- /dev/null +++ b/packages/react-pacer/src/async-batcher/useAsyncBatcher.ts @@ -0,0 +1,96 @@ +import { useMemo, useState } from 'react' +import { AsyncBatcher } from '@tanstack/pacer/async-batcher' +import { useStore } from '@tanstack/react-store' +import type { + AsyncBatcherOptions, + AsyncBatcherState, +} from '@tanstack/pacer/async-batcher' + +export interface ReactAsyncBatcher< + TValue, + TSelected = AsyncBatcherState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the batcher state changes + * + * Use this instead of `batcher.store.state` + */ + readonly state: Readonly +} + +/** + * A React hook that creates an `AsyncBatcher` instance for managing asynchronous batches of items. + * + * This is the async version of the useBatcher hook. Unlike the sync version, this async batcher: + * - Handles promises and returns results from batch executions + * - Provides error handling with configurable error behavior + * - Tracks success, error, and settle counts separately + * - Has state tracking for when batches are executing + * - Returns the result of the batch function execution + * + * Features: + * - Configurable batch size and wait time + * - Custom batch processing logic via getShouldExecute + * - Event callbacks for monitoring batch operations + * - Error handling for failed batch operations + * - Automatic or manual batch processing + * + * The batcher collects items and processes them in batches based on: + * - Maximum batch size (number of items per batch) + * - Time-based batching (process after X milliseconds) + * - Custom batch processing logic via getShouldExecute + * + * Error Handling: + * - If an `onError` handler is provided, it will be called with the error and batcher instance + * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown + * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed + * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown + * - The error state can be checked using the underlying AsyncBatcher instance + * + * @example + * ```tsx + * // Basic async batcher for API requests + * const asyncBatcher = useAsyncBatcher( + * async (items) => { + * const results = await Promise.all(items.map(item => processItem(item))); + * return results; + * }, + * { + * maxSize: 10, + * wait: 2000, + * onSuccess: (result) => { + * console.log('Batch processed successfully:', result); + * }, + * onError: (error) => { + * console.error('Batch processing failed:', error); + * } + * } + * ); + * + * // Add items to batch + * asyncBatcher.addItem(newItem); + * + * // Manually execute batch + * const result = await asyncBatcher.execute(); + * ``` + */ +export function useAsyncBatcher>( + fn: (items: Array) => Promise, + options: AsyncBatcherOptions = {}, + selector?: (state: AsyncBatcherState) => TSelected, +): ReactAsyncBatcher { + const [asyncBatcher] = useState(() => new AsyncBatcher(fn, options)) + + const state = useStore(asyncBatcher.store, selector) + + asyncBatcher.setOptions(options) + + return useMemo( + () => + ({ + ...asyncBatcher, + state, + }) as unknown as ReactAsyncBatcher, // omit `store` in favor of `state` + [asyncBatcher, state], + ) +} diff --git a/packages/react-pacer/src/async-debouncer/index.ts b/packages/react-pacer/src/async-debouncer/index.ts index e61e0b246..ac389c847 100644 --- a/packages/react-pacer/src/async-debouncer/index.ts +++ b/packages/react-pacer/src/async-debouncer/index.ts @@ -2,3 +2,4 @@ export * from '@tanstack/pacer/async-debouncer' export * from './useAsyncDebouncer' +export * from './useAsyncDebouncedCallback' diff --git a/packages/react-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts b/packages/react-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts new file mode 100644 index 000000000..b9d6690d2 --- /dev/null +++ b/packages/react-pacer/src/async-debouncer/useAsyncDebouncedCallback.ts @@ -0,0 +1,60 @@ +import { useCallback } from 'react' +import { useAsyncDebouncer } from './useAsyncDebouncer' +import type { + AsyncDebouncerOptions, + AsyncDebouncerState, +} from '@tanstack/pacer/async-debouncer' +import type { AnyAsyncFunction } from '@tanstack/pacer/types' + +/** + * A React hook that creates a debounced version of an async callback function. + * This hook is a convenient wrapper around the `useAsyncDebouncer` hook, + * providing a stable, debounced async function reference for use in React components. + * + * The debounced async function will only execute after the specified wait time has elapsed + * since its last invocation. If called again before the wait time expires, the timer + * resets and starts waiting again. The returned function always returns a promise + * that resolves or rejects with the result of the original async function. + * + * This hook provides a simpler API compared to `useAsyncDebouncer`, making it ideal for basic + * async debouncing needs. However, it does not expose the underlying AsyncDebouncer instance. + * + * For advanced usage requiring features like: + * - Manual cancellation + * - Access to execution/error state + * - Custom useCallback dependencies + * + * Consider using the `useAsyncDebouncer` hook instead. + * + * @example + * ```tsx + * // Debounce an async search handler + * const handleSearch = useAsyncDebouncedCallback(async (query: string) => { + * const results = await fetchSearchResults(query); + * return results; + * }, { + * wait: 500 // Wait 500ms between executions + * }); + * + * // Use in an input + * handleSearch(e.target.value)} + * /> + * ``` + */ +export function useAsyncDebouncedCallback< + TFn extends AnyAsyncFunction, + TSelected = {}, +>( + fn: TFn, + options: AsyncDebouncerOptions, + selector: (state: AsyncDebouncerState) => TSelected = () => + ({}) as TSelected, +): (...args: Parameters) => Promise> { + const asyncDebouncedFn = useAsyncDebouncer(fn, options, selector).maybeExecute + return useCallback( + (...args) => asyncDebouncedFn(...args) as Promise>, + [asyncDebouncedFn], + ) +} diff --git a/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts b/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts index ff7ba18f5..fde7c82ad 100644 --- a/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts +++ b/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts @@ -1,8 +1,23 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { AsyncDebouncer } from '@tanstack/pacer/async-debouncer' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { useStore } from '@tanstack/react-store' import type { AnyAsyncFunction } from '@tanstack/pacer/types' -import type { AsyncDebouncerOptions } from '@tanstack/pacer/async-debouncer' +import type { + AsyncDebouncerOptions, + AsyncDebouncerState, +} from '@tanstack/pacer/async-debouncer' + +export interface ReactAsyncDebouncer< + TFn extends AnyAsyncFunction, + TSelected = AsyncDebouncerState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the debouncer state changes + * + * Use this instead of `debouncer.store.state` + */ + readonly state: Readonly +} /** * A low-level React hook that creates an `AsyncDebouncer` instance to delay execution of an async function. @@ -57,13 +72,17 @@ import type { AsyncDebouncerOptions } from '@tanstack/pacer/async-debouncer' * ); * ``` */ -export function useAsyncDebouncer( +export function useAsyncDebouncer< + TFn extends AnyAsyncFunction, + TSelected = AsyncDebouncerState, +>( fn: TFn, options: AsyncDebouncerOptions, -): AsyncDebouncer { - const [asyncDebouncer] = useState(() => - bindInstanceMethods(new AsyncDebouncer(fn, options)), - ) + selector?: (state: AsyncDebouncerState) => TSelected, +): ReactAsyncDebouncer { + const [asyncDebouncer] = useState(() => new AsyncDebouncer(fn, options)) + + const state = useStore(asyncDebouncer.store, selector) asyncDebouncer.setOptions(options) @@ -73,5 +92,12 @@ export function useAsyncDebouncer( } }, [asyncDebouncer]) - return asyncDebouncer + return useMemo( + () => + ({ + ...asyncDebouncer, + state, + }) as unknown as ReactAsyncDebouncer, // omit `store` in favor of `state` + [asyncDebouncer, state], + ) } diff --git a/packages/react-pacer/src/async-queuer/useAsyncQueuedState.ts b/packages/react-pacer/src/async-queuer/useAsyncQueuedState.ts index a8533f4c5..35cc0283b 100644 --- a/packages/react-pacer/src/async-queuer/useAsyncQueuedState.ts +++ b/packages/react-pacer/src/async-queuer/useAsyncQueuedState.ts @@ -1,8 +1,8 @@ -import { useState } from 'react' import { useAsyncQueuer } from './useAsyncQueuer' +import type { ReactAsyncQueuer } from './useAsyncQueuer' import type { - AsyncQueuer, AsyncQueuerOptions, + AsyncQueuerState, } from '@tanstack/pacer/async-queuer' /** @@ -50,23 +50,18 @@ import type { * const pendingCount = asyncQueuer.peekPendingItems().length; * ``` */ -export function useAsyncQueuedState( +export function useAsyncQueuedState< + TValue, + TSelected extends Pick< + AsyncQueuerState, + 'items' + > = AsyncQueuerState, +>( fn: (value: TValue) => Promise, options: AsyncQueuerOptions = {}, -): [Array, AsyncQueuer] { - const [items, setItems] = useState>(options.initialItems ?? []) + selector?: (state: AsyncQueuerState) => TSelected, +): [Array, ReactAsyncQueuer] { + const asyncQueuer = useAsyncQueuer(fn, options, selector) - const asyncQueuer = useAsyncQueuer(fn, { - ...options, - onItemsChange: (asyncQueuer) => { - setItems(asyncQueuer.peekAllItems()) - options.onItemsChange?.(asyncQueuer) - }, - onIsRunningChange: (queue) => { - setItems((prev) => [...prev]) // rerender - options.onIsRunningChange?.(queue) - }, - }) - - return [items, asyncQueuer] + return [asyncQueuer.state.items, asyncQueuer] } diff --git a/packages/react-pacer/src/async-queuer/useAsyncQueuer.ts b/packages/react-pacer/src/async-queuer/useAsyncQueuer.ts index ab3859157..e152f086b 100644 --- a/packages/react-pacer/src/async-queuer/useAsyncQueuer.ts +++ b/packages/react-pacer/src/async-queuer/useAsyncQueuer.ts @@ -1,7 +1,20 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import { AsyncQueuer } from '@tanstack/pacer/async-queuer' -import { bindInstanceMethods } from '@tanstack/pacer/utils' -import type { AsyncQueuerOptions } from '@tanstack/pacer/async-queuer' +import { useStore } from '@tanstack/react-store' +import type { + AsyncQueuerOptions, + AsyncQueuerState, +} from '@tanstack/pacer/async-queuer' + +export interface ReactAsyncQueuer> + extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the queuer state changes + * + * Use this instead of `queuer.store.state` + */ + readonly state: Readonly +} /** * A lower-level React hook that creates an `AsyncQueuer` instance for managing an async queue of items. @@ -48,15 +61,23 @@ import type { AsyncQueuerOptions } from '@tanstack/pacer/async-queuer' * asyncQueuer.start(); * ``` */ -export function useAsyncQueuer( +export function useAsyncQueuer>( fn: (value: TValue) => Promise, options: AsyncQueuerOptions = {}, -): AsyncQueuer { - const [asyncQueuer] = useState(() => - bindInstanceMethods(new AsyncQueuer(fn, options)), - ) + selector?: (state: AsyncQueuerState) => TSelected, +): ReactAsyncQueuer { + const [asyncQueuer] = useState(() => new AsyncQueuer(fn, options)) + + const state = useStore(asyncQueuer.store, selector) asyncQueuer.setOptions(options) - return asyncQueuer + return useMemo( + () => + ({ + ...asyncQueuer, + state, + }) as unknown as ReactAsyncQueuer, // omit `store` in favor of `state` + [asyncQueuer, state], + ) } diff --git a/packages/react-pacer/src/async-rate-limiter/index.ts b/packages/react-pacer/src/async-rate-limiter/index.ts index f10072171..14a0cc7e7 100644 --- a/packages/react-pacer/src/async-rate-limiter/index.ts +++ b/packages/react-pacer/src/async-rate-limiter/index.ts @@ -2,3 +2,4 @@ export * from '@tanstack/pacer/async-rate-limiter' export * from './useAsyncRateLimiter' +export * from './useAsyncRateLimitedCallback' diff --git a/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimitedCallback.ts b/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimitedCallback.ts new file mode 100644 index 000000000..94947b05a --- /dev/null +++ b/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimitedCallback.ts @@ -0,0 +1,79 @@ +import { useCallback } from 'react' +import { useAsyncRateLimiter } from './useAsyncRateLimiter' +import type { AnyAsyncFunction } from '@tanstack/pacer/types' +import type { + AsyncRateLimiterOptions, + AsyncRateLimiterState, +} from '@tanstack/pacer/async-rate-limiter' + +/** + * A React hook that creates a rate-limited version of an async callback function. + * This hook is a convenient wrapper around the `useAsyncRateLimiter` hook, + * providing a stable, async rate-limited function reference for use in React components. + * + * Async rate limiting is a "hard limit" approach for async functions: it allows all calls + * until the limit is reached, then blocks (rejects) subsequent calls until the window resets. + * Unlike throttling or debouncing, it does not attempt to space out or collapse calls. + * This can lead to bursts of rapid executions followed by periods where all calls are blocked. + * + * The async rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All executions within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more + * consistent rate of execution over time. + * + * For smoother execution patterns, consider: + * - useAsyncThrottledCallback: When you want consistent spacing between executions (e.g. UI updates) + * - useAsyncDebouncedCallback: When you want to collapse rapid calls into a single execution (e.g. search input) + * + * Async rate limiting should primarily be used when you need to enforce strict limits + * on async operations, like API rate limits or other scenarios requiring hard caps + * on execution frequency. + * + * This hook provides a simpler API compared to `useAsyncRateLimiter`, making it ideal for basic + * async rate limiting needs. However, it does not expose the underlying AsyncRateLimiter instance. + * + * For advanced usage requiring features like: + * - Manual cancellation + * - Access to execution counts + * - Custom useCallback dependencies + * + * Consider using the `useAsyncRateLimiter` hook instead. + * + * @example + * ```tsx + * // Rate limit async API calls to maximum 5 calls per minute with a sliding window + * const makeApiCall = useAsyncRateLimitedCallback( + * async (data: ApiData) => { + * return fetch('/api/endpoint', { method: 'POST', body: JSON.stringify(data) }); + * }, + * { + * limit: 5, + * window: 60000, // 1 minute + * windowType: 'sliding', + * onReject: () => { + * console.warn('API rate limit reached. Please wait before trying again.'); + * } + * } + * ); + * ``` + */ +export function useAsyncRateLimitedCallback< + TFn extends AnyAsyncFunction, + TSelected = {}, +>( + fn: TFn, + options: AsyncRateLimiterOptions, + selector: (state: AsyncRateLimiterState) => TSelected = () => + ({}) as TSelected, +): (...args: Parameters) => Promise> { + const asyncRateLimitedFn = useAsyncRateLimiter( + fn, + options, + selector, + ).maybeExecute + return useCallback( + (...args) => asyncRateLimitedFn(...args) as Promise>, + [asyncRateLimitedFn], + ) +} diff --git a/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts b/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts index 5881db3e8..6b6578079 100644 --- a/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts +++ b/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts @@ -1,8 +1,23 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import { AsyncRateLimiter } from '@tanstack/pacer/async-rate-limiter' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { useStore } from '@tanstack/react-store' import type { AnyAsyncFunction } from '@tanstack/pacer/types' -import type { AsyncRateLimiterOptions } from '@tanstack/pacer/async-rate-limiter' +import type { + AsyncRateLimiterOptions, + AsyncRateLimiterState, +} from '@tanstack/pacer/async-rate-limiter' + +export interface ReactAsyncRateLimiter< + TFn extends AnyAsyncFunction, + TSelected = AsyncRateLimiterState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the rate limiter state changes + * + * Use this instead of `rateLimiter.store.state` + */ + readonly state: Readonly +} /** * A low-level React hook that creates an `AsyncRateLimiter` instance to limit how many times an async function can execute within a time window. @@ -64,15 +79,28 @@ import type { AsyncRateLimiterOptions } from '@tanstack/pacer/async-rate-limiter * ); * ``` */ -export function useAsyncRateLimiter( +export function useAsyncRateLimiter< + TFn extends AnyAsyncFunction, + TSelected = AsyncRateLimiterState, +>( fn: TFn, options: AsyncRateLimiterOptions, -): AsyncRateLimiter { - const [asyncRateLimiter] = useState(() => - bindInstanceMethods(new AsyncRateLimiter(fn, options)), + selector?: (state: AsyncRateLimiterState) => TSelected, +): ReactAsyncRateLimiter { + const [asyncRateLimiter] = useState( + () => new AsyncRateLimiter(fn, options), ) + const state = useStore(asyncRateLimiter.store, selector) + asyncRateLimiter.setOptions(options) - return asyncRateLimiter + return useMemo( + () => + ({ + ...asyncRateLimiter, + state, + }) as unknown as ReactAsyncRateLimiter, // omit `store` in favor of `state` + [asyncRateLimiter, state], + ) } diff --git a/packages/react-pacer/src/async-throttler/index.ts b/packages/react-pacer/src/async-throttler/index.ts index 284251dac..f557de642 100644 --- a/packages/react-pacer/src/async-throttler/index.ts +++ b/packages/react-pacer/src/async-throttler/index.ts @@ -2,3 +2,4 @@ export * from '@tanstack/pacer/async-throttler' export * from './useAsyncThrottler' +export * from './useAsyncThrottledCallback' diff --git a/packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts b/packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts new file mode 100644 index 000000000..3d7021f82 --- /dev/null +++ b/packages/react-pacer/src/async-throttler/useAsyncThrottledCallback.ts @@ -0,0 +1,58 @@ +import { useCallback } from 'react' +import { useAsyncThrottler } from './useAsyncThrottler' +import type { + AsyncThrottlerOptions, + AsyncThrottlerState, +} from '@tanstack/pacer/async-throttler' +import type { AnyAsyncFunction } from '@tanstack/pacer/types' + +/** + * A React hook that creates a throttled version of an async callback function. + * This hook is a convenient wrapper around the `useAsyncThrottler` hook, + * providing a stable, throttled async function reference for use in React components. + * + * The throttled async function will execute at most once within the specified wait time period, + * regardless of how many times it is called. If called multiple times during the wait period, + * only the first invocation will execute, and subsequent calls will be ignored until + * the wait period has elapsed. The returned function always returns a promise + * that resolves or rejects with the result of the original async function. + * + * This hook provides a simpler API compared to `useAsyncThrottler`, making it ideal for basic + * async throttling needs. However, it does not expose the underlying AsyncThrottler instance. + * + * For advanced usage requiring features like: + * - Manual cancellation + * - Access to execution/error state + * - Custom useCallback dependencies + * + * Consider using the `useAsyncThrottler` hook instead. + * + * @example + * ```tsx + * // Throttle an async API call + * const handleApiCall = useAsyncThrottledCallback(async (data) => { + * const result = await sendDataToServer(data); + * return result; + * }, { + * wait: 200 // Execute at most once every 200ms + * }); + * + * // Use in an event handler + * + * ``` + */ +export function useAsyncThrottledCallback< + TFn extends AnyAsyncFunction, + TSelected = {}, +>( + fn: TFn, + options: AsyncThrottlerOptions, + selector: (state: AsyncThrottlerState) => TSelected = () => + ({}) as TSelected, +): (...args: Parameters) => Promise> { + const asyncThrottledFn = useAsyncThrottler(fn, options, selector).maybeExecute + return useCallback( + (...args) => asyncThrottledFn(...args) as Promise>, + [asyncThrottledFn], + ) +} diff --git a/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts b/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts index 4243332ef..4331b1247 100644 --- a/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts +++ b/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts @@ -1,8 +1,23 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { AsyncThrottler } from '@tanstack/pacer/async-throttler' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { useStore } from '@tanstack/react-store' import type { AnyAsyncFunction } from '@tanstack/pacer/types' -import type { AsyncThrottlerOptions } from '@tanstack/pacer/async-throttler' +import type { + AsyncThrottlerOptions, + AsyncThrottlerState, +} from '@tanstack/pacer/async-throttler' + +export interface ReactAsyncThrottler< + TFn extends AnyAsyncFunction, + TSelected = AsyncThrottlerState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the debouncer state changes + * + * Use this instead of `debouncer.store.state` + */ + readonly state: Readonly +} /** * A low-level React hook that creates an `AsyncThrottler` instance to limit how often an async function can execute. @@ -52,19 +67,27 @@ import type { AsyncThrottlerOptions } from '@tanstack/pacer/async-throttler' * ); * ``` */ -export function useAsyncThrottler( +export function useAsyncThrottler< + TFn extends AnyAsyncFunction, + TSelected = AsyncThrottlerState, +>( fn: TFn, options: AsyncThrottlerOptions, -): AsyncThrottler { - const [asyncThrottler] = useState(() => - bindInstanceMethods(new AsyncThrottler(fn, options)), - ) + selector?: (state: AsyncThrottlerState) => TSelected, +): ReactAsyncThrottler { + const [asyncThrottler] = useState(() => new AsyncThrottler(fn, options)) - asyncThrottler.setOptions(options) + const state = useStore(asyncThrottler.store, selector) useEffect(() => { return () => asyncThrottler.cancel() }, [asyncThrottler]) - return asyncThrottler + return useMemo( + () => ({ + ...asyncThrottler, + state, + }), + [asyncThrottler, state], + ) } diff --git a/packages/react-pacer/src/batcher/useBatcher.ts b/packages/react-pacer/src/batcher/useBatcher.ts index 26eafd4ae..2d85d799b 100644 --- a/packages/react-pacer/src/batcher/useBatcher.ts +++ b/packages/react-pacer/src/batcher/useBatcher.ts @@ -1,7 +1,17 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import { Batcher } from '@tanstack/pacer/batcher' -import { bindInstanceMethods } from '@tanstack/pacer/utils' -import type { BatcherOptions } from '@tanstack/pacer/batcher' +import { useStore } from '@tanstack/react-store' +import type { BatcherOptions, BatcherState } from '@tanstack/pacer/batcher' + +export interface ReactBatcher> + extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the batcher state changes + * + * Use this instead of `batcher.store.state` + */ + readonly state: Readonly +} /** * A React hook that creates and manages a Batcher instance. @@ -40,15 +50,22 @@ import type { BatcherOptions } from '@tanstack/pacer/batcher' * batcher.start(); // Resume batching * ``` */ -export function useBatcher( +export function useBatcher>( fn: (items: Array) => void, options: BatcherOptions = {}, -): Batcher { - const [batcher] = useState(() => - bindInstanceMethods(new Batcher(fn, options)), - ) + selector?: (state: BatcherState) => TSelected, +): ReactBatcher { + const [batcher] = useState(() => new Batcher(fn, options)) + + const state = useStore(batcher.store, selector) batcher.setOptions(options) - return batcher + return useMemo( + () => ({ + ...batcher, + state, + }), + [batcher, state], + ) } diff --git a/packages/react-pacer/src/compare/index.ts b/packages/react-pacer/src/compare/index.ts deleted file mode 100644 index c33052fd9..000000000 --- a/packages/react-pacer/src/compare/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@tanstack/pacer/compare' diff --git a/packages/react-pacer/src/debouncer/useDebouncedCallback.ts b/packages/react-pacer/src/debouncer/useDebouncedCallback.ts index 01047a989..673d6ef86 100644 --- a/packages/react-pacer/src/debouncer/useDebouncedCallback.ts +++ b/packages/react-pacer/src/debouncer/useDebouncedCallback.ts @@ -1,6 +1,9 @@ import { useCallback } from 'react' import { useDebouncer } from './useDebouncer' -import type { DebouncerOptions } from '@tanstack/pacer/debouncer' +import type { + DebouncerOptions, + DebouncerState, +} from '@tanstack/pacer/debouncer' import type { AnyFunction } from '@tanstack/pacer/types' /** @@ -39,13 +42,11 @@ import type { AnyFunction } from '@tanstack/pacer/types' * /> * ``` */ -export function useDebouncedCallback( +export function useDebouncedCallback( fn: TFn, options: DebouncerOptions, -) { - const debouncedFn = useDebouncer(fn, options).maybeExecute - return useCallback( - (...args: Parameters) => debouncedFn(...args), - [debouncedFn], - ) + selector: (state: DebouncerState) => TSelected = () => ({}) as TSelected, +): (...args: Parameters) => void { + const debouncedFn = useDebouncer(fn, options, selector).maybeExecute + return useCallback((...args) => debouncedFn(...args), [debouncedFn]) } diff --git a/packages/react-pacer/src/debouncer/useDebouncedState.ts b/packages/react-pacer/src/debouncer/useDebouncedState.ts index 24ac87a0d..dcae588e6 100644 --- a/packages/react-pacer/src/debouncer/useDebouncedState.ts +++ b/packages/react-pacer/src/debouncer/useDebouncedState.ts @@ -1,6 +1,10 @@ import { useState } from 'react' import { useDebouncer } from './useDebouncer' -import type { Debouncer, DebouncerOptions } from '@tanstack/pacer/debouncer' +import type { ReactDebouncer } from './useDebouncer' +import type { + DebouncerOptions, + DebouncerState, +} from '@tanstack/pacer/debouncer' /** * A React hook that creates a debounced state value, combining React's useState with debouncing functionality. @@ -32,18 +36,24 @@ import type { Debouncer, DebouncerOptions } from '@tanstack/pacer/debouncer' * const executionCount = debouncer.getExecutionCount(); * * // Get the pending state - * const isPending = debouncer.getIsPending(); + * const isPending = debouncer.getState().isPending; * ``` */ -export function useDebouncedState( +export function useDebouncedState< + TValue, + TSelected = DebouncerState>>, +>( value: TValue, options: DebouncerOptions>>, + selector?: ( + state: DebouncerState>>, + ) => TSelected, ): [ TValue, React.Dispatch>, - Debouncer>>, + ReactDebouncer>, TSelected>, ] { - const [debouncedValue, setDebouncedValue] = useState(value) - const debouncer = useDebouncer(setDebouncedValue, options) + const [debouncedValue, setDebouncedValue] = useState(value) + const debouncer = useDebouncer(setDebouncedValue, options, selector) return [debouncedValue, debouncer.maybeExecute, debouncer] } diff --git a/packages/react-pacer/src/debouncer/useDebouncedValue.ts b/packages/react-pacer/src/debouncer/useDebouncedValue.ts index 8c37cface..5504b46d8 100644 --- a/packages/react-pacer/src/debouncer/useDebouncedValue.ts +++ b/packages/react-pacer/src/debouncer/useDebouncedValue.ts @@ -1,6 +1,10 @@ import { useEffect } from 'react' import { useDebouncedState } from './useDebouncedState' -import type { Debouncer, DebouncerOptions } from '@tanstack/pacer/debouncer' +import type { ReactDebouncer } from './useDebouncer' +import type { + DebouncerOptions, + DebouncerState, +} from '@tanstack/pacer/debouncer' /** * A React hook that creates a debounced value that updates only after a specified delay. @@ -38,13 +42,23 @@ import type { Debouncer, DebouncerOptions } from '@tanstack/pacer/debouncer' * }; * ``` */ -export function useDebouncedValue( +export function useDebouncedValue< + TValue, + TSelected = DebouncerState>>, +>( value: TValue, options: DebouncerOptions>>, -): [TValue, Debouncer>>] { + selector?: ( + state: DebouncerState>>, + ) => TSelected, +): [ + TValue, + ReactDebouncer>, TSelected>, +] { const [debouncedValue, setDebouncedValue, debouncer] = useDebouncedState( value, options, + selector, ) useEffect(() => { diff --git a/packages/react-pacer/src/debouncer/useDebouncer.ts b/packages/react-pacer/src/debouncer/useDebouncer.ts index 81bc3c56c..a68dbe102 100644 --- a/packages/react-pacer/src/debouncer/useDebouncer.ts +++ b/packages/react-pacer/src/debouncer/useDebouncer.ts @@ -1,9 +1,24 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Debouncer } from '@tanstack/pacer/debouncer' -import { bindInstanceMethods } from '@tanstack/pacer/utils' -import type { DebouncerOptions } from '@tanstack/pacer/debouncer' +import { useStore } from '@tanstack/react-store' +import type { + DebouncerOptions, + DebouncerState, +} from '@tanstack/pacer/debouncer' import type { AnyFunction } from '@tanstack/pacer/types' +export interface ReactDebouncer< + TFn extends AnyFunction, + TSelected = DebouncerState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the debouncer state changes + * + * Use this instead of `debouncer.store.state` + */ + readonly state: Readonly +} + /** * A React hook that creates and manages a Debouncer instance. * @@ -36,16 +51,20 @@ import type { AnyFunction } from '@tanstack/pacer/types' * const executionCount = searchDebouncer.getExecutionCount(); * * // Get the pending state - * const isPending = searchDebouncer.getIsPending(); + * const isPending = searchdebouncer.getState().isPending; * ``` */ -export function useDebouncer( +export function useDebouncer< + TFn extends AnyFunction, + TSelected = DebouncerState, +>( fn: TFn, options: DebouncerOptions, -): Debouncer { - const [debouncer] = useState(() => - bindInstanceMethods(new Debouncer(fn, options)), - ) + selector?: (state: DebouncerState) => TSelected, +): ReactDebouncer { + const [debouncer] = useState(() => new Debouncer(fn, options)) + + const state = useStore(debouncer.store, selector) debouncer.setOptions(options) @@ -55,5 +74,12 @@ export function useDebouncer( } }, [debouncer]) - return debouncer + return useMemo( + () => + ({ + ...debouncer, + state, + }) as unknown as ReactDebouncer, // omit `store` in favor of `state` + [debouncer, state], + ) } diff --git a/packages/react-pacer/src/index.ts b/packages/react-pacer/src/index.ts index f1c4a7d1b..790ad0da5 100644 --- a/packages/react-pacer/src/index.ts +++ b/packages/react-pacer/src/index.ts @@ -6,10 +6,11 @@ export * from '@tanstack/pacer' */ // async-batcher -// export * from './async-batcher/useAsyncBatcher' +export * from './async-batcher/useAsyncBatcher' // async-debouncer export * from './async-debouncer/useAsyncDebouncer' +export * from './async-debouncer/useAsyncDebouncedCallback' // async-queuer export * from './async-queuer/useAsyncQueuer' @@ -17,9 +18,11 @@ export * from './async-queuer/useAsyncQueuedState' // async-rate-limiter export * from './async-rate-limiter/useAsyncRateLimiter' +export * from './async-rate-limiter/useAsyncRateLimitedCallback' // async-throttler export * from './async-throttler/useAsyncThrottler' +export * from './async-throttler/useAsyncThrottledCallback' // batcher export * from './batcher/useBatcher' diff --git a/packages/react-pacer/src/queuer/useQueuedState.ts b/packages/react-pacer/src/queuer/useQueuedState.ts index 66e23b5ea..bfdb4b335 100644 --- a/packages/react-pacer/src/queuer/useQueuedState.ts +++ b/packages/react-pacer/src/queuer/useQueuedState.ts @@ -1,6 +1,6 @@ -import { useState } from 'react' import { useQueuer } from './useQueuer' -import type { Queuer, QueuerOptions } from '@tanstack/pacer/queuer' +import type { ReactQueuer } from './useQueuer' +import type { Queuer, QueuerOptions, QueuerState } from '@tanstack/pacer/queuer' /** * A React hook that creates a queuer with managed state, combining React's useState with queuing functionality. @@ -51,25 +51,15 @@ import type { Queuer, QueuerOptions } from '@tanstack/pacer/queuer' * }; * ``` */ -export function useQueuedState( +export function useQueuedState< + TValue, + TSelected extends Pick, 'items'> = QueuerState, +>( fn: (item: TValue) => void, options: QueuerOptions = {}, -): [Array, Queuer['addItem'], Queuer] { - const [allItems, setAllItems] = useState>( - options.initialItems || [], - ) + selector?: (state: QueuerState) => TSelected, +): [Array, Queuer['addItem'], ReactQueuer] { + const queue = useQueuer(fn, options, selector) - const queue = useQueuer(fn, { - ...options, - onItemsChange: (queue) => { - setAllItems(queue.peekAllItems()) - options.onItemsChange?.(queue) - }, - onIsRunningChange: (queue) => { - setAllItems((prev) => [...prev]) // rerender - options.onIsRunningChange?.(queue) - }, - }) - - return [allItems, queue.addItem, queue] + return [queue.state.items, queue.addItem, queue] } diff --git a/packages/react-pacer/src/queuer/useQueuedValue.ts b/packages/react-pacer/src/queuer/useQueuedValue.ts index 2dae097a1..d4c08dcc2 100644 --- a/packages/react-pacer/src/queuer/useQueuedValue.ts +++ b/packages/react-pacer/src/queuer/useQueuedValue.ts @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react' import { useQueuedState } from './useQueuedState' -import type { Queuer, QueuerOptions } from '@tanstack/pacer/queuer' +import type { ReactQueuer } from './useQueuer' +import type { QueuerOptions, QueuerState } from '@tanstack/pacer/queuer' /** * A React hook that creates a queued value that processes state changes in order with an optional delay. @@ -37,20 +38,22 @@ import type { Queuer, QueuerOptions } from '@tanstack/pacer/queuer' * }; * ``` */ -export function useQueuedValue( +export function useQueuedValue< + TValue, + TSelected extends Pick, 'items'> = QueuerState, +>( initialValue: TValue, options: QueuerOptions = {}, -): [TValue, Queuer] { + selector?: (state: QueuerState) => TSelected, +): [TValue, ReactQueuer] { const [value, setValue] = useState(initialValue) - const [, addItem, queuer] = useQueuedState( + const [, addItem, queuer] = useQueuedState( (item) => { setValue(item) }, - { - started: true, - ...options, - }, + options, + selector, ) useEffect(() => { diff --git a/packages/react-pacer/src/queuer/useQueuer.ts b/packages/react-pacer/src/queuer/useQueuer.ts index 2f421ffcc..690d2b2ec 100644 --- a/packages/react-pacer/src/queuer/useQueuer.ts +++ b/packages/react-pacer/src/queuer/useQueuer.ts @@ -1,7 +1,17 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import { Queuer } from '@tanstack/pacer/queuer' -import { bindInstanceMethods } from '@tanstack/pacer/utils' -import type { QueuerOptions } from '@tanstack/pacer/queuer' +import { useStore } from '@tanstack/react-store' +import type { QueuerOptions, QueuerState } from '@tanstack/pacer/queuer' + +export interface ReactQueuer> + extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the queuer state changes + * + * Use this instead of `queuer.store.state` + */ + readonly state: Readonly +} /** * A React hook that creates and manages a Queuer instance. @@ -41,15 +51,23 @@ import type { QueuerOptions } from '@tanstack/pacer/queuer' * queue.start(); // Resume processing * ``` */ -export function useQueuer( +export function useQueuer>( fn: (item: TValue) => void, options: QueuerOptions = {}, -): Queuer { - const [queuer] = useState(() => - bindInstanceMethods(new Queuer(fn, options)), - ) + selector?: (state: QueuerState) => TSelected, +): ReactQueuer { + const [queuer] = useState(() => new Queuer(fn, options)) + + const state = useStore(queuer.store, selector) queuer.setOptions(options) - return queuer + return useMemo( + () => + ({ + ...queuer, + state, + }) as unknown as ReactQueuer, // omit `store` in favor of `state` + [queuer, state], + ) } diff --git a/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts b/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts index c5514cdb6..63f9a99ba 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts @@ -1,7 +1,10 @@ import { useCallback } from 'react' import { useRateLimiter } from './useRateLimiter' import type { AnyFunction } from '@tanstack/pacer/types' -import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' +import type { + RateLimiterOptions, + RateLimiterState, +} from '@tanstack/pacer/rate-limiter' /** * A React hook that creates a rate-limited version of a callback function. @@ -56,13 +59,11 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * ); * ``` */ -export function useRateLimitedCallback< - TFn extends AnyFunction, - TArgs extends Parameters, ->(fn: TFn, options: RateLimiterOptions) { - const rateLimitedFn = useRateLimiter(fn, options).maybeExecute - return useCallback( - (...args: TArgs) => rateLimitedFn(...args), - [rateLimitedFn], - ) +export function useRateLimitedCallback( + fn: TFn, + options: RateLimiterOptions, + selector: (state: RateLimiterState) => TSelected = () => ({}) as TSelected, +): (...args: Parameters) => boolean { + const rateLimitedFn = useRateLimiter(fn, options, selector).maybeExecute + return useCallback((...args) => rateLimitedFn(...args), [rateLimitedFn]) } diff --git a/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts b/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts index f59058734..473d2d7f6 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts @@ -1,8 +1,9 @@ import { useState } from 'react' import { useRateLimiter } from './useRateLimiter' +import type { ReactRateLimiter } from './useRateLimiter' import type { - RateLimiter, RateLimiterOptions, + RateLimiterState, } from '@tanstack/pacer/rate-limiter' /** @@ -63,15 +64,16 @@ import type { * }; * ``` */ -export function useRateLimitedState( +export function useRateLimitedState( value: TValue, options: RateLimiterOptions>>, + selector?: (state: RateLimiterState) => TSelected, ): [ TValue, React.Dispatch>, - RateLimiter>>, + ReactRateLimiter>, TSelected>, ] { - const [rateLimitedValue, setRateLimitedValue] = useState(value) - const rateLimiter = useRateLimiter(setRateLimitedValue, options) + const [rateLimitedValue, setRateLimitedValue] = useState(value) + const rateLimiter = useRateLimiter(setRateLimitedValue, options, selector) return [rateLimitedValue, rateLimiter.maybeExecute, rateLimiter] } diff --git a/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts b/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts index 4cc9d50a9..956694d60 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts @@ -1,8 +1,9 @@ import { useEffect } from 'react' import { useRateLimitedState } from './useRateLimitedState' +import type { ReactRateLimiter } from './useRateLimiter' import type { - RateLimiter, RateLimiterOptions, + RateLimiterState, } from '@tanstack/pacer/rate-limiter' /** @@ -52,12 +53,16 @@ import type { * }); * ``` */ -export function useRateLimitedValue( +export function useRateLimitedValue( value: TValue, options: RateLimiterOptions>>, -): [TValue, RateLimiter>>] { + selector?: (state: RateLimiterState) => TSelected, +): [ + TValue, + ReactRateLimiter>, TSelected>, +] { const [rateLimitedValue, setRateLimitedValue, rateLimiter] = - useRateLimitedState(value, options) + useRateLimitedState(value, options, selector) useEffect(() => { setRateLimitedValue(value) diff --git a/packages/react-pacer/src/rate-limiter/useRateLimiter.ts b/packages/react-pacer/src/rate-limiter/useRateLimiter.ts index 488f012cd..6732f259f 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimiter.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimiter.ts @@ -1,9 +1,24 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import { RateLimiter } from '@tanstack/pacer/rate-limiter' -import { bindInstanceMethods } from '@tanstack/pacer/utils' -import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' +import { useStore } from '@tanstack/react-store' +import type { + RateLimiterOptions, + RateLimiterState, +} from '@tanstack/pacer/rate-limiter' import type { AnyFunction } from '@tanstack/pacer/types' +export interface ReactRateLimiter< + TFn extends AnyFunction, + TSelected = RateLimiterState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the rate limiter state changes + * + * Use this instead of `rateLimiter.store.state` + */ + readonly state: Readonly +} + /** * A low-level React hook that creates a `RateLimiter` instance to enforce rate limits on function execution. * @@ -52,15 +67,26 @@ import type { AnyFunction } from '@tanstack/pacer/types' * }; * ``` */ -export function useRateLimiter( +export function useRateLimiter< + TFn extends AnyFunction, + TSelected = RateLimiterState, +>( fn: TFn, options: RateLimiterOptions, -): RateLimiter { - const [rateLimiter] = useState(() => - bindInstanceMethods(new RateLimiter(fn, options)), - ) + selector?: (state: RateLimiterState) => TSelected, +): ReactRateLimiter { + const [rateLimiter] = useState(() => new RateLimiter(fn, options)) + + const state = useStore(rateLimiter.store, selector) rateLimiter.setOptions(options) - return rateLimiter + return useMemo( + () => + ({ + ...rateLimiter, + state, + }) as ReactRateLimiter, // omit `store` in favor of `state` + [rateLimiter, state], + ) } diff --git a/packages/react-pacer/src/throttler/useThrottledCallback.ts b/packages/react-pacer/src/throttler/useThrottledCallback.ts index d0395ea76..de6175612 100644 --- a/packages/react-pacer/src/throttler/useThrottledCallback.ts +++ b/packages/react-pacer/src/throttler/useThrottledCallback.ts @@ -1,6 +1,9 @@ import { useCallback } from 'react' import { useThrottler } from './useThrottler' -import type { ThrottlerOptions } from '@tanstack/pacer/throttler' +import type { + ThrottlerOptions, + ThrottlerState, +} from '@tanstack/pacer/throttler' import type { AnyFunction } from '@tanstack/pacer/types' /** @@ -40,10 +43,11 @@ import type { AnyFunction } from '@tanstack/pacer/types' * }, [handleResize]); * ``` */ -export function useThrottledCallback< - TFn extends AnyFunction, - TArgs extends Parameters, ->(fn: TFn, options: ThrottlerOptions) { - const throttledFn = useThrottler(fn, options).maybeExecute - return useCallback((...args: TArgs) => throttledFn(...args), [throttledFn]) +export function useThrottledCallback( + fn: TFn, + options: ThrottlerOptions, + selector: (state: ThrottlerState) => TSelected = () => ({}) as TSelected, +): (...args: Parameters) => void { + const throttledFn = useThrottler(fn, options, selector).maybeExecute + return useCallback((...args) => throttledFn(...args), [throttledFn]) } diff --git a/packages/react-pacer/src/throttler/useThrottledState.ts b/packages/react-pacer/src/throttler/useThrottledState.ts index 78d0e6b39..ad9c6f716 100644 --- a/packages/react-pacer/src/throttler/useThrottledState.ts +++ b/packages/react-pacer/src/throttler/useThrottledState.ts @@ -1,6 +1,10 @@ import { useState } from 'react' import { useThrottler } from './useThrottler' -import type { Throttler, ThrottlerOptions } from '@tanstack/pacer/throttler' +import type { ReactThrottler } from './useThrottler' +import type { + ThrottlerOptions, + ThrottlerState, +} from '@tanstack/pacer/throttler' /** * A React hook that creates a throttled state value that updates at most once within a specified time window. @@ -37,15 +41,21 @@ import type { Throttler, ThrottlerOptions } from '@tanstack/pacer/throttler' * ``` */ -export function useThrottledState( +export function useThrottledState< + TValue, + TSelected = ThrottlerState>>, +>( value: TValue, options: ThrottlerOptions>>, + selector?: ( + state: ThrottlerState>>, + ) => TSelected, ): [ TValue, React.Dispatch>, - Throttler>>, + ReactThrottler>, TSelected>, ] { const [throttledValue, setThrottledValue] = useState(value) - const throttler = useThrottler(setThrottledValue, options) + const throttler = useThrottler(setThrottledValue, options, selector) return [throttledValue, throttler.maybeExecute, throttler] } diff --git a/packages/react-pacer/src/throttler/useThrottledValue.ts b/packages/react-pacer/src/throttler/useThrottledValue.ts index 30f6be8ab..72694b6cf 100644 --- a/packages/react-pacer/src/throttler/useThrottledValue.ts +++ b/packages/react-pacer/src/throttler/useThrottledValue.ts @@ -1,6 +1,10 @@ import { useEffect } from 'react' import { useThrottledState } from './useThrottledState' -import type { Throttler, ThrottlerOptions } from '@tanstack/pacer/throttler' +import type { ReactThrottler } from './useThrottler' +import type { + ThrottlerOptions, + ThrottlerState, +} from '@tanstack/pacer/throttler' /** * A high-level React hook that creates a throttled version of a value that updates at most once within a specified time window. @@ -29,13 +33,23 @@ import type { Throttler, ThrottlerOptions } from '@tanstack/pacer/throttler' * }); * ``` */ -export function useThrottledValue( +export function useThrottledValue< + TValue, + TSelected = ThrottlerState>>, +>( value: TValue, options: ThrottlerOptions>>, -): [TValue, Throttler>>] { + selector?: ( + state: ThrottlerState>>, + ) => TSelected, +): [ + TValue, + ReactThrottler>, TSelected>, +] { const [throttledValue, setThrottledValue, throttler] = useThrottledState( value, options, + selector, ) useEffect(() => { diff --git a/packages/react-pacer/src/throttler/useThrottler.ts b/packages/react-pacer/src/throttler/useThrottler.ts index cfefb081b..53dc33daa 100644 --- a/packages/react-pacer/src/throttler/useThrottler.ts +++ b/packages/react-pacer/src/throttler/useThrottler.ts @@ -1,8 +1,23 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Throttler } from '@tanstack/pacer/throttler' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { useStore } from '@tanstack/react-store' import type { AnyFunction } from '@tanstack/pacer/types' -import type { ThrottlerOptions } from '@tanstack/pacer/throttler' +import type { + ThrottlerOptions, + ThrottlerState, +} from '@tanstack/pacer/throttler' + +export interface ReactThrottler< + TFn extends AnyFunction, + TSelected = ThrottlerState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the throttler state changes + * + * Use this instead of `throttler.store.state` + */ + readonly state: Readonly +} /** * A low-level React hook that creates a `Throttler` instance that limits how often the provided function can execute. @@ -21,13 +36,6 @@ import type { ThrottlerOptions } from '@tanstack/pacer/throttler' * const [value, setValue] = useState(0); * const throttler = useThrottler(setValue, { wait: 1000 }); * - * // With Redux - * const dispatch = useDispatch(); - * const throttler = useThrottler( - * (value) => dispatch(updateAction(value)), - * { wait: 1000 } - * ); - * * // With any state manager * const throttler = useThrottler( * (value) => stateManager.setState(value), @@ -39,13 +47,17 @@ import type { ThrottlerOptions } from '@tanstack/pacer/throttler' * ); * ``` */ -export function useThrottler( +export function useThrottler< + TFn extends AnyFunction, + TSelected = ThrottlerState, +>( fn: TFn, options: ThrottlerOptions, -): Throttler { - const [throttler] = useState(() => - bindInstanceMethods(new Throttler(fn, options)), - ) + selector?: (state: ThrottlerState) => TSelected, +): ReactThrottler { + const [throttler] = useState(() => new Throttler(fn, options)) + + const state = useStore(throttler.store, selector) throttler.setOptions(options) @@ -55,5 +67,12 @@ export function useThrottler( } }, [throttler]) - return throttler + return useMemo( + () => + ({ + ...throttler, + state, + }) as ReactThrottler, // omit `store` in favor of `state` + [throttler, state], + ) } diff --git a/packages/react-pacer/vite.config.ts b/packages/react-pacer/vite.config.ts index ed729bf51..2657f80b3 100644 --- a/packages/react-pacer/vite.config.ts +++ b/packages/react-pacer/vite.config.ts @@ -19,12 +19,12 @@ export default mergeConfig( config, tanstackViteConfig({ entry: [ + './src/async-batcher/index.ts', './src/async-debouncer/index.ts', './src/async-queuer/index.ts', './src/async-rate-limiter/index.ts', './src/async-throttler/index.ts', './src/batcher/index.ts', - './src/compare/index.ts', './src/debouncer/index.ts', './src/index.ts', './src/queuer/index.ts', diff --git a/packages/react-persister/CHANGELOG.md b/packages/react-persister/CHANGELOG.md new file mode 100644 index 000000000..1a138582a --- /dev/null +++ b/packages/react-persister/CHANGELOG.md @@ -0,0 +1 @@ +# @tanstack/react-persister diff --git a/packages/react-persister/eslint.config.js b/packages/react-persister/eslint.config.js new file mode 100644 index 000000000..3b8dc1504 --- /dev/null +++ b/packages/react-persister/eslint.config.js @@ -0,0 +1,32 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactCompiler from 'eslint-plugin-react-compiler' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + files: ['**/*.{ts,tsx}'], + ...pluginReact.configs.recommended, + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + 'react-compiler': pluginReactCompiler, + }, + rules: { + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-compiler/react-compiler': 'error', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + // 'react-compiler/react-compiler': 'off', + }, + }, +] diff --git a/packages/react-persister/package.json b/packages/react-persister/package.json new file mode 100644 index 000000000..649a809aa --- /dev/null +++ b/packages/react-persister/package.json @@ -0,0 +1,119 @@ +{ + "name": "@tanstack/react-persister", + "version": "0.0.0", + "description": "Utilities for persisting state to local storage, session storage, indexedDB, and more.", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/pacer.git", + "directory": "packages/react-persister" + }, + "homepage": "https://tanstack.com/persister", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "react", + "persister", + "persist", + "storage", + "localstorage", + "sessionstorage", + "indexeddb" + ], + "type": "module", + "types": "dist/esm/index.d.ts", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./async-persister": { + "import": { + "types": "./dist/esm/async-persister/index.d.ts", + "default": "./dist/esm/async-persister/index.js" + }, + "require": { + "types": "./dist/cjs/async-persister/index.d.cts", + "default": "./dist/cjs/async-persister/index.cjs" + } + }, + "./persister": { + "import": { + "types": "./dist/esm/persister/index.d.ts", + "default": "./dist/esm/persister/index.js" + }, + "require": { + "types": "./dist/cjs/persister/index.d.cts", + "default": "./dist/cjs/persister/index.cjs" + } + }, + "./storage-persister": { + "import": { + "types": "./dist/esm/storage-persister/index.d.ts", + "default": "./dist/esm/storage-persister/index.js" + }, + "require": { + "types": "./dist/cjs/storage-persister/index.d.cts", + "default": "./dist/cjs/storage-persister/index.cjs" + } + }, + "./types": { + "types": "./dist/esm/types/index.d.ts", + "default": "./dist/esm/types/index.js" + }, + "./utils": { + "import": { + "types": "./dist/esm/utils/index.d.ts", + "default": "./dist/esm/utils/index.js" + }, + "require": { + "types": "./dist/cjs/utils/index.d.cts", + "default": "./dist/cjs/utils/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "clean": "premove ./build ./dist", + "test:eslint": "eslint ./src", + "test:lib": "vitest --passWithNoTests", + "test:lib:dev": "pnpm test:lib --watch", + "test:types": "tsc", + "test:build": "publint --strict", + "build": "vite build" + }, + "dependencies": { + "@tanstack/persister": "workspace:*" + }, + "devDependencies": { + "@eslint-react/eslint-plugin": "^1.52.2", + "@types/react": "^19.1.8", + "@vitejs/plugin-react": "^4.6.0", + "eslint-plugin-react-compiler": "19.1.0-rc.2", + "eslint-plugin-react-hooks": "^5.2.0", + "react": "^19.1.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } +} diff --git a/packages/react-persister/src/async-persister/index.ts b/packages/react-persister/src/async-persister/index.ts new file mode 100644 index 000000000..22fb4da1b --- /dev/null +++ b/packages/react-persister/src/async-persister/index.ts @@ -0,0 +1 @@ +export * from '@tanstack/persister/async-persister' diff --git a/packages/react-persister/src/compare/index.ts b/packages/react-persister/src/compare/index.ts new file mode 100644 index 000000000..7509b6895 --- /dev/null +++ b/packages/react-persister/src/compare/index.ts @@ -0,0 +1 @@ +export * from '@tanstack/persister/compare' diff --git a/packages/react-persister/src/index.ts b/packages/react-persister/src/index.ts new file mode 100644 index 000000000..80c3e3afc --- /dev/null +++ b/packages/react-persister/src/index.ts @@ -0,0 +1,12 @@ +// re-export everything from the core pacer package +export * from '@tanstack/persister' + +/** + * Export every hook individually - DON'T export from barrel files + */ + +// async-debouncer + +// persister +export * from './storage-persister/useStoragePersister' +export * from './storage-persister/useStorageState' diff --git a/packages/react-persister/src/persister/index.ts b/packages/react-persister/src/persister/index.ts new file mode 100644 index 000000000..358c27bd1 --- /dev/null +++ b/packages/react-persister/src/persister/index.ts @@ -0,0 +1 @@ +export * from '@tanstack/persister/persister' diff --git a/packages/react-persister/src/storage-persister/index.ts b/packages/react-persister/src/storage-persister/index.ts new file mode 100644 index 000000000..8065353cf --- /dev/null +++ b/packages/react-persister/src/storage-persister/index.ts @@ -0,0 +1,4 @@ +export * from '@tanstack/persister/storage-persister' + +export * from './useStoragePersister' +export * from './useStorageState' diff --git a/packages/react-persister/src/storage-persister/useStoragePersister.ts b/packages/react-persister/src/storage-persister/useStoragePersister.ts new file mode 100644 index 000000000..c4aecf534 --- /dev/null +++ b/packages/react-persister/src/storage-persister/useStoragePersister.ts @@ -0,0 +1,16 @@ +import { useState } from 'react' +import { StoragePersister } from '@tanstack/persister/storage-persister' +import type { StoragePersisterOptions } from '@tanstack/persister/storage-persister' + +export function useStoragePersister< + TState, + TSelected extends Partial = TState, +>( + options: StoragePersisterOptions, +): StoragePersister { + const [persister] = useState(() => new StoragePersister(options)) + + persister.setOptions(options) + + return persister +} diff --git a/packages/react-persister/src/storage-persister/useStorageState.ts b/packages/react-persister/src/storage-persister/useStorageState.ts new file mode 100644 index 000000000..3128a8aa2 --- /dev/null +++ b/packages/react-persister/src/storage-persister/useStorageState.ts @@ -0,0 +1,73 @@ +import { useEffect, useState } from 'react' +import { useStoragePersister } from './useStoragePersister' +import type { StoragePersisterOptions } from '@tanstack/persister/storage-persister' + +function useStorageState = TState>( + initialValue: TState, + options: StoragePersisterOptions, +) { + const persister = useStoragePersister(options) + + const [state, setState] = useState(initialValue) + + useEffect(() => { + // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect + setState(persister.loadState() ?? initialValue) + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + persister.saveState(state) + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state]) + + return [state, setState] as const +} + +/** + * A hook that persists state to localStorage and syncs it across tabs + * + * @example + * ```tsx + * const [value, setValue] = useLocalStorageState('my-key', 'initial value') + * ``` + */ +export function useLocalStorageState< + TValue, + TSelected extends Partial = TValue, +>( + key: string, + initialValue: TValue, + options?: Omit, 'key' | 'storage'>, +) { + return useStorageState(initialValue, { + ...options, + key, + storage: typeof window !== 'undefined' ? localStorage : null, + }) +} + +/** + * A hook that persists state to sessionStorage and syncs it across tabs + * + * @example + * ```tsx + * const [value, setValue] = useSessionStorageState('my-key', 'initial value') + * ``` + */ +export function useSessionStorageState< + TValue, + TSelected extends Partial = TValue, +>( + key: string, + initialValue: TValue, + options?: Omit, 'key' | 'storage'>, +) { + return useStorageState(initialValue, { + ...options, + key, + storage: typeof window !== 'undefined' ? sessionStorage : null, + }) +} diff --git a/packages/react-persister/src/types/index.ts b/packages/react-persister/src/types/index.ts new file mode 100644 index 000000000..294d1807e --- /dev/null +++ b/packages/react-persister/src/types/index.ts @@ -0,0 +1 @@ +export * from '@tanstack/persister/types' diff --git a/packages/react-persister/src/utils/index.ts b/packages/react-persister/src/utils/index.ts new file mode 100644 index 000000000..477d7b75a --- /dev/null +++ b/packages/react-persister/src/utils/index.ts @@ -0,0 +1 @@ +export * from '@tanstack/persister/utils' diff --git a/packages/react-persister/tsconfig.docs.json b/packages/react-persister/tsconfig.docs.json new file mode 100644 index 000000000..20ccb038c --- /dev/null +++ b/packages/react-persister/tsconfig.docs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@tanstack/pacer": ["../pacer/src"] + } + }, + "include": ["src"] +} diff --git a/packages/react-persister/tsconfig.json b/packages/react-persister/tsconfig.json new file mode 100644 index 000000000..84562bb4a --- /dev/null +++ b/packages/react-persister/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react" + }, + "include": ["src", "eslint.config.js", "vite.config.ts", "tests"] +} diff --git a/packages/react-persister/vite.config.ts b/packages/react-persister/vite.config.ts new file mode 100644 index 000000000..40249e7be --- /dev/null +++ b/packages/react-persister/vite.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import react from '@vitejs/plugin-react' +import packageJson from './package.json' + +const config = defineConfig({ + plugins: [react()], + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'jsdom', + // setupFiles: ['./tests/test-setup.ts'], + globals: true, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: [ + './src/async-persister/index.ts', + './src/compare/index.ts', + './src/index.ts', + './src/persister/index.ts', + './src/storage-persister/index.ts', + './src/types/index.ts', + './src/utils/index.ts', + ], + srcDir: './src', + }), +) diff --git a/packages/solid-pacer/package.json b/packages/solid-pacer/package.json index 82320aab1..f3e9c34bf 100644 --- a/packages/solid-pacer/package.json +++ b/packages/solid-pacer/package.json @@ -37,6 +37,16 @@ "default": "./dist/cjs/index.cjs" } }, + "./async-batcher": { + "import": { + "types": "./dist/esm/async-batcher/index.d.ts", + "default": "./dist/esm/async-batcher/index.js" + }, + "require": { + "types": "./dist/cjs/async-batcher/index.d.cts", + "default": "./dist/cjs/async-batcher/index.cjs" + } + }, "./async-debouncer": { "import": { "types": "./dist/esm/async-debouncer/index.d.ts", @@ -161,11 +171,12 @@ "build": "vite build" }, "dependencies": { - "@tanstack/pacer": "workspace:*" + "@tanstack/pacer": "workspace:*", + "@tanstack/solid-store": "^0.7.3" }, "devDependencies": { "solid-js": "^1.9.7", - "vite-plugin-solid": "^2.11.6" + "vite-plugin-solid": "^2.11.7" }, "peerDependencies": { "solid-js": ">=1.9.5" diff --git a/packages/solid-pacer/src/async-batcher/createAsyncBatcher.ts b/packages/solid-pacer/src/async-batcher/createAsyncBatcher.ts new file mode 100644 index 000000000..27f94dfcb --- /dev/null +++ b/packages/solid-pacer/src/async-batcher/createAsyncBatcher.ts @@ -0,0 +1,98 @@ +import { AsyncBatcher } from '@tanstack/pacer/async-batcher' +import { useStore } from '@tanstack/solid-store' +import type { Accessor } from 'solid-js' +import type { + AsyncBatcherOptions, + AsyncBatcherState, +} from '@tanstack/pacer/async-batcher' + +export interface SolidAsyncBatcher< + TValue, + TSelected = AsyncBatcherState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated when the batcher state changes + * + * Use this instead of `batcher.store.state` + */ + readonly state: Accessor> +} + +/** + * Creates a Solid-compatible AsyncBatcher instance for managing asynchronous batches of items, exposing Solid signals for all stateful properties. + * + * This is the async version of the createBatcher hook. Unlike the sync version, this async batcher: + * - Handles promises and returns results from batch executions + * - Provides error handling with configurable error behavior + * - Tracks success, error, and settle counts separately + * - Has state tracking for when batches are executing + * - Returns the result of the batch function execution + * + * Features: + * - Configurable batch size and wait time + * - Custom batch processing logic via getShouldExecute + * - Event callbacks for monitoring batch operations + * - Error handling for failed batch operations + * - Automatic or manual batch processing + * - All stateful properties (items, counts, etc.) are exposed as Solid signals for reactivity + * + * The batcher collects items and processes them in batches based on: + * - Maximum batch size (number of items per batch) + * - Time-based batching (process after X milliseconds) + * - Custom batch processing logic via getShouldExecute + * + * Error Handling: + * - If an `onError` handler is provided, it will be called with the error and batcher instance + * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown + * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed + * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown + * - The error state can be checked using the underlying AsyncBatcher instance + * + * Example usage: + * ```tsx + * // Basic async batcher for API requests + * const asyncBatcher = createAsyncBatcher( + * async (items) => { + * const results = await Promise.all(items.map(item => processItem(item))); + * return results; + * }, + * { + * maxSize: 10, + * wait: 2000, + * onSuccess: (result) => { + * console.log('Batch processed successfully:', result); + * }, + * onError: (error) => { + * console.error('Batch processing failed:', error); + * } + * } + * ); + * + * // Add items to batch + * asyncBatcher.addItem(newItem); + * + * // Manually execute batch + * const result = await asyncBatcher.execute(); + * + * // Use Solid signals in your UI + * const items = asyncBatcher.state().items; + * const isExecuting = asyncBatcher.state().isExecuting; + * ``` + */ +export function createAsyncBatcher< + TValue, + TSelected = AsyncBatcherState, +>( + fn: (items: Array) => Promise, + initialOptions: AsyncBatcherOptions = {}, + selector?: (state: AsyncBatcherState) => TSelected, +): SolidAsyncBatcher { + const asyncBatcher = new AsyncBatcher(fn, initialOptions) + + const state = useStore(asyncBatcher.store, selector) + + return { + ...asyncBatcher, + state, + } as unknown as SolidAsyncBatcher // omit `store` in favor of `state` +} diff --git a/packages/solid-pacer/src/async-batcher/index.ts b/packages/solid-pacer/src/async-batcher/index.ts new file mode 100644 index 000000000..7252cd9f2 --- /dev/null +++ b/packages/solid-pacer/src/async-batcher/index.ts @@ -0,0 +1,3 @@ +export * from '@tanstack/pacer/async-batcher' + +export * from './createAsyncBatcher' diff --git a/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts index 65255c922..53d756147 100644 --- a/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts +++ b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts @@ -1,24 +1,23 @@ import { AsyncDebouncer } from '@tanstack/pacer/async-debouncer' -import { createSignal } from 'solid-js' -import { bindInstanceMethods } from '@tanstack/pacer/utils' -import type { AsyncDebouncerOptions } from '@tanstack/pacer/async-debouncer' -import type { AnyAsyncFunction } from '@tanstack/pacer/types' +import { useStore } from '@tanstack/solid-store' +import { createEffect, onCleanup } from 'solid-js' import type { Accessor } from 'solid-js' +import type { + AsyncDebouncerOptions, + AsyncDebouncerState, +} from '@tanstack/pacer/async-debouncer' +import type { AnyAsyncFunction } from '@tanstack/pacer/types' -export interface SolidAsyncDebouncer - extends Omit< - AsyncDebouncer, - | 'getErrorCount' - | 'getIsPending' - | 'getLastResult' - | 'getSettleCount' - | 'getSuccessCount' - > { - errorCount: Accessor - isPending: Accessor - lastResult: Accessor | undefined> - settleCount: Accessor - successCount: Accessor +export interface SolidAsyncDebouncer< + TFn extends AnyAsyncFunction, + TSelected = AsyncDebouncerState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated when the debouncer state changes + * + * Use this instead of `debouncer.store.state` + */ + readonly state: Accessor> } /** @@ -74,50 +73,26 @@ export interface SolidAsyncDebouncer * ); * ``` */ -export function createAsyncDebouncer( +export function createAsyncDebouncer< + TFn extends AnyAsyncFunction, + TSelected = AsyncDebouncerState, +>( fn: TFn, initialOptions: AsyncDebouncerOptions, -): SolidAsyncDebouncer { + selector?: (state: AsyncDebouncerState) => TSelected, +): SolidAsyncDebouncer { const asyncDebouncer = new AsyncDebouncer(fn, initialOptions) - const [errorCount, setErrorCount] = createSignal( - asyncDebouncer.getErrorCount(), - ) - const [settleCount, setSettleCount] = createSignal( - asyncDebouncer.getSettleCount(), - ) - const [successCount, setSuccessCount] = createSignal( - asyncDebouncer.getSuccessCount(), - ) - const [isPending, setIsPending] = createSignal(asyncDebouncer.getIsPending()) - const [lastResult, setLastResult] = createSignal( - asyncDebouncer.getLastResult(), - ) + const state = useStore(asyncDebouncer.store, selector) - function setOptions(newOptions: Partial>) { - asyncDebouncer.setOptions({ - ...newOptions, - onSettled: (asyncDebouncer) => { - setSuccessCount(asyncDebouncer.getSuccessCount()) - setErrorCount(asyncDebouncer.getErrorCount()) - setSettleCount(asyncDebouncer.getSettleCount()) - setIsPending(asyncDebouncer.getIsPending()) - setLastResult(asyncDebouncer.getLastResult()) - const onSettled = newOptions.onSettled ?? initialOptions.onSettled - onSettled?.(asyncDebouncer) - }, + createEffect(() => { + onCleanup(() => { + asyncDebouncer.cancel() }) - } - - setOptions(initialOptions) + }) return { - ...bindInstanceMethods(asyncDebouncer), - errorCount, - isPending, - lastResult, - settleCount, - successCount, - setOptions, - } as SolidAsyncDebouncer + ...asyncDebouncer, + state, + } as unknown as SolidAsyncDebouncer // omit `store` in favor of `state` } diff --git a/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts b/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts index 598c7f557..796ee27cb 100644 --- a/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts +++ b/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts @@ -1,78 +1,19 @@ import { AsyncQueuer } from '@tanstack/pacer/async-queuer' -import { createSignal } from 'solid-js' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { useStore } from '@tanstack/solid-store' import type { Accessor } from 'solid-js' -import type { AsyncQueuerOptions } from '@tanstack/pacer/async-queuer' +import type { + AsyncQueuerOptions, + AsyncQueuerState, +} from '@tanstack/pacer/async-queuer' -export interface SolidAsyncQueuer - extends Omit< - AsyncQueuer, - | 'getErrorCount' - | 'getIsEmpty' - | 'getIsFull' - | 'getIsIdle' - | 'getIsRunning' - | 'getRejectionCount' - | 'getSettledCount' - | 'getSize' - | 'getSuccessCount' - | 'peekActiveItems' - | 'peekAllItems' - | 'peekNextItem' - | 'peekPendingItems' - > { +export interface SolidAsyncQueuer> + extends Omit, 'store'> { /** - * Signal version of `peekActiveItems` + * Reactive state that will be updated and re-rendered when the queuer state changes + * + * Use this instead of `queuer.store.state` */ - activeItems: Accessor> - /** - * Signal version of `peekAllItems` - */ - allItems: Accessor> - /** - * Signal version of `getErrorCount` - */ - errorCount: Accessor - /** - * Signal version of `getIsEmpty` - */ - isEmpty: Accessor - /** - * Signal version of `getIsFull` - */ - isFull: Accessor - /** - * Signal version of `getIsIdle` - */ - isIdle: Accessor - /** - * Signal version of `getIsRunning` - */ - isRunning: Accessor - /** - * Signal version of `peekNextItem` - */ - nextItem: Accessor - /** - * Signal version of `peekPendingItems` - */ - pendingItems: Accessor> - /** - * Signal version of `getRejectionCount` - */ - rejectionCount: Accessor - /** - * Signal version of `getSettledCount` - */ - settledCount: Accessor - /** - * Signal version of `getSize` - */ - size: Accessor - /** - * Signal version of `getSuccessCount` - */ - successCount: Accessor + readonly state: Accessor> } /** @@ -127,81 +68,17 @@ export interface SolidAsyncQueuer * const pending = asyncQueuer.pendingItems(); * ``` */ -export function createAsyncQueuer( +export function createAsyncQueuer>( fn: (value: TValue) => Promise, initialOptions: AsyncQueuerOptions = {}, -): SolidAsyncQueuer { + selector?: (state: AsyncQueuerState) => TSelected, +): SolidAsyncQueuer { const asyncQueuer = new AsyncQueuer(fn, initialOptions) - const [successCount, setSuccessCount] = createSignal( - asyncQueuer.getSuccessCount(), - ) - const [errorCount, setErrorCount] = createSignal(asyncQueuer.getErrorCount()) - const [settledCount, setSettledCount] = createSignal( - asyncQueuer.getSettledCount(), - ) - const [rejectionCount, setRejectionCount] = createSignal( - asyncQueuer.getRejectionCount(), - ) - const [isEmpty, setIsEmpty] = createSignal(asyncQueuer.getIsEmpty()) - const [isFull, setIsFull] = createSignal(asyncQueuer.getIsFull()) - const [isIdle, setIsIdle] = createSignal(asyncQueuer.getIsIdle()) - const [isRunning, setIsRunning] = createSignal(asyncQueuer.getIsRunning()) - const [allItems, setAllItems] = createSignal>( - asyncQueuer.peekAllItems(), - ) - const [activeItems, setActiveItems] = createSignal>( - asyncQueuer.peekActiveItems(), - ) - const [pendingItems, setPendingItems] = createSignal>( - asyncQueuer.peekPendingItems(), - ) - const [nextItem, setNextItem] = createSignal( - asyncQueuer.peekNextItem(), - ) - const [size, setSize] = createSignal(asyncQueuer.getSize()) - - asyncQueuer.setOptions({ - onItemsChange: (queuer) => { - setActiveItems(queuer.peekActiveItems()) - setAllItems(queuer.peekAllItems()) - setErrorCount(queuer.getErrorCount()) - setIsEmpty(queuer.getIsEmpty()) - setIsFull(queuer.getIsFull()) - setIsIdle(queuer.getIsIdle()) - setNextItem(() => queuer.peekNextItem()) - setPendingItems(queuer.peekPendingItems()) - setRejectionCount(queuer.getRejectionCount()) - setSettledCount(queuer.getSettledCount()) - setSize(queuer.getSize()) - setSuccessCount(queuer.getSuccessCount()) - initialOptions.onItemsChange?.(queuer) - }, - onIsRunningChange: (queuer) => { - setIsRunning(queuer.getIsRunning()) - setIsIdle(queuer.getIsIdle()) - initialOptions.onIsRunningChange?.(queuer) - }, - onReject: (item, queuer) => { - setRejectionCount(queuer.getRejectionCount()) - initialOptions.onReject?.(item, queuer) - }, - }) + const state = useStore(asyncQueuer.store, selector) return { - ...bindInstanceMethods(asyncQueuer), - activeItems, - allItems, - errorCount, - isEmpty, - isFull, - isIdle, - isRunning, - nextItem, - pendingItems, - rejectionCount, - settledCount, - size, - successCount, - } as SolidAsyncQueuer + ...asyncQueuer, + state, + } as unknown as SolidAsyncQueuer // omit `store` in favor of `state` } diff --git a/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts b/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts index 52787281e..d0deec6e6 100644 --- a/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts +++ b/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts @@ -1,26 +1,17 @@ import { AsyncRateLimiter } from '@tanstack/pacer/async-rate-limiter' -import { createSignal } from 'solid-js' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { useStore } from '@tanstack/solid-store' import type { Accessor } from 'solid-js' import type { AnyAsyncFunction } from '@tanstack/pacer/types' -import type { AsyncRateLimiterOptions } from '@tanstack/pacer/async-rate-limiter' +import type { + AsyncRateLimiterOptions, + AsyncRateLimiterState, +} from '@tanstack/pacer/async-rate-limiter' -export interface SolidAsyncRateLimiter - extends Omit< - AsyncRateLimiter, - | 'getSuccessCount' - | 'getSettleCount' - | 'getErrorCount' - | 'getRejectionCount' - | 'getRemainingInWindow' - | 'getMsUntilNextWindow' - > { - successCount: Accessor - settleCount: Accessor - errorCount: Accessor - rejectionCount: Accessor - remainingInWindow: Accessor - msUntilNextWindow: Accessor +export interface SolidAsyncRateLimiter< + TFn extends AnyAsyncFunction, + TSelected = AsyncRateLimiterState, +> extends Omit, 'store'> { + readonly state: Accessor> } /** @@ -85,66 +76,20 @@ export interface SolidAsyncRateLimiter * ); * ``` */ -export function createAsyncRateLimiter( +export function createAsyncRateLimiter< + TFn extends AnyAsyncFunction, + TSelected = AsyncRateLimiterState, +>( fn: TFn, initialOptions: AsyncRateLimiterOptions, -): SolidAsyncRateLimiter { + selector?: (state: AsyncRateLimiterState) => TSelected, +): SolidAsyncRateLimiter { const asyncRateLimiter = new AsyncRateLimiter(fn, initialOptions) - const [successCount, setSuccessCount] = createSignal( - asyncRateLimiter.getSuccessCount(), - ) - const [rejectionCount, setRejectionCount] = createSignal( - asyncRateLimiter.getRejectionCount(), - ) - const [errorCount, setErrorCount] = createSignal( - asyncRateLimiter.getErrorCount(), - ) - const [settleCount, setSettleCount] = createSignal( - asyncRateLimiter.getSettleCount(), - ) - const [remainingInWindow, setRemainingInWindow] = createSignal( - asyncRateLimiter.getRemainingInWindow(), - ) - const [msUntilNextWindow, setMsUntilNextWindow] = createSignal( - asyncRateLimiter.getMsUntilNextWindow(), - ) - - function setOptions(newOptions: Partial>) { - asyncRateLimiter.setOptions({ - ...newOptions, - onSettled: (rateLimiter) => { - setSuccessCount(rateLimiter.getSuccessCount()) - setSettleCount(rateLimiter.getSettleCount()) - setErrorCount(rateLimiter.getErrorCount()) - setRejectionCount(rateLimiter.getRejectionCount()) - setRemainingInWindow(rateLimiter.getRemainingInWindow()) - setMsUntilNextWindow(rateLimiter.getMsUntilNextWindow()) - - const onSettled = newOptions.onSettled ?? initialOptions.onSettled - onSettled?.(rateLimiter) - }, - onReject: (rateLimiter) => { - setRejectionCount(rateLimiter.getRejectionCount()) - setRemainingInWindow(rateLimiter.getRemainingInWindow()) - setMsUntilNextWindow(rateLimiter.getMsUntilNextWindow()) - - const onReject = newOptions.onReject ?? initialOptions.onReject - onReject?.(rateLimiter) - }, - }) - } - - setOptions(initialOptions) + const state = useStore(asyncRateLimiter.store, selector) return { - ...bindInstanceMethods(asyncRateLimiter), - errorCount, - remainingInWindow, - msUntilNextWindow, - rejectionCount, - setOptions, - settleCount, - successCount, - } as SolidAsyncRateLimiter + ...asyncRateLimiter, + state, + } as SolidAsyncRateLimiter } diff --git a/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts b/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts index 290a6f4ca..db830d9cf 100644 --- a/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts +++ b/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts @@ -1,30 +1,22 @@ import { AsyncThrottler } from '@tanstack/pacer/async-throttler' -import { createSignal } from 'solid-js' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { useStore } from '@tanstack/solid-store' import type { Accessor } from 'solid-js' import type { AnyAsyncFunction } from '@tanstack/pacer/types' -import type { AsyncThrottlerOptions } from '@tanstack/pacer/async-throttler' +import type { + AsyncThrottlerOptions, + AsyncThrottlerState, +} from '@tanstack/pacer/async-throttler' -export interface SolidAsyncThrottler - extends Omit< - AsyncThrottler, - | 'getSuccessCount' - | 'getSettleCount' - | 'getErrorCount' - | 'getIsPending' - | 'getIsExecuting' - | 'getLastResult' - | 'getLastExecutionTime' - | 'getNextExecutionTime' - > { - successCount: Accessor - settleCount: Accessor - errorCount: Accessor - isPending: Accessor - isExecuting: Accessor - lastResult: Accessor | undefined> - lastExecutionTime: Accessor - nextExecutionTime: Accessor +export interface SolidAsyncThrottler< + TFn extends AnyAsyncFunction, + TSelected = AsyncThrottlerState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated and re-rendered when the throttler state changes + * + * Use this instead of `throttler.store.state` + */ + readonly state: Accessor> } /** @@ -77,68 +69,20 @@ export interface SolidAsyncThrottler * ); * ``` */ -export function createAsyncThrottler( +export function createAsyncThrottler< + TFn extends AnyAsyncFunction, + TSelected = AsyncThrottlerState, +>( fn: TFn, initialOptions: AsyncThrottlerOptions, -): SolidAsyncThrottler { - const asyncThrottler = bindInstanceMethods( - new AsyncThrottler(fn, initialOptions), - ) + selector?: (state: AsyncThrottlerState) => TSelected, +): SolidAsyncThrottler { + const asyncThrottler = new AsyncThrottler(fn, initialOptions) - const [successCount, setSuccessCount] = createSignal( - asyncThrottler.getSuccessCount(), - ) - const [settleCount, setSettleCount] = createSignal( - asyncThrottler.getSettleCount(), - ) - const [errorCount, setErrorCount] = createSignal( - asyncThrottler.getErrorCount(), - ) - const [isPending, setIsPending] = createSignal(asyncThrottler.getIsPending()) - const [isExecuting, setIsExecuting] = createSignal( - asyncThrottler.getIsExecuting(), - ) - const [lastResult, setLastResult] = createSignal( - asyncThrottler.getLastResult(), - ) - const [lastExecutionTime, setLastExecutionTime] = createSignal( - asyncThrottler.getLastExecutionTime(), - ) - const [nextExecutionTime, setNextExecutionTime] = createSignal( - asyncThrottler.getNextExecutionTime(), - ) - - function setOptions(newOptions: Partial>) { - asyncThrottler.setOptions({ - ...newOptions, - onSettled: (throttler) => { - setSuccessCount(throttler.getSuccessCount()) - setSettleCount(throttler.getSettleCount()) - setErrorCount(throttler.getErrorCount()) - setIsPending(throttler.getIsPending()) - setIsExecuting(throttler.getIsExecuting()) - setLastExecutionTime(throttler.getLastExecutionTime()) - setNextExecutionTime(throttler.getNextExecutionTime()) - setLastResult(throttler.getLastResult()) - - const onSettled = newOptions.onSettled ?? initialOptions.onSettled - onSettled?.(throttler) - }, - }) - } - - setOptions(initialOptions) + const state = useStore(asyncThrottler.store, selector) return { ...asyncThrottler, - errorCount, - isExecuting, - isPending, - lastExecutionTime, - lastResult, - nextExecutionTime, - setOptions, - settleCount, - successCount, - } as SolidAsyncThrottler + state, + } as SolidAsyncThrottler } diff --git a/packages/solid-pacer/src/batcher/createBatcher.ts b/packages/solid-pacer/src/batcher/createBatcher.ts index 17cb64c68..093ed49b9 100644 --- a/packages/solid-pacer/src/batcher/createBatcher.ts +++ b/packages/solid-pacer/src/batcher/createBatcher.ts @@ -1,43 +1,16 @@ import { Batcher } from '@tanstack/pacer/batcher' -import { createSignal } from 'solid-js' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { useStore } from '@tanstack/solid-store' import type { Accessor } from 'solid-js' -import type { BatcherOptions } from '@tanstack/pacer/batcher' +import type { BatcherOptions, BatcherState } from '@tanstack/pacer/batcher' -export interface SolidBatcher - extends Omit< - Batcher, - | 'peekAllItems' - | 'getBatchExecutionCount' - | 'getIsEmpty' - | 'getIsRunning' - | 'getItemExecutionCount' - | 'getSize' - > { +export interface SolidBatcher> + extends Omit, 'store'> { /** - * Signal version of `peekAllItems` + * Reactive state that will be updated when the batcher state changes + * + * Use this instead of `batcher.store.state` */ - allItems: Accessor> - /** - * Signal version of `getBatchExecutionCount` - */ - batchExecutionCount: Accessor - /** - * Signal version of `getIsEmpty` - */ - isEmpty: Accessor - /** - * Signal version of `getIsRunning` - */ - isRunning: Accessor - /** - * Signal version of `getItemExecutionCount` - */ - itemExecutionCount: Accessor - /** - * Signal version of `getSize` - */ - size: Accessor + readonly state: Accessor> } /** @@ -83,70 +56,20 @@ export interface SolidBatcher * console.log('Size:', batcher.size()); * console.log('Is empty:', batcher.isEmpty()); * console.log('Is running:', batcher.isRunning()); - * console.log('Batch count:', batcher.batchExecutionCount()); - * console.log('Item count:', batcher.itemExecutionCount()); + * console.log('Batch count:', batcher.executionCount()); + * console.log('Item count:', batcher.totalItemsProcessed()); * ``` */ -export function createBatcher( +export function createBatcher>( fn: (items: Array) => void, initialOptions: BatcherOptions = {}, -): SolidBatcher { - const batcher = bindInstanceMethods(new Batcher(fn, initialOptions)) - - const [allItems, setAllItems] = createSignal>( - batcher.peekAllItems(), - ) - const [batchExecutionCount, setBatchExecutionCount] = createSignal( - batcher.getBatchExecutionCount(), - ) - const [itemExecutionCount, setItemExecutionCount] = createSignal( - batcher.getItemExecutionCount(), - ) - const [isEmpty, setIsEmpty] = createSignal(batcher.getIsEmpty()) - const [isRunning, setIsRunning] = createSignal(batcher.getIsRunning()) - const [size, setSize] = createSignal(batcher.getSize()) - - function setOptions(newOptions: Partial>) { - batcher.setOptions({ - ...newOptions, - onItemsChange: (batcher) => { - setAllItems(batcher.peekAllItems()) - setBatchExecutionCount(batcher.getBatchExecutionCount()) - setItemExecutionCount(batcher.getItemExecutionCount()) - setIsEmpty(batcher.getIsEmpty()) - setSize(batcher.getSize()) - - const onItemsChange = - newOptions.onItemsChange ?? initialOptions.onItemsChange - onItemsChange?.(batcher) - }, - onExecute: (batcher) => { - setBatchExecutionCount(batcher.getBatchExecutionCount()) - setItemExecutionCount(batcher.getItemExecutionCount()) - - const onExecute = newOptions.onExecute ?? initialOptions.onExecute - onExecute?.(batcher) - }, - onIsRunningChange: (batcher) => { - setIsRunning(batcher.getIsRunning()) - - const onIsRunningChange = - newOptions.onIsRunningChange ?? initialOptions.onIsRunningChange - onIsRunningChange?.(batcher) - }, - }) - } - - setOptions(initialOptions) + selector?: (state: BatcherState) => TSelected, +): SolidBatcher { + const batcher = new Batcher(fn, initialOptions) + const state = useStore(batcher.store, selector) return { ...batcher, - allItems, - batchExecutionCount, - isEmpty, - isRunning, - itemExecutionCount, - size, - setOptions, - } as SolidBatcher + state, + } as SolidBatcher } diff --git a/packages/solid-pacer/src/compare/index.ts b/packages/solid-pacer/src/compare/index.ts deleted file mode 100644 index c33052fd9..000000000 --- a/packages/solid-pacer/src/compare/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@tanstack/pacer/compare' diff --git a/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts b/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts index 7783ac897..6f72122f3 100644 --- a/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts +++ b/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts @@ -2,7 +2,10 @@ import { createSignal } from 'solid-js' import { createDebouncer } from './createDebouncer' import type { SolidDebouncer } from './createDebouncer' import type { Accessor, Setter } from 'solid-js' -import type { DebouncerOptions } from '@tanstack/pacer/debouncer' +import type { + DebouncerOptions, + DebouncerState, +} from '@tanstack/pacer/debouncer' /** * A Solid hook that creates a debounced state value, combining Solid's createSignal with debouncing functionality. @@ -43,13 +46,21 @@ import type { DebouncerOptions } from '@tanstack/pacer/debouncer' * }); * ``` */ -export function createDebouncedSignal( +export function createDebouncedSignal< + TValue, + TSelected = DebouncerState>, +>( value: TValue, initialOptions: DebouncerOptions>, -): [Accessor, Setter, SolidDebouncer>] { + selector?: (state: DebouncerState>) => TSelected, +): [ + Accessor, + Setter, + SolidDebouncer, TSelected>, +] { const [debouncedValue, setDebouncedValue] = createSignal(value) - const debouncer = createDebouncer(setDebouncedValue, initialOptions) + const debouncer = createDebouncer(setDebouncedValue, initialOptions, selector) return [debouncedValue, debouncer.maybeExecute as Setter, debouncer] } diff --git a/packages/solid-pacer/src/debouncer/createDebouncedValue.ts b/packages/solid-pacer/src/debouncer/createDebouncedValue.ts index e610d6a26..6151c405a 100644 --- a/packages/solid-pacer/src/debouncer/createDebouncedValue.ts +++ b/packages/solid-pacer/src/debouncer/createDebouncedValue.ts @@ -2,7 +2,10 @@ import { createEffect } from 'solid-js' import { createDebouncedSignal } from './createDebouncedSignal' import type { SolidDebouncer } from './createDebouncer' import type { Accessor, Setter } from 'solid-js' -import type { DebouncerOptions } from '@tanstack/pacer/debouncer' +import type { + DebouncerOptions, + DebouncerState, +} from '@tanstack/pacer/debouncer' /** * A Solid hook that creates a debounced value that updates only after a specified delay. @@ -38,13 +41,18 @@ import type { DebouncerOptions } from '@tanstack/pacer/debouncer' * debouncer.cancel(); // Cancel any pending updates * ``` */ -export function createDebouncedValue( +export function createDebouncedValue< + TValue, + TSelected = DebouncerState>, +>( value: Accessor, initialOptions: DebouncerOptions>, -): [Accessor, SolidDebouncer>] { + selector?: (state: DebouncerState>) => TSelected, +): [Accessor, SolidDebouncer, TSelected>] { const [debouncedValue, setDebouncedValue, debouncer] = createDebouncedSignal( value(), initialOptions, + selector, ) createEffect(() => { diff --git a/packages/solid-pacer/src/debouncer/createDebouncer.ts b/packages/solid-pacer/src/debouncer/createDebouncer.ts index 6ad07ffe2..14f6d584d 100644 --- a/packages/solid-pacer/src/debouncer/createDebouncer.ts +++ b/packages/solid-pacer/src/debouncer/createDebouncer.ts @@ -1,17 +1,23 @@ import { Debouncer } from '@tanstack/pacer/debouncer' -import { createEffect, createSignal, onCleanup } from 'solid-js' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { createEffect, onCleanup } from 'solid-js' +import { useStore } from '@tanstack/solid-store' import type { Accessor } from 'solid-js' import type { AnyFunction } from '@tanstack/pacer/types' -import type { DebouncerOptions } from '@tanstack/pacer/debouncer' +import type { + DebouncerOptions, + DebouncerState, +} from '@tanstack/pacer/debouncer' -/** - * An extension of the Debouncer class that adds Solid signals to access the internal state of the debouncer - */ -export interface SolidDebouncer - extends Omit, 'getExecutionCount' | 'getIsPending'> { - executionCount: Accessor - isPending: Accessor +export interface SolidDebouncer< + TFn extends AnyFunction, + TSelected = DebouncerState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated when the debouncer state changes + * + * Use this instead of `debouncer.store.state` + */ + readonly state: Accessor> } /** @@ -50,42 +56,26 @@ export interface SolidDebouncer * debouncer.setOptions({ wait: 1000 }); * ``` */ -export function createDebouncer( +export function createDebouncer< + TFn extends AnyFunction, + TSelected = DebouncerState, +>( fn: TFn, initialOptions: DebouncerOptions, -): SolidDebouncer { - const debouncer = bindInstanceMethods(new Debouncer(fn, initialOptions)) - - const [executionCount, setExecutionCount] = createSignal( - debouncer.getExecutionCount(), - ) - const [isPending, setIsPending] = createSignal(debouncer.getIsPending()) - - function setOptions(newOptions: Partial>) { - debouncer.setOptions({ - ...newOptions, - onExecute: (debouncer) => { - setExecutionCount(debouncer.getExecutionCount()) - setIsPending(debouncer.getIsPending()) - - const onExecute = newOptions.onExecute ?? initialOptions.onExecute - onExecute?.(debouncer) - }, - }) - } + selector?: (state: DebouncerState) => TSelected, +): SolidDebouncer { + const asyncDebouncer = new Debouncer(fn, initialOptions) - setOptions(initialOptions) + const state = useStore(asyncDebouncer.store, selector) createEffect(() => { onCleanup(() => { - debouncer.cancel() + asyncDebouncer.cancel() }) }) return { - ...debouncer, - executionCount, - isPending, - setOptions, - } as SolidDebouncer + ...asyncDebouncer, + state, + } as SolidDebouncer // omit `store` in favor of `state` } diff --git a/packages/solid-pacer/src/index.ts b/packages/solid-pacer/src/index.ts index c88707a8f..9ad6c4de4 100644 --- a/packages/solid-pacer/src/index.ts +++ b/packages/solid-pacer/src/index.ts @@ -5,6 +5,9 @@ export * from '@tanstack/pacer' * Export every hook individually - DON'T export from barrel files */ +// async-batcher +export * from './async-batcher/createAsyncBatcher' + // async-debouncer export * from './async-debouncer/createAsyncDebouncer' diff --git a/packages/solid-pacer/src/queuer/createQueuer.ts b/packages/solid-pacer/src/queuer/createQueuer.ts index 406473e89..a14930623 100644 --- a/packages/solid-pacer/src/queuer/createQueuer.ts +++ b/packages/solid-pacer/src/queuer/createQueuer.ts @@ -1,57 +1,16 @@ import { Queuer } from '@tanstack/pacer/queuer' -import { createSignal } from 'solid-js' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { useStore } from '@tanstack/solid-store' import type { Accessor } from 'solid-js' -import type { QueuerOptions } from '@tanstack/pacer/queuer' +import type { QueuerOptions, QueuerState } from '@tanstack/pacer/queuer' -export interface SolidQueuer - extends Omit< - Queuer, - | 'getExecutionCount' - | 'getIsEmpty' - | 'getIsFull' - | 'getIsIdle' - | 'getIsRunning' - | 'getSize' - | 'peekAllItems' - | 'peekNextItem' - > { +export interface SolidQueuer> + extends Omit, 'store'> { /** - * Signal version of `peekAllItems` + * Reactive state that will be updated when the queuer state changes + * + * Use this instead of `queuer.store.state` */ - allItems: Accessor> - /** - * Signal version of `getExecutionCount` - */ - executionCount: Accessor - /** - * Signal version of `getIsEmpty` - */ - isEmpty: Accessor - /** - * Signal version of `getIsFull` - */ - isFull: Accessor - /** - * Signal version of `getIsIdle` - */ - isIdle: Accessor - /** - * Signal version of `getIsRunning` - */ - isRunning: Accessor - /** - * Signal version of `peekNextItem` - */ - nextItem: Accessor - /** - * Signal version of `getRejectionCount` - */ - rejectionCount: Accessor - /** - * Signal version of `getSize` - */ - size: Accessor + readonly state: Accessor> } /** @@ -105,76 +64,17 @@ export interface SolidQueuer * console.log('Next item:', queue.nextItem()); * ``` */ -export function createQueuer( +export function createQueuer>( fn: (item: TValue) => void, initialOptions: QueuerOptions = {}, -): SolidQueuer { - const queuer = bindInstanceMethods(new Queuer(fn, initialOptions)) - - const [allItems, setAllItems] = createSignal>( - queuer.peekAllItems(), - ) - const [executionCount, setExecutionCount] = createSignal( - queuer.getExecutionCount(), - ) - const [rejectionCount, setRejectionCount] = createSignal( - queuer.getRejectionCount(), - ) - const [isEmpty, setIsEmpty] = createSignal(queuer.getIsEmpty()) - const [isFull, setIsFull] = createSignal(queuer.getIsFull()) - const [isIdle, setIsIdle] = createSignal(queuer.getIsIdle()) - const [isRunning, setIsRunning] = createSignal(queuer.getIsRunning()) - const [nextItem, setNextItem] = createSignal( - queuer.peekNextItem(), - ) - const [size, setSize] = createSignal(queuer.getSize()) - - function setOptions(newOptions: Partial>) { - queuer.setOptions({ - ...newOptions, - onItemsChange: (queuer) => { - setAllItems(queuer.peekAllItems()) - setExecutionCount(queuer.getExecutionCount()) - setIsEmpty(queuer.getIsEmpty()) - setIsFull(queuer.getIsFull()) - setIsIdle(queuer.getIsIdle()) - setNextItem(() => queuer.peekNextItem()) - setSize(queuer.getSize()) - - const onItemsChange = - newOptions.onItemsChange ?? initialOptions.onItemsChange - onItemsChange?.(queuer) - }, - onIsRunningChange: (queuer) => { - setIsRunning(queuer.getIsRunning()) - setIsIdle(queuer.getIsIdle()) - - const onIsRunningChange = - newOptions.onIsRunningChange ?? initialOptions.onIsRunningChange - onIsRunningChange?.(queuer) - }, - onReject: (item, queuer) => { - setRejectionCount(queuer.getRejectionCount()) - - const onReject = newOptions.onReject ?? initialOptions.onReject - onReject?.(item, queuer) - }, - }) - } + selector?: (state: QueuerState) => TSelected, +): SolidQueuer { + const queuer = new Queuer(fn, initialOptions) - setOptions(initialOptions) + const state = useStore(queuer.store, selector) return { ...queuer, - allItems, - executionCount, - isEmpty, - isFull, - isIdle, - isRunning, - nextItem, - rejectionCount, - size, - setOptions, - } as SolidQueuer + state, + } as unknown as SolidQueuer // omit `store` in favor of `state` } diff --git a/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts b/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts index 87651d5a9..22d9103b9 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts @@ -2,7 +2,10 @@ import { createSignal } from 'solid-js' import { createRateLimiter } from './createRateLimiter' import type { SolidRateLimiter } from './createRateLimiter' import type { Accessor, Setter } from 'solid-js' -import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' +import type { + RateLimiterOptions, + RateLimiterState, +} from '@tanstack/pacer/rate-limiter' /** * A Solid hook that creates a rate-limited state value that enforces a hard limit on state updates within a time window. @@ -62,17 +65,26 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * }; * ``` */ -export function createRateLimitedSignal( +export function createRateLimitedSignal( value: TValue, initialOptions: RateLimiterOptions>, -): [Accessor, Setter, SolidRateLimiter>] { + selector?: (state: RateLimiterState) => TSelected, +): [ + Accessor, + Setter, + SolidRateLimiter, TSelected>, +] { const [rateLimitedValue, setRateLimitedValue] = createSignal(value) - const rateLimiter = createRateLimiter(setRateLimitedValue, initialOptions) + const rateLimiter = createRateLimiter( + setRateLimitedValue, + initialOptions, + selector, + ) return [ rateLimitedValue, - rateLimiter.maybeExecute.bind(rateLimiter) as Setter, + rateLimiter.maybeExecute as Setter, rateLimiter, ] } diff --git a/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts b/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts index f4512e817..c2a9187fe 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts @@ -2,7 +2,10 @@ import { createEffect } from 'solid-js' import { createRateLimitedSignal } from './createRateLimitedSignal' import type { SolidRateLimiter } from './createRateLimiter' import type { Accessor, Setter } from 'solid-js' -import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' +import type { + RateLimiterOptions, + RateLimiterState, +} from '@tanstack/pacer/rate-limiter' /** * A high-level Solid hook that creates a rate-limited version of a value that updates at most a certain number of times within a time window. @@ -47,12 +50,13 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * rateLimiter.reset(); // Reset the rate limit window * ``` */ -export function createRateLimitedValue( +export function createRateLimitedValue( value: Accessor, initialOptions: RateLimiterOptions>, -): [Accessor, SolidRateLimiter>] { + selector?: (state: RateLimiterState) => TSelected, +): [Accessor, SolidRateLimiter, TSelected>] { const [rateLimitedValue, setRateLimitedValue, rateLimiter] = - createRateLimitedSignal(value(), initialOptions) + createRateLimitedSignal(value(), initialOptions, selector) createEffect(() => { setRateLimitedValue(value() as any) diff --git a/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts b/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts index e3801f361..736abae2b 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts @@ -1,22 +1,22 @@ import { RateLimiter } from '@tanstack/pacer/rate-limiter' -import { createSignal } from 'solid-js' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { useStore } from '@tanstack/solid-store' import type { Accessor } from 'solid-js' import type { AnyFunction } from '@tanstack/pacer/types' -import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' +import type { + RateLimiterOptions, + RateLimiterState, +} from '@tanstack/pacer/rate-limiter' -export interface SolidRateLimiter - extends Omit< - RateLimiter, - | 'getExecutionCount' - | 'getMsUntilNextWindow' - | 'getRejectionCount' - | 'getRemainingInWindow' - > { - executionCount: Accessor - msUntilNextWindow: Accessor - rejectionCount: Accessor - remainingInWindow: Accessor +export interface SolidRateLimiter< + TFn extends AnyFunction, + TSelected = RateLimiterState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated when the rate limiter state changes + * + * Use this instead of `rateLimiter.store.state` + */ + readonly state: Accessor> } /** @@ -59,57 +59,20 @@ export interface SolidRateLimiter * console.log('Next window in:', rateLimiter.msUntilNextWindow()); * ``` */ -export function createRateLimiter( +export function createRateLimiter< + TFn extends AnyFunction, + TSelected = RateLimiterState, +>( fn: TFn, initialOptions: RateLimiterOptions, -): SolidRateLimiter { - const rateLimiter = bindInstanceMethods( - new RateLimiter(fn, initialOptions), - ) + selector?: (state: RateLimiterState) => TSelected, +): SolidRateLimiter { + const rateLimiter = new RateLimiter(fn, initialOptions) - const [executionCount, setExecutionCount] = createSignal( - rateLimiter.getExecutionCount(), - ) - const [rejectionCount, setRejectionCount] = createSignal( - rateLimiter.getRejectionCount(), - ) - const [remainingInWindow, setRemainingInWindow] = createSignal( - rateLimiter.getRemainingInWindow(), - ) - const [msUntilNextWindow, setMsUntilNextWindow] = createSignal( - rateLimiter.getMsUntilNextWindow(), - ) - - function setOptions(newOptions: Partial>) { - rateLimiter.setOptions({ - ...newOptions, - onExecute: (rateLimiter) => { - setExecutionCount(rateLimiter.getExecutionCount()) - setRemainingInWindow(rateLimiter.getRemainingInWindow()) - setMsUntilNextWindow(rateLimiter.getMsUntilNextWindow()) - - const onExecute = newOptions.onExecute ?? initialOptions.onExecute - onExecute?.(rateLimiter) - }, - onReject: (rateLimiter) => { - setRejectionCount(rateLimiter.getRejectionCount()) - setRemainingInWindow(rateLimiter.getRemainingInWindow()) - setMsUntilNextWindow(rateLimiter.getMsUntilNextWindow()) - - const onReject = newOptions.onReject ?? initialOptions.onReject - onReject?.(rateLimiter) - }, - }) - } - - setOptions(initialOptions) + const state = useStore(rateLimiter.store, selector) return { ...rateLimiter, - executionCount, - rejectionCount, - remainingInWindow, - msUntilNextWindow, - setOptions, - } as SolidRateLimiter + state, + } as unknown as SolidRateLimiter // omit `store` in favor of `state` } diff --git a/packages/solid-pacer/src/throttler/createThrottledSignal.ts b/packages/solid-pacer/src/throttler/createThrottledSignal.ts index f77916ee8..3dd95c805 100644 --- a/packages/solid-pacer/src/throttler/createThrottledSignal.ts +++ b/packages/solid-pacer/src/throttler/createThrottledSignal.ts @@ -2,7 +2,10 @@ import { createSignal } from 'solid-js' import { createThrottler } from './createThrottler' import type { SolidThrottler } from './createThrottler' import type { Accessor, Setter } from 'solid-js' -import type { ThrottlerOptions } from '@tanstack/pacer/throttler' +import type { + ThrottlerOptions, + ThrottlerState, +} from '@tanstack/pacer/throttler' /** * A Solid hook that creates a throttled state value that updates at most once within a specified time window. @@ -38,11 +41,19 @@ import type { ThrottlerOptions } from '@tanstack/pacer/throttler' * console.log('Next execution:', throttler.nextExecutionTime()); * ``` */ -export function createThrottledSignal( +export function createThrottledSignal< + TValue, + TSelected = ThrottlerState>, +>( value: TValue, initialOptions: ThrottlerOptions>, -): [Accessor, Setter, SolidThrottler>] { + selector?: (state: ThrottlerState>) => TSelected, +): [ + Accessor, + Setter, + SolidThrottler, TSelected>, +] { const [throttledValue, setThrottledValue] = createSignal(value) - const throttler = createThrottler(setThrottledValue, initialOptions) + const throttler = createThrottler(setThrottledValue, initialOptions, selector) return [throttledValue, throttler.maybeExecute as Setter, throttler] } diff --git a/packages/solid-pacer/src/throttler/createThrottledValue.ts b/packages/solid-pacer/src/throttler/createThrottledValue.ts index eb17e01cd..6138f459d 100644 --- a/packages/solid-pacer/src/throttler/createThrottledValue.ts +++ b/packages/solid-pacer/src/throttler/createThrottledValue.ts @@ -2,7 +2,10 @@ import { createEffect } from 'solid-js' import { createThrottledSignal } from './createThrottledSignal' import type { SolidThrottler } from './createThrottler' import type { Accessor, Setter } from 'solid-js' -import type { ThrottlerOptions } from '@tanstack/pacer/throttler' +import type { + ThrottlerOptions, + ThrottlerState, +} from '@tanstack/pacer/throttler' /** * A high-level Solid hook that creates a throttled version of a value that updates at most once within a specified time window. @@ -32,13 +35,18 @@ import type { ThrottlerOptions } from '@tanstack/pacer/throttler' * throttler.cancel(); // Cancel any pending updates * ``` */ -export function createThrottledValue( +export function createThrottledValue< + TValue, + TSelected = ThrottlerState>, +>( value: Accessor, initialOptions: ThrottlerOptions>, -): [Accessor, SolidThrottler>] { + selector?: (state: ThrottlerState>) => TSelected, +): [Accessor, SolidThrottler, TSelected>] { const [throttledValue, setThrottledValue, throttler] = createThrottledSignal( value(), initialOptions, + selector, ) createEffect(() => { diff --git a/packages/solid-pacer/src/throttler/createThrottler.ts b/packages/solid-pacer/src/throttler/createThrottler.ts index ad4f73e4e..3a19ae08b 100644 --- a/packages/solid-pacer/src/throttler/createThrottler.ts +++ b/packages/solid-pacer/src/throttler/createThrottler.ts @@ -1,25 +1,23 @@ import { Throttler } from '@tanstack/pacer/throttler' -import { createEffect, createSignal, onCleanup } from 'solid-js' -import { bindInstanceMethods } from '@tanstack/pacer/utils' +import { createEffect, onCleanup } from 'solid-js' +import { useStore } from '@tanstack/solid-store' import type { Accessor } from 'solid-js' import type { AnyFunction } from '@tanstack/pacer/types' -import type { ThrottlerOptions } from '@tanstack/pacer/throttler' +import type { + ThrottlerOptions, + ThrottlerState, +} from '@tanstack/pacer/throttler' -/** - * An extension of the Throttler class that adds Solid signals to access the internal state of the throttler - */ -export interface SolidThrottler - extends Omit< - Throttler, - | 'getExecutionCount' - | 'getIsPending' - | 'getLastExecutionTime' - | 'getNextExecutionTime' - > { - executionCount: Accessor - isPending: Accessor - lastExecutionTime: Accessor - nextExecutionTime: Accessor +export interface SolidThrottler< + TFn extends AnyFunction, + TSelected = ThrottlerState, +> extends Omit, 'store'> { + /** + * Reactive state that will be updated when the throttler state changes + * + * Use this instead of `throttler.store.state` + */ + readonly state: Accessor> } /** @@ -56,52 +54,26 @@ export interface SolidThrottler * console.log(throttler.nextExecutionTime()); // timestamp of next allowed execution * ``` */ -export function createThrottler( +export function createThrottler< + TFn extends AnyFunction, + TSelected = ThrottlerState, +>( fn: TFn, initialOptions: ThrottlerOptions, -): SolidThrottler { - const throttler = bindInstanceMethods(new Throttler(fn, initialOptions)) - - const [executionCount, setExecutionCount] = createSignal( - throttler.getExecutionCount(), - ) - const [isPending, setIsPending] = createSignal(throttler.getIsPending()) - const [lastExecutionTime, setLastExecutionTime] = createSignal( - throttler.getLastExecutionTime(), - ) - const [nextExecutionTime, setNextExecutionTime] = createSignal( - throttler.getNextExecutionTime(), - ) - - function setOptions(newOptions: Partial>) { - throttler.setOptions({ - ...newOptions, - onExecute: (throttler) => { - setExecutionCount(throttler.getExecutionCount()) - setIsPending(throttler.getIsPending()) - setLastExecutionTime(throttler.getLastExecutionTime()) - setNextExecutionTime(throttler.getNextExecutionTime()) - - const onExecute = newOptions.onExecute ?? initialOptions.onExecute - onExecute?.(throttler) - }, - }) - } + selector?: (state: ThrottlerState) => TSelected, +): SolidThrottler { + const asyncThrottler = new Throttler(fn, initialOptions) - setOptions(initialOptions) + const state = useStore(asyncThrottler.store, selector) createEffect(() => { onCleanup(() => { - throttler.cancel() + asyncThrottler.cancel() }) }) return { - ...throttler, - executionCount, - isPending, - lastExecutionTime, - nextExecutionTime, - setOptions, - } as SolidThrottler + ...asyncThrottler, + state, + } as SolidThrottler // omit `store` in favor of `state` } diff --git a/packages/solid-pacer/vite.config.ts b/packages/solid-pacer/vite.config.ts index 621b9e0a7..db733e3f1 100644 --- a/packages/solid-pacer/vite.config.ts +++ b/packages/solid-pacer/vite.config.ts @@ -19,12 +19,12 @@ export default mergeConfig( config, tanstackViteConfig({ entry: [ + './src/async-batcher/index.ts', './src/async-debouncer/index.ts', './src/async-queuer/index.ts', './src/async-rate-limiter/index.ts', './src/async-throttler/index.ts', './src/batcher/index.ts', - './src/compare/index.ts', './src/debouncer/index.ts', './src/index.ts', './src/queuer/index.ts', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdb21f969..51aec220e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: devDependencies: '@changesets/cli': - specifier: ^2.29.4 - version: 2.29.4 + specifier: ^2.29.5 + version: 2.29.5 '@faker-js/faker': - specifier: ^9.8.0 - version: 9.8.0 + specifier: ^9.9.0 + version: 9.9.0 '@size-limit/preset-small-lib': specifier: ^11.2.0 version: 11.2.0(size-limit@11.2.0) @@ -21,20 +21,20 @@ importers: specifier: ^1.2.0 version: 1.2.0 '@tanstack/config': - specifier: 0.18.2 - version: 0.18.2(@types/node@22.15.30)(@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0(jiti@2.4.2))(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: 0.19.0 + version: 0.19.0(@types/node@24.0.13)(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.30.1(jiti@2.4.2))(rollup@4.44.1)(typescript@5.8.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 '@types/node': - specifier: ^22.15.30 - version: 22.15.30 + specifier: ^24.0.13 + version: 24.0.13 eslint: - specifier: ^9.28.0 - version: 9.28.0(jiti@2.4.2) + specifier: ^9.30.1 + version: 9.30.1(jiti@2.4.2) eslint-plugin-unused-imports: specifier: ^4.1.4 - version: 4.1.4(@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)) + version: 4.1.4(@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)) fast-glob: specifier: ^3.3.3 version: 3.3.3 @@ -42,29 +42,29 @@ importers: specifier: ^26.1.0 version: 26.1.0 knip: - specifier: ^5.60.2 - version: 5.60.2(@types/node@22.15.30)(typescript@5.8.3) + specifier: ^5.61.3 + version: 5.61.3(@types/node@24.0.13)(typescript@5.8.3) markdown-link-extractor: specifier: ^4.0.2 version: 4.0.2 nx: - specifier: ^21.1.3 - version: 21.1.3 + specifier: ^21.2.2 + version: 21.2.2 premove: specifier: ^4.0.0 version: 4.0.0 prettier: - specifier: ^3.5.3 - version: 3.5.3 + specifier: ^3.6.2 + version: 3.6.2 prettier-plugin-svelte: specifier: ^3.4.0 - version: 3.4.0(prettier@3.5.3)(svelte@5.22.6) + version: 3.4.0(prettier@3.6.2)(svelte@5.22.6) publint: specifier: ^0.3.12 version: 0.3.12 sherif: - specifier: ^1.5.0 - version: 1.5.0 + specifier: ^1.6.1 + version: 1.6.1 size-limit: specifier: ^11.2.0 version: 11.2.0 @@ -72,11 +72,11 @@ importers: specifier: 5.8.3 version: 5.8.3 vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vitest: - specifier: ^3.2.2 - version: 3.2.2(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.0.13)(jiti@2.4.2)(jsdom@26.1.0)(tsx@4.19.3)(yaml@2.7.0) examples/react/asyncDebounce: dependencies: @@ -91,17 +91,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/asyncRateLimit: dependencies: @@ -116,17 +116,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/asyncThrottle: dependencies: @@ -141,17 +141,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/batch: dependencies: @@ -166,17 +166,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/debounce: dependencies: @@ -191,17 +191,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/queue: dependencies: @@ -216,17 +216,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/rateLimit: dependencies: @@ -241,17 +241,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/react-query-debounced-prefetch: dependencies: @@ -259,11 +259,11 @@ importers: specifier: ^0.8.0 version: link:../../../packages/react-pacer '@tanstack/react-query': - specifier: ^5.80.6 - version: 5.80.6(react@19.1.0) + specifier: ^5.82.0 + version: 5.82.0(react@19.1.0) '@tanstack/react-query-devtools': - specifier: ^5.80.6 - version: 5.80.6(@tanstack/react-query@5.80.6(react@19.1.0))(react@19.1.0) + specifier: ^5.82.0 + version: 5.82.0(@tanstack/react-query@5.82.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.1.0 version: 19.1.0 @@ -272,17 +272,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/react-query-queued-prefetch: dependencies: @@ -290,11 +290,11 @@ importers: specifier: ^0.8.0 version: link:../../../packages/react-pacer '@tanstack/react-query': - specifier: ^5.80.6 - version: 5.80.6(react@19.1.0) + specifier: ^5.82.0 + version: 5.82.0(react@19.1.0) '@tanstack/react-query-devtools': - specifier: ^5.80.6 - version: 5.80.6(@tanstack/react-query@5.80.6(react@19.1.0))(react@19.1.0) + specifier: ^5.82.0 + version: 5.82.0(@tanstack/react-query@5.82.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.1.0 version: 19.1.0 @@ -303,17 +303,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/react-query-throttled-prefetch: dependencies: @@ -321,11 +321,11 @@ importers: specifier: ^0.8.0 version: link:../../../packages/react-pacer '@tanstack/react-query': - specifier: ^5.80.6 - version: 5.80.6(react@19.1.0) + specifier: ^5.82.0 + version: 5.82.0(react@19.1.0) '@tanstack/react-query-devtools': - specifier: ^5.80.6 - version: 5.80.6(@tanstack/react-query@5.80.6(react@19.1.0))(react@19.1.0) + specifier: ^5.82.0 + version: 5.82.0(@tanstack/react-query@5.82.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.1.0 version: 19.1.0 @@ -334,17 +334,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/throttle: dependencies: @@ -359,17 +359,42 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': + specifier: ^19.1.8 + version: 19.1.8 + '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6 + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite: + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + + examples/react/useAsyncBatcher: + dependencies: + '@tanstack/react-pacer': + specifier: ^0.8.0 + version: link:../../../packages/react-pacer + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useAsyncDebouncer: dependencies: @@ -384,17 +409,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useAsyncQueuedState: dependencies: @@ -409,17 +434,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useAsyncQueuer: dependencies: @@ -434,23 +459,26 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useAsyncRateLimiter: dependencies: '@tanstack/react-pacer': specifier: ^0.8.0 version: link:../../../packages/react-pacer + '@tanstack/react-persister': + specifier: ^0.0.0 + version: link:../../../packages/react-persister react: specifier: ^19.1.0 version: 19.1.0 @@ -459,17 +487,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useAsyncThrottler: dependencies: @@ -484,17 +512,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useBatcher: dependencies: @@ -509,17 +537,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useDebouncedCallback: dependencies: @@ -534,17 +562,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useDebouncedState: dependencies: @@ -559,17 +587,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useDebouncedValue: dependencies: @@ -584,17 +612,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useDebouncer: dependencies: @@ -609,17 +637,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useQueuedState: dependencies: @@ -634,17 +662,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useQueuedValue: dependencies: @@ -659,23 +687,26 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useQueuer: dependencies: '@tanstack/react-pacer': specifier: ^0.8.0 version: link:../../../packages/react-pacer + '@tanstack/react-persister': + specifier: ^0.0.0 + version: link:../../../packages/react-persister react: specifier: ^19.1.0 version: 19.1.0 @@ -684,17 +715,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useRateLimitedCallback: dependencies: @@ -709,17 +740,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useRateLimitedState: dependencies: @@ -734,17 +765,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useRateLimitedValue: dependencies: @@ -759,23 +790,26 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useRateLimiter: dependencies: '@tanstack/react-pacer': specifier: ^0.8.0 version: link:../../../packages/react-pacer + '@tanstack/react-persister': + specifier: ^0.0.0 + version: link:../../../packages/react-persister react: specifier: ^19.1.0 version: 19.1.0 @@ -784,17 +818,42 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': + specifier: ^19.1.8 + version: 19.1.8 + '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6 + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite: + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + + examples/react/useStorageState: + dependencies: + '@tanstack/react-persister': + specifier: ^0.0.0 + version: link:../../../packages/react-persister + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useThrottledCallback: dependencies: @@ -809,17 +868,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useThrottledState: dependencies: @@ -834,17 +893,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useThrottledValue: dependencies: @@ -859,17 +918,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useThrottler: dependencies: @@ -884,17 +943,17 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 '@types/react-dom': specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.6) + version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/solid/asyncDebounce: dependencies: @@ -906,11 +965,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/asyncRateLimit: dependencies: @@ -922,11 +981,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/asyncThrottle: dependencies: @@ -938,11 +997,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/batch: dependencies: @@ -954,11 +1013,27 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + + examples/solid/createAsyncBatcher: + dependencies: + '@tanstack/solid-pacer': + specifier: ^0.8.0 + version: link:../../../packages/solid-pacer + solid-js: + specifier: ^1.9.7 + version: 1.9.7 + devDependencies: + vite: + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite-plugin-solid: + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createAsyncDebouncer: dependencies: @@ -970,11 +1045,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createAsyncQueuer: dependencies: @@ -986,11 +1061,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createAsyncRateLimiter: dependencies: @@ -1002,11 +1077,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createAsyncThrottler: dependencies: @@ -1018,11 +1093,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createBatcher: dependencies: @@ -1034,11 +1109,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createDebouncedSignal: dependencies: @@ -1050,11 +1125,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createDebouncedValue: dependencies: @@ -1066,11 +1141,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createDebouncer: dependencies: @@ -1082,11 +1157,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createQueuer: dependencies: @@ -1098,11 +1173,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createRateLimitedSignal: dependencies: @@ -1114,11 +1189,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createRateLimitedValue: dependencies: @@ -1130,11 +1205,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createRateLimiter: dependencies: @@ -1146,11 +1221,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createThrottledSignal: dependencies: @@ -1162,11 +1237,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createThrottledValue: dependencies: @@ -1178,11 +1253,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createThrottler: dependencies: @@ -1194,11 +1269,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/debounce: dependencies: @@ -1210,11 +1285,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/queue: dependencies: @@ -1226,11 +1301,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/rateLimit: dependencies: @@ -1242,11 +1317,11 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/throttle: dependencies: @@ -1258,38 +1333,75 @@ importers: version: 1.9.7 devDependencies: vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^7.0.4 + version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + + packages/pacer: + dependencies: + '@tanstack/store': + specifier: ^0.7.2 + version: 0.7.2 - packages/pacer: {} + packages/persister: {} packages/react-pacer: dependencies: '@tanstack/pacer': specifier: workspace:* version: link:../pacer + '@tanstack/react-store': + specifier: ^0.7.3 + version: 0.7.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-dom: specifier: '>=16.8' - version: 19.0.0(react@19.1.0) + version: 19.1.0(react@19.1.0) devDependencies: '@eslint-react/eslint-plugin': - specifier: ^1.51.2 - version: 1.51.2(eslint@9.28.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3) + specifier: ^1.52.2 + version: 1.52.2(eslint@9.30.1(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3) '@types/react': - specifier: ^19.1.6 - version: 19.1.6 + specifier: ^19.1.8 + version: 19.1.8 + '@vitejs/plugin-react': + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + eslint-plugin-react-compiler: + specifier: 19.1.0-rc.2 + version: 19.1.0-rc.2(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.30.1(jiti@2.4.2)) + react: + specifier: ^19.1.0 + version: 19.1.0 + + packages/react-persister: + dependencies: + '@tanstack/persister': + specifier: workspace:* + version: link:../persister + react-dom: + specifier: '>=16.8' + version: 19.1.0(react@19.1.0) + devDependencies: + '@eslint-react/eslint-plugin': + specifier: ^1.52.2 + version: 1.52.2(eslint@9.30.1(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3) + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) eslint-plugin-react-compiler: specifier: 19.1.0-rc.2 - version: 19.1.0-rc.2(eslint@9.28.0(jiti@2.4.2)) + version: 19.1.0-rc.2(eslint@9.30.1(jiti@2.4.2)) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.28.0(jiti@2.4.2)) + version: 5.2.0(eslint@9.30.1(jiti@2.4.2)) react: specifier: ^19.1.0 version: 19.1.0 @@ -1299,13 +1411,16 @@ importers: '@tanstack/pacer': specifier: workspace:* version: link:../pacer + '@tanstack/solid-store': + specifier: ^0.7.3 + version: 0.7.3(solid-js@1.9.7) devDependencies: solid-js: specifier: ^1.9.7 version: 1.9.7 vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^2.11.7 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) packages: @@ -1323,16 +1438,24 @@ packages: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.26.8': resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + '@babel/core@7.26.10': resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} engines: {node: '>=6.9.0'} - '@babel/core@7.26.9': - resolution: {integrity: sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==} + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} engines: {node: '>=6.9.0'} '@babel/generator@7.26.9': @@ -1343,6 +1466,10 @@ packages: resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.25.9': resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} engines: {node: '>=6.9.0'} @@ -1351,12 +1478,20 @@ packages: resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.27.0': resolution: {integrity: sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + '@babel/helper-member-expression-to-functions@7.25.9': resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} engines: {node: '>=6.9.0'} @@ -1369,12 +1504,22 @@ packages: resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.26.0': resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.25.9': resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} engines: {node: '>=6.9.0'} @@ -1383,6 +1528,10 @@ packages: resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + '@babel/helper-replace-supers@7.26.5': resolution: {integrity: sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==} engines: {node: '>=6.9.0'} @@ -1397,22 +1546,34 @@ packages: resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.9': resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.26.9': - resolution: {integrity: sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} '@babel/helpers@7.27.0': resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} engines: {node: '>=6.9.0'} + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.26.9': resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} engines: {node: '>=6.0.0'} @@ -1423,6 +1584,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-proposal-private-methods@7.18.6': resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} @@ -1436,14 +1602,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.25.9': - resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.25.9': - resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1460,6 +1626,10 @@ packages: resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.26.9': resolution: {integrity: sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==} engines: {node: '>=6.9.0'} @@ -1468,6 +1638,10 @@ packages: resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + '@babel/types@7.26.9': resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} engines: {node: '>=6.9.0'} @@ -1476,17 +1650,21 @@ packages: resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.0': + resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + engines: {node: '>=6.9.0'} + '@changesets/apply-release-plan@7.0.12': resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==} - '@changesets/assemble-release-plan@6.0.8': - resolution: {integrity: sha512-y8+8LvZCkKJdbUlpXFuqcavpzJR80PN0OIfn8HZdwK7Sh6MgLXm4hKY5vu6/NDoKp8lAlM4ERZCqRMLxP4m+MQ==} + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.29.4': - resolution: {integrity: sha512-VW30x9oiFp/un/80+5jLeWgEU6Btj8IqOgI+X/zAYu4usVOWXjPIK5jSSlt5jsCU7/6Z7AxEkarxBxGUqkAmNg==} + '@changesets/cli@2.29.5': + resolution: {integrity: sha512-0j0cPq3fgxt2dPdFsg4XvO+6L66RC0pZybT9F4dG5TBrLA3jA/1pNkdTXH9IBBVHkgsKrNKenI3n1mPyPlIydg==} hasBin: true '@changesets/config@3.1.1': @@ -1501,8 +1679,8 @@ packages: '@changesets/get-github-info@0.6.0': resolution: {integrity: sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==} - '@changesets/get-release-plan@4.0.12': - resolution: {integrity: sha512-KukdEgaafnyGryUwpHG2kZ7xJquOmWWWk5mmoeQaSvZTWH1DC5D/Sw6ClgGFYtQnOMSQhgoEbDxAbpIIayKH1g==} + '@changesets/get-release-plan@4.0.13': + resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} @@ -1729,12 +1907,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.1': - resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1745,20 +1917,20 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-react/ast@1.51.2': - resolution: {integrity: sha512-ro3AjpT+PDl0CVYjbqKFIPZtsaco6+9vdvstKusTqzqG6cpq2Arzm1rZ86RFitgo4TUOJH1IWUB1TWm3z2q2Vw==} + '@eslint-react/ast@1.52.2': + resolution: {integrity: sha512-L0Tbbzx5l7JHgkQ1TqPWQuZ4+PsXDcgtt3056FOYqstUrDRG+5ylm7h3gEWu98I3FDdgLS8q9dOzz0PGgwZCTA==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint-react/core@1.51.2': - resolution: {integrity: sha512-qo8pvQc8DMJgiL0+M+7g9Z7KYFvTnA+Ph3yJlD7915UnITafgw570xeSfI5pDtJ7TN+3UJBRzclQelYC89v2+w==} + '@eslint-react/core@1.52.2': + resolution: {integrity: sha512-FpxKZJHlf3zXETNL+WQP/SoYuVQNheWm1iDgW68RyHygD8mzk9CnVLDgjMrfmh2n0eaOqnWCL/IC2YzD6VpYOQ==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint-react/eff@1.51.2': - resolution: {integrity: sha512-1cv83iz29cHYpeogwSwJQbZ4/3/0N9nd/856Wq2Opx783pvyrou8+43sOhytc4HL458tubj203I3wNEyyhhNnQ==} + '@eslint-react/eff@1.52.2': + resolution: {integrity: sha512-YBPE2J1+PfXrR9Ct+9rQsw8uRU06zHopI508cfj0usaIBf3hz18V2GoRTVhsjniP0QbvKQdHzyPmmS/B6uyMZQ==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint-react/eslint-plugin@1.51.2': - resolution: {integrity: sha512-wXSDrOvMi2DX8dTiXDDSyGkTm77xn/I3qLRYyn1gX/7GV6GPq+wzHF2aL4n8bRRdYmUBCD2ztLp9VaniKNT5YQ==} + '@eslint-react/eslint-plugin@1.52.2': + resolution: {integrity: sha512-e93chCIWTM6DiYpcuEpc7qDUP7bF7swG7Giq0J6S38czLJvtw9YeMaC9y1BL5rlFbmAcCybDm9QcRI55h/EuMw==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1767,24 +1939,24 @@ packages: typescript: optional: true - '@eslint-react/kit@1.51.2': - resolution: {integrity: sha512-+uip93wD1Qp6nvMdzv8JXkZw/dxMoJ95x4fGKNvTCBei4XaLjrdIueaUno4hgsSyzNptzOMxGnEyplSLo3ukuQ==} + '@eslint-react/kit@1.52.2': + resolution: {integrity: sha512-k0cSgFnPlDPI1xyRzHjEWIapLG0zCy7mx1HBLg5wuKf/zzSh3iNFId53xMebR05vM2k9YH63gsvTwRkGx/77Zw==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint-react/shared@1.51.2': - resolution: {integrity: sha512-+C1a0/+KMmTgbnTPsW3fMv0D24E6xPuFCwvyeg8MA2Xz046UpLz9u4ds5/T6ebGqM4c3EfN6iIxNV5RHz8P2Zw==} + '@eslint-react/shared@1.52.2': + resolution: {integrity: sha512-YHysVcCfmBoxt2+6Ao4HdLPUYNSem70gy+0yzOQvlQFSsGhh+uifQ68SSa/2uJBWfNUm9xQlyDsr2raeO4BlgA==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint-react/var@1.51.2': - resolution: {integrity: sha512-vHm2jsQ96aARePi1vbv3yTHSs8G3tZ6zeKsG30ntT2WzuRXAlG3VpuiANGVSkhSaaxMXm2tBUWl82GYYSzs7Vg==} + '@eslint-react/var@1.52.2': + resolution: {integrity: sha512-/7IYMPsmO0tIYqkqAVnkqB4eXeVBvgBL/a9hcGCO2eUSzslYzQHSzNPhIoPLD9HXng+0CWlT+KupOFIqP9a26A==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint/config-array@0.20.0': - resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.1': - resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.14.0': @@ -1795,14 +1967,14 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.27.0': - resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.28.0': resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.30.1': + resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1811,8 +1983,8 @@ packages: resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@faker-js/faker@9.8.0': - resolution: {integrity: sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==} + '@faker-js/faker@9.9.0': + resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} '@gerrit0/mini-shiki@1.27.2': @@ -1842,6 +2014,9 @@ packages: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1860,6 +2035,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -1903,53 +2081,53 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nx/nx-darwin-arm64@21.1.3': - resolution: {integrity: sha512-gbBKQrw9ecjXHVs7Kwaht5Dip//NBCgmnkf3GGoA40ad3zyvHDe+MBWMxueRToUVW/mDPh8b5lvLbmFApiY6sQ==} + '@nx/nx-darwin-arm64@21.2.2': + resolution: {integrity: sha512-qDF1SHW9UYzFQBRA3MGLYDPCU/j1ACasAdjv5kMXXBtmg+1WC3mZ/KO84wXJE7j9ImXOPKm9dmiW63LfXteXZw==} cpu: [arm64] os: [darwin] - '@nx/nx-darwin-x64@21.1.3': - resolution: {integrity: sha512-yGDWqxwNty1BJcuvZlwGGravAhg8eIRMEIp2omfIxeyfZEVA4b7egwMCqczwU2Li/StNjTtzrUe1HPWgcCVAuQ==} + '@nx/nx-darwin-x64@21.2.2': + resolution: {integrity: sha512-gdxOcfGonAD+eM5oKKd+2rcrGWmJOfON5HJpLkDfgOO/vyb6FUQub3xUu/JB2RAJ4r6iW/8JZxzheFDIiHDEug==} cpu: [x64] os: [darwin] - '@nx/nx-freebsd-x64@21.1.3': - resolution: {integrity: sha512-vpZPfSQgNIQ0vmnQA26DlJKZog20ISdS14ir234mvCaJJFdlgWGcpyEOSCU3Vg+32Z/VsSx7kIkBwRhfEZ73Ag==} + '@nx/nx-freebsd-x64@21.2.2': + resolution: {integrity: sha512-uO+k4AXGchOlzsoE3uljBKYlI84hv15R2CcLfXjbwrIw+0YZOIeZ/pDYNZMpOy1HePTuCVUxaYQCEBO7N2PI3w==} cpu: [x64] os: [freebsd] - '@nx/nx-linux-arm-gnueabihf@21.1.3': - resolution: {integrity: sha512-R2GzEyHvyree2m7w+e/MOZjUY/l99HbW4E/jJl5BBXRGEAnGTIx9fOxSDiOW5QK6U0oZb2YO2b565t+IC+7rBQ==} + '@nx/nx-linux-arm-gnueabihf@21.2.2': + resolution: {integrity: sha512-7ZaZKJNqQvvXs66GYdvY7kJoZ3wFnaIamjdlFYtH+5oQdCTqRTHb9HsB0/q6pf5nEDCEW/FJkXszKgCfViDZLA==} cpu: [arm] os: [linux] - '@nx/nx-linux-arm64-gnu@21.1.3': - resolution: {integrity: sha512-TlFT0G5gO6ujdkT7KUmvS2bwurvpV3olQwchqW1rQwuZ1eEQ1GVDuyzg49UG7lgESYruFn2HRhBf4V+iaD8WIw==} + '@nx/nx-linux-arm64-gnu@21.2.2': + resolution: {integrity: sha512-M1YuraXtzYTm/HXDAUWN7e009lWFTvpFF1Z38f7IuB07u76ARw1Fb/BcjVYHwt65QR70AcM7MQ5Fpq7PThHPkw==} cpu: [arm64] os: [linux] - '@nx/nx-linux-arm64-musl@21.1.3': - resolution: {integrity: sha512-YkdzrZ7p2Y0YpteRyT9lPKhfuz2t5rNFQ87x9WHK2/cFD6H6M42Fg2JldCPIVj2chN9liH+s5ougW5oPQpZyKw==} + '@nx/nx-linux-arm64-musl@21.2.2': + resolution: {integrity: sha512-raXkg8uijQFOgfKadUzwkFetyFb5pQbY0u6aLz0o9Eq5ml82B8ODrHwZdj2YLVNx2bB2Y0nq6R6HeYQRB94xIQ==} cpu: [arm64] os: [linux] - '@nx/nx-linux-x64-gnu@21.1.3': - resolution: {integrity: sha512-nnHxhakNCr4jR1y13g0yS/UOmn5aXkJ+ZA1R6jFQxIwLv3Ocy05i0ZvU7rPOtflluDberxEop8xzoiuEZXDa/w==} + '@nx/nx-linux-x64-gnu@21.2.2': + resolution: {integrity: sha512-je6D2kG8jCB72QVrYRXs4xRrU2g2zQREqODt+s1zI2lWlMDJcBwxDxGtlxXM3mDyeUGCh2s9nlkrA0GCTin1LQ==} cpu: [x64] os: [linux] - '@nx/nx-linux-x64-musl@21.1.3': - resolution: {integrity: sha512-poPt/LnFbq54CA3PZ1af8wcdQ4VsWRuA9w1Q1/G1BhCfDUAVIOZ0mhH1NzFpPwCxgVZ1TbNCZWhV2qjVRwQtlw==} + '@nx/nx-linux-x64-musl@21.2.2': + resolution: {integrity: sha512-ZDCNM0iBACq5Wgb1+JY20jMMRmxQKIDAoCrkxMciSAjh5s/1fGOboqWmKoztwW5g9QPJs/GdOojWbesu4B42eg==} cpu: [x64] os: [linux] - '@nx/nx-win32-arm64-msvc@21.1.3': - resolution: {integrity: sha512-gBSVMRkXRqxTKgj/dabAD1EaptROy64fEtlU1llPz/RtcJcVhIlDczBF/y2WSD6A72cSv6zF/F1n3NrekNSfBA==} + '@nx/nx-win32-arm64-msvc@21.2.2': + resolution: {integrity: sha512-jQRWpp2i5yAYD0FcZWZu6HMVxPWGEEa1DAf9wn7gHsORCehYH91GeOeVmaXcsPEg56uN+QhJhpIRIcDE5Ob4kw==} cpu: [arm64] os: [win32] - '@nx/nx-win32-x64-msvc@21.1.3': - resolution: {integrity: sha512-k3/1b2dLQjnWzrg2UqHDLCoaqEBx2SRgujjYCACRJ12vmYH2gTyFX2UPXikVbbpaTJNeXv8eaCzyCKhuvPK1sQ==} + '@nx/nx-win32-x64-msvc@21.2.2': + resolution: {integrity: sha512-qBrVdqYVRV1KQFyRtQbtic/R5ByH9F0kZJoQM3hSmcHgbg2s2+v9ivnaik4L6iX8FbAoCjYYm+J8L42yuOgCJA==} cpu: [x64] os: [win32] @@ -2022,8 +2200,8 @@ packages: resolution: {integrity: sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==} engines: {node: '>=18'} - '@rolldown/pluginutils@1.0.0-beta.9': - resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} + '@rolldown/pluginutils@1.0.0-beta.19': + resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} '@rollup/pluginutils@5.1.4': resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} @@ -2034,98 +2212,103 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.35.0': - resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==} + '@rollup/rollup-android-arm-eabi@4.44.1': + resolution: {integrity: sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.35.0': - resolution: {integrity: sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==} + '@rollup/rollup-android-arm64@4.44.1': + resolution: {integrity: sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.35.0': - resolution: {integrity: sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==} + '@rollup/rollup-darwin-arm64@4.44.1': + resolution: {integrity: sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.35.0': - resolution: {integrity: sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==} + '@rollup/rollup-darwin-x64@4.44.1': + resolution: {integrity: sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.35.0': - resolution: {integrity: sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==} + '@rollup/rollup-freebsd-arm64@4.44.1': + resolution: {integrity: sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.35.0': - resolution: {integrity: sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==} + '@rollup/rollup-freebsd-x64@4.44.1': + resolution: {integrity: sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.35.0': - resolution: {integrity: sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==} + '@rollup/rollup-linux-arm-gnueabihf@4.44.1': + resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.35.0': - resolution: {integrity: sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==} + '@rollup/rollup-linux-arm-musleabihf@4.44.1': + resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.35.0': - resolution: {integrity: sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==} + '@rollup/rollup-linux-arm64-gnu@4.44.1': + resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.35.0': - resolution: {integrity: sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==} + '@rollup/rollup-linux-arm64-musl@4.44.1': + resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.35.0': - resolution: {integrity: sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==} + '@rollup/rollup-linux-loongarch64-gnu@4.44.1': + resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': - resolution: {integrity: sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==} + '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': + resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.35.0': - resolution: {integrity: sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==} + '@rollup/rollup-linux-riscv64-gnu@4.44.1': + resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.44.1': + resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.35.0': - resolution: {integrity: sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==} + '@rollup/rollup-linux-s390x-gnu@4.44.1': + resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.35.0': - resolution: {integrity: sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==} + '@rollup/rollup-linux-x64-gnu@4.44.1': + resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.35.0': - resolution: {integrity: sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==} + '@rollup/rollup-linux-x64-musl@4.44.1': + resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.35.0': - resolution: {integrity: sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==} + '@rollup/rollup-win32-arm64-msvc@4.44.1': + resolution: {integrity: sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.35.0': - resolution: {integrity: sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==} + '@rollup/rollup-win32-ia32-msvc@4.44.1': + resolution: {integrity: sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.35.0': - resolution: {integrity: sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==} + '@rollup/rollup-win32-x64-msvc@4.44.1': + resolution: {integrity: sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==} cpu: [x64] os: [win32] @@ -2195,35 +2378,49 @@ packages: resolution: {integrity: sha512-08eKiDAjj4zLug1taXSIJ0kGL5cawjVCyJkBb6EWSg5fEPX6L+Wtr0CH2If4j5KYylz85iaZiFlUItvgJvll5g==} engines: {node: ^14.13.1 || ^16.0.0 || >=18} - '@tanstack/config@0.18.2': - resolution: {integrity: sha512-QuEq2Aky5pEJcDA7eZrnp4f2Fyq3X8iXPdcXmbPpanFXoBJYZCWVH9npkRuaNYNIuL2suFeaGzjwW4HhyWNq9g==} + '@tanstack/config@0.19.0': + resolution: {integrity: sha512-Ir9RF+J6BrNvPBe5t7Mh32Cj1GxCWE8sNrP0ItqhYRud7iwiKN2MNQWlMLDSkiigSh/r831n/Dfwk84qBLqjjw==} engines: {node: '>=18'} '@tanstack/eslint-config@0.2.0': resolution: {integrity: sha512-KUJUDvYFaqxekc8IwgokZ7+yJUoR7LPFu788VSfCxtsbqP/wZyppuoaToC/74LIFjBKIHJQN+YdvBFedD2fqJg==} engines: {node: '>=18'} - '@tanstack/publish-config@0.1.1': - resolution: {integrity: sha512-gw46t9d8fkf4ICU+EVcQdVWWRRux3K3+DM/vO3xp3KrU4/BlQOZgP9BFEWY7znU5pqD5rJ3ozrTbv9ezw78Qyw==} + '@tanstack/publish-config@0.2.0': + resolution: {integrity: sha512-RC0yRBFJvGuR58tKQUIkMXVEiATXgESIc+3/NTqoCC7D2YOF4fZGmHGYIanFEPQH7EGfQ5+Bwi+H6BOtKnymtw==} engines: {node: '>=18'} - '@tanstack/query-core@5.80.6': - resolution: {integrity: sha512-nl7YxT/TAU+VTf+e2zTkObGTyY8YZBMnbgeA1ee66lIVqzKlYursAII6z5t0e6rXgwUMJSV4dshBTNacNpZHbQ==} + '@tanstack/query-core@5.82.0': + resolution: {integrity: sha512-JrjoVuaajBQtnoWSg8iaPHaT4mW73lK2t+exxHNOSMqy0+13eKLqJgTKXKImLejQIfdAHQ6Un0njEhOvUtOd5w==} - '@tanstack/query-devtools@5.80.0': - resolution: {integrity: sha512-D6gH4asyjaoXrCOt5vG5Og/YSj0D/TxwNQgtLJIgWbhbWCC/emu2E92EFoVHh4ppVWg1qT2gKHvKyQBEFZhCuA==} + '@tanstack/query-devtools@5.81.2': + resolution: {integrity: sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==} - '@tanstack/react-query-devtools@5.80.6': - resolution: {integrity: sha512-y7Es0OJ4RYQxrPYsuuQP0jxjgJ40a03UbEPmJ6vwf/ERVMRoRIMkpjtvPxf1D+n9nwPfWmGdD0jW8Wxd+TxeEw==} + '@tanstack/react-query-devtools@5.82.0': + resolution: {integrity: sha512-MC05Zq3zr/59jhgF7dL6JSGPg1krbasDSizmRxjNcvxgh/sUTwRFD9CGN10YYX7LB6jq0ZpFtCjSVGdLiFrKAA==} peerDependencies: - '@tanstack/react-query': ^5.80.6 + '@tanstack/react-query': ^5.82.0 react: ^18 || ^19 - '@tanstack/react-query@5.80.6': - resolution: {integrity: sha512-izX+5CnkpON3NQGcEm3/d7LfFQNo9ZpFtX2QsINgCYK9LT2VCIdi8D3bMaMSNhrAJCznRoAkFic76uvLroALBw==} + '@tanstack/react-query@5.82.0': + resolution: {integrity: sha512-mnk8/ofKEthFeMdhV1dV8YXRf+9HqvXAcciXkoo755d/ocfWq7N/Y9jGOzS3h7ZW9dDGwSIhs3/HANWUBsyqYg==} peerDependencies: react: ^18 || ^19 + '@tanstack/react-store@0.7.3': + resolution: {integrity: sha512-3Dnqtbw9P2P0gw8uUM8WP2fFfg8XMDSZCTsywRPZe/XqqYW8PGkXKZTvP0AHkE4mpqP9Y43GpOg9vwO44azu6Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/solid-store@0.7.3': + resolution: {integrity: sha512-fEdiGQdlzG/eS4lOaIWvKp94hopVZiDZ2IgrQ9ntu5QtLPXOABq/2VDSQI7jkx43e3AdLdvlXVrljxw2BVB9tw==} + peerDependencies: + solid-js: ^1.6.0 + + '@tanstack/store@0.7.2': + resolution: {integrity: sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg==} + '@tanstack/typedoc-config@0.2.0': resolution: {integrity: sha512-1ak0ZirlLRxd3dNNOFnMoYORBeC83nK4C+OiXpE0dxsO8ZVrBqCtNCKr8SG+W9zICXcWGiFu9qYLsgNKTayOqw==} engines: {node: '>=18'} @@ -2266,6 +2463,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -2275,16 +2475,16 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@22.15.30': - resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} + '@types/node@24.0.13': + resolution: {integrity: sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==} '@types/react-dom@19.1.6': resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} peerDependencies: '@types/react': ^19.0.0 - '@types/react@19.1.6': - resolution: {integrity: sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==} + '@types/react@19.1.8': + resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -2310,20 +2510,32 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.31.1': - resolution: {integrity: sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==} + '@typescript-eslint/project-service@8.35.1': + resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/scope-manager@8.33.1': resolution: {integrity: sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.35.1': + resolution: {integrity: sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.33.1': resolution: {integrity: sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/tsconfig-utils@8.35.1': + resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/type-utils@8.33.1': resolution: {integrity: sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2331,19 +2543,20 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.31.1': - resolution: {integrity: sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==} + '@typescript-eslint/type-utils@8.35.1': + resolution: {integrity: sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/types@8.33.1': resolution: {integrity: sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.31.1': - resolution: {integrity: sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==} + '@typescript-eslint/types@8.35.1': + resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/typescript-estree@8.33.1': resolution: {integrity: sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==} @@ -2351,11 +2564,10 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.31.1': - resolution: {integrity: sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==} + '@typescript-eslint/typescript-estree@8.35.1': + resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/utils@8.33.1': @@ -2365,14 +2577,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.31.1': - resolution: {integrity: sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==} + '@typescript-eslint/utils@8.35.1': + resolution: {integrity: sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/visitor-keys@8.33.1': resolution: {integrity: sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.35.1': + resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@unrs/resolver-binding-darwin-arm64@1.7.11': resolution: {integrity: sha512-i3/wlWjQJXMh1uiGtiv7k1EYvrrS3L1hdwmWJJiz1D8jWy726YFYPIxQWbEIVPVAgrfRR0XNlLrTQwq17cuCGw==} cpu: [arm64] @@ -2458,17 +2677,17 @@ packages: cpu: [x64] os: [win32] - '@vitejs/plugin-react@4.5.1': - resolution: {integrity: sha512-uPZBqSI0YD4lpkIru6M35sIfylLGTyhGHvDZbNLuMA73lMlwJKz5xweH7FajfcCAc2HnINciejA9qTz0dr0M7A==} + '@vitejs/plugin-react@4.6.0': + resolution: {integrity: sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - '@vitest/expect@3.2.2': - resolution: {integrity: sha512-ipHw0z669vEMjzz3xQE8nJX1s0rQIb7oEl4jjl35qWTwm/KIHERIg/p/zORrjAaZKXfsv7IybcNGHwhOOAPMwQ==} + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/mocker@3.2.2': - resolution: {integrity: sha512-jKojcaRyIYpDEf+s7/dD3LJt53c0dPfp5zCPXz9H/kcGrSlovU/t1yEaNzM9oFME3dcd4ULwRI/x0Po1Zf+LTw==} + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 @@ -2478,20 +2697,20 @@ packages: vite: optional: true - '@vitest/pretty-format@3.2.2': - resolution: {integrity: sha512-FY4o4U1UDhO9KMd2Wee5vumwcaHw7Vg4V7yR4Oq6uK34nhEJOmdRYrk3ClburPRUA09lXD/oXWZ8y/Sdma0aUQ==} + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/runner@3.2.2': - resolution: {integrity: sha512-GYcHcaS3ejGRZYed2GAkvsjBeXIEerDKdX3orQrBJqLRiea4NSS9qvn9Nxmuy1IwIB+EjFOaxXnX79l8HFaBwg==} + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - '@vitest/snapshot@3.2.2': - resolution: {integrity: sha512-aMEI2XFlR1aNECbBs5C5IZopfi5Lb8QJZGGpzS8ZUHML5La5wCbrbhLOVSME68qwpT05ROEEOAZPRXFpxZV2wA==} + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - '@vitest/spy@3.2.2': - resolution: {integrity: sha512-6Utxlx3o7pcTxvp0u8kUiXtRFScMrUg28KjB3R2hon7w4YqOFAEA9QwzPVVS1QNL3smo4xRNOpNZClRVfpMcYg==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/utils@3.2.2': - resolution: {integrity: sha512-qJYMllrWpF/OYfWHP32T31QCaLa3BAzT/n/8mNGhPdVcjY+JYazQFO1nsJvXU12Kp1xMpNY4AGuljPTNjQve6A==} + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} '@volar/language-core@2.4.12': resolution: {integrity: sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA==} @@ -2547,6 +2766,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.3: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} @@ -3024,8 +3248,8 @@ packages: peerDependencies: eslint: '>=7' - eslint-plugin-react-debug@1.51.2: - resolution: {integrity: sha512-5PxCAjrFXl/rE+W7FF9JeNDtRlqBhBDw/YjOnj0bCG0vu9tCtqf7XJVsaAWsx18KisiQrBV7GsJvm/7gmQFTng==} + eslint-plugin-react-debug@1.52.2: + resolution: {integrity: sha512-9aJoZbC7VPhZ9ByKEg0R1ReDaltLGb9oLMwXL+oxoP4MFYQOL2BKNca+yfe74YZbSCOYidV1nsmCdTEQxh3nhg==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3034,8 +3258,8 @@ packages: typescript: optional: true - eslint-plugin-react-dom@1.51.2: - resolution: {integrity: sha512-4nm3HFMZCkuZjeHyPnjboq07aInTGtRf7HWBA/zgJOkS2+jo4bRt7V4GyFFFj24Zq4mcM+p0ZeWxbGK0E4Uu4A==} + eslint-plugin-react-dom@1.52.2: + resolution: {integrity: sha512-HDwQTwGfJTFAa4x0Bf9NH/TVHULEFjI0/vBNhkZt7JAHFb7v+SrhlXGUIIKfQTPHHJIAQZm8v3yzc5g/NlCokA==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3044,8 +3268,8 @@ packages: typescript: optional: true - eslint-plugin-react-hooks-extra@1.51.2: - resolution: {integrity: sha512-wo3KFmVDRx+4KkiC2iqsU544JG1vy4CIWstHcpdZ7fT3gEuRGiGFNlWOAQ0wH/RMvPwMXstc127FPS8R3sUL9A==} + eslint-plugin-react-hooks-extra@1.52.2: + resolution: {integrity: sha512-95vjCeNMGNZGFoBSwrvaAKfCDvHXXbrdiaizlCmD57AYTHALI9CzvEapQP9qjETNzuf5Uta0/kmRI5Ln4v2y6A==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3060,8 +3284,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-naming-convention@1.51.2: - resolution: {integrity: sha512-oFoxppmuYwj+D6NpxRmVyUjySnuqfsZltz1y+S0BYROA/XDWAdpLkqfVbsJQSHSsytgqk5wpHtsB2FD6Y0jelA==} + eslint-plugin-react-naming-convention@1.52.2: + resolution: {integrity: sha512-Nww0JUC5aq1Wj0ezuPylBfC4w+j3t3pvg0vR0b+OXjMVAttLQJURgXmAzpURJ1dQOrROLtEQGL4lLTeIAEJ3uQ==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3070,8 +3294,8 @@ packages: typescript: optional: true - eslint-plugin-react-web-api@1.51.2: - resolution: {integrity: sha512-nSJUX3vYTcXGW9VI3ingkQzDb8qC51Y/VGMKysCNFUq3ReXdMGtbW4j+UqsaTYbZmzai3BwI0eE4aAjBXEBivA==} + eslint-plugin-react-web-api@1.52.2: + resolution: {integrity: sha512-EAwSufPNZHWievnCGBRnpE9BcH351dZWTdnuLnDBOmoP5VJnfvaaxgupuFeGSYwM+emzA+0h8qZa/uwjG57TOw==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3080,8 +3304,8 @@ packages: typescript: optional: true - eslint-plugin-react-x@1.51.2: - resolution: {integrity: sha512-PW/93ckImql0/nDEzxFWpjucZe1FLNWs0sMT+T/HPT7WCs78IlAQ3y6Iz/18yonnl0sTOmDFqN06O7Py1+crGQ==} + eslint-plugin-react-x@1.52.2: + resolution: {integrity: sha512-Pxpf3YxCUcNgzJVT6blAJ2KvLX32pUxtXndaCZoTdiytFw/H9OZKq4Qczxx/Lpo9Ri5rm4FbIZL3BfL/HGmzBw==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3106,6 +3330,10 @@ packages: resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3114,8 +3342,12 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.28.0: - resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.30.1: + resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3131,6 +3363,10 @@ packages: resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -3199,8 +3435,8 @@ packages: picomatch: optional: true - fdir@6.4.4: - resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -3294,9 +3530,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-tsconfig@4.10.0: - resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} - get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} @@ -3520,6 +3753,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -3575,8 +3811,8 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - knip@5.60.2: - resolution: {integrity: sha512-TsYqEsoL3802RmhGL5MN7RLI6/03kocMYx/4BpMmwo3dSwEJxmzV7HqRxMVZr6c1llbd25+MqjgA86bv1IwsPA==} + knip@5.61.3: + resolution: {integrity: sha512-8iSz8i8ufIjuUwUKzEwye7ROAW0RzCze7T770bUiz0PKL+SSwbs4RS32fjMztLwcOzSsNPlXdUAeqmkdzXxJ1Q==} engines: {node: '>=18.18.0'} hasBin: true peerDependencies: @@ -3632,6 +3868,9 @@ packages: loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3729,8 +3968,8 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - nanoid@3.3.9: - resolution: {integrity: sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -3775,8 +4014,8 @@ packages: nwsapi@2.2.18: resolution: {integrity: sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA==} - nx@21.1.3: - resolution: {integrity: sha512-GZ7+Bve4xOVIk/hb9nN16fVqVq5PNNyFom1SCQbEGhGkyABJF8kA4JImCKhZpZyg1CtZeUrkPHK4xNO+rw9G5w==} + nx@21.2.2: + resolution: {integrity: sha512-SP+gojzJhvUfGPw94myECAvF+a7KDQe8c1HUr2HOPR20oSukpdhZM2B1Ki4FGUUuzOcCILhNT2QHLo82+FGLng==} hasBin: true peerDependencies: '@swc-node/register': ^1.8.0 @@ -3906,8 +4145,8 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -3930,8 +4169,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} hasBin: true @@ -3961,11 +4200,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-dom@19.0.0: - resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} - peerDependencies: - react: ^19.0.0 - react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: @@ -4042,8 +4276,8 @@ packages: peerDependencies: rollup: 2.x || 3.x || 4.x - rollup@4.35.0: - resolution: {integrity: sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==} + rollup@4.44.1: + resolution: {integrity: sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -4067,9 +4301,6 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.25.0: - resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} - scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -4082,11 +4313,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -4110,38 +4336,38 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - sherif-darwin-arm64@1.5.0: - resolution: {integrity: sha512-4BYYCEXVJ8+xqfnntxnyGcwgByjrHSeKOitPYdBPMB3aauptC4GUMeVqXRXf5JNbx3lRKg8bZdoq0QPnbsr6BQ==} + sherif-darwin-arm64@1.6.1: + resolution: {integrity: sha512-J15oBJcrnCAZ0rQE8WbMShYw3204A18akCH6C/uZrILTwX/vZyJIqi7lAt5L00LzsadA3HcyQqVjLNNCvuihoQ==} cpu: [arm64] os: [darwin] - sherif-darwin-x64@1.5.0: - resolution: {integrity: sha512-Y8wkqXzpZt2D5n0KnEXKK1QosoPofDpx4IDzxjyVjGvhR4YBoKcyU93tY9mEyYXMlzQGlyezGHpZkFeP0bioDA==} + sherif-darwin-x64@1.6.1: + resolution: {integrity: sha512-oLA/GtvUasi+qCl35LczOhQ4g/xY2mxE5/eiTYQGT3Ow7FKLscnkE6v5l28bgkFeR/uke0AgZ/CgHhozAf0ulg==} cpu: [x64] os: [darwin] - sherif-linux-arm64@1.5.0: - resolution: {integrity: sha512-l4JBoYCD+mJkwxNuQhLXloySXqSNOdTwJ9euBw8GbRjW+iXK2LxFOya1SlZy8CXZngTOBpNNoO9jH4E9NgI0XA==} + sherif-linux-arm64@1.6.1: + resolution: {integrity: sha512-OoltlucT7v9BZdkYZRbs1QU0DYMCQ5qgpMqQdMW1Rq3w3amr7+oEiV9NHntD83udOo8xRxKq0uPXfNYu+VptJw==} cpu: [arm64] os: [linux] - sherif-linux-x64@1.5.0: - resolution: {integrity: sha512-RaSRnYe58Y6yLTeE8Hw5xmEDn7ted1F4kT1FFSR+ola36R5lnCeWV+xZqjKWU7JE8fVSqApJ4EL0mwtwGZT98Q==} + sherif-linux-x64@1.6.1: + resolution: {integrity: sha512-qyDyYqpi3ABGkRuCnjnxN3OMT8DxMiiLzhS9p9xC05Y9nr5hjkxvqP4DdJ4e5opm4E7vzRAS7VQoZ6m7h6tsgQ==} cpu: [x64] os: [linux] - sherif-windows-arm64@1.5.0: - resolution: {integrity: sha512-xcOnhsCTcg0fg1/SYhFN4AErk1k8QfW4sRK8ziaEt/SDtUGIYmt/+3CsWCBIGoMSaURS8upcU2JtofI3P82qHQ==} + sherif-windows-arm64@1.6.1: + resolution: {integrity: sha512-wAbCiqP//lo7bZUlHmZUV3/sGjnJxo6QB5/fqhz5/GUeWh4CTyvlSacJKZxLnXnzpiUSeFnWutquWnHkRov5Ug==} cpu: [arm64] os: [win32] - sherif-windows-x64@1.5.0: - resolution: {integrity: sha512-kWFctc7kEgYq9IoTFWVGQdZv725eMOwhkF3JpkuApMLF/K+pDb1JKHUbmU52nHdMMKpT3SDVqrHyCaUOKmxU5w==} + sherif-windows-x64@1.6.1: + resolution: {integrity: sha512-2r0qMxZGCMO2aq8Hlq7npxtAsUFVDsEFtUM/6dFo1npa/jHe2mbU7ii/Ymy0bloSa/qw/azrSfRV6GLU7Gjtxg==} cpu: [x64] os: [win32] - sherif@1.5.0: - resolution: {integrity: sha512-sCn0w5b3APLRJauj4UDaK5TV5fyuc1ndDGv2cgt0lnb7zVUAekFm6D2duHVg4sf8/ZlDGFcLRc9Y/DxSRXdOAA==} + sherif@1.6.1: + resolution: {integrity: sha512-ZnwyTnmXoUOPClkOA37JWIyFxCoozMGHmhk/p7XbTREI554XXCnBAn3BMX8UsqkhSzQ9eNQsq4U+jnImEIppsQ==} hasBin: true siginfo@2.0.0: @@ -4154,8 +4380,8 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-git@3.27.0: - resolution: {integrity: sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==} + simple-git@3.28.0: + resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==} size-limit@11.2.0: resolution: {integrity: sha512-2kpQq2DD/pRpx3Tal/qRW1SYwcIeQ0iq8li5CJHQgOC+FtPn2BVmuDtzUCgNnpCrbgtfEHqh+iWzxK+Tq6C+RQ==} @@ -4240,6 +4466,9 @@ packages: resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} engines: {node: '>=14.16'} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -4288,16 +4517,12 @@ packages: resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} - tinyglobby@0.2.13: - resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} - tinypool@1.1.0: - resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} tinyrainbow@2.0.0: @@ -4342,12 +4567,6 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@2.0.1: - resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -4429,8 +4648,8 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} undici@6.21.3: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} @@ -4456,14 +4675,19 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} validate-html-nesting@1.2.2: resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} - vite-node@3.2.2: - resolution: {integrity: sha512-Xj/jovjZvDXOq2FgLXu8NsY4uHUMWtzVmMC2LkCu9HWdr9Qu1Is5sanX3Z4jOFKdohfaWDnEJWp9pRP0vVpAcA==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -4482,8 +4706,8 @@ packages: peerDependencies: vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 - vite-plugin-solid@2.11.6: - resolution: {integrity: sha512-Sl5CTqJTGyEeOsmdH6BOgalIZlwH3t4/y0RQuFLMGnvWMBvxb4+lq7x3BSiAw6etf0QexfNJW7HSOO/Qf7pigg==} + vite-plugin-solid@2.11.7: + resolution: {integrity: sha512-5TgK1RnE449g0Ryxb9BXqem89RSy7fE8XGVCo+Gw84IHgPuPVP7nYNP6WBVAaY/0xw+OqfdQee+kusL0y3XYNg==} peerDependencies: '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* solid-js: ^1.7.2 @@ -4500,19 +4724,19 @@ packages: vite: optional: true - vite@6.3.5: - resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vite@7.0.4: + resolution: {integrity: sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@types/node': ^20.19.0 || >=22.12.0 jiti: '>=1.21.0' - less: '*' + less: ^4.0.0 lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 @@ -4548,16 +4772,16 @@ packages: vite: optional: true - vitest@3.2.2: - resolution: {integrity: sha512-fyNn/Rp016Bt5qvY0OQvIUCwW2vnaEBLxP42PmKbNIoasSYjML+8xyeADOPvBe+Xfl/ubIw4og7Lt9jflRsCNw==} + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.2 - '@vitest/ui': 3.2.2 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -4700,6 +4924,9 @@ packages: zod@3.25.56: resolution: {integrity: sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==} + zod@3.25.72: + resolution: {integrity: sha512-Cl+fe4dNL4XumOBNBsr0lHfA80PQiZXHI4xEMTEr8gt6aGz92t3lBA32e71j9+JeF/VAYvdfBnuwJs+BMx/BrA==} + snapshots: '@adobe/css-tools@4.4.2': {} @@ -4723,8 +4950,16 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.26.8': {} + '@babel/compat-data@7.28.0': {} + '@babel/core@7.26.10': dependencies: '@ampproject/remapping': 2.3.0 @@ -4745,20 +4980,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@7.26.9': + '@babel/core@7.28.0': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.9 - '@babel/helper-compilation-targets': 7.26.5 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.9) - '@babel/helpers': 7.26.9 - '@babel/parser': 7.26.9 - '@babel/template': 7.26.9 - '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4781,6 +5016,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.25.9': dependencies: '@babel/types': 7.27.0 @@ -4793,6 +5036,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.27.0(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -4806,6 +5057,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-globals@7.28.0': {} + '@babel/helper-member-expression-to-functions@7.25.9': dependencies: '@babel/traverse': 7.27.0 @@ -4824,6 +5077,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -4833,12 +5093,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.9)': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.26.9 + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 transitivePeerDependencies: - supports-color @@ -4848,6 +5108,8 @@ snapshots: '@babel/helper-plugin-utils@7.26.5': {} + '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-replace-supers@7.26.5(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -4866,20 +5128,26 @@ snapshots: '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-option@7.25.9': {} - '@babel/helpers@7.26.9': - dependencies: - '@babel/template': 7.26.9 - '@babel/types': 7.26.9 + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.27.0': dependencies: '@babel/template': 7.27.0 '@babel/types': 7.27.0 + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + '@babel/parser@7.26.9': dependencies: '@babel/types': 7.26.9 @@ -4888,6 +5156,10 @@ snapshots: dependencies: '@babel/types': 7.27.0 + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.0 + '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -4896,20 +5168,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.9)': + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 '@babel/runtime@7.26.10': dependencies: @@ -4927,6 +5199,12 @@ snapshots: '@babel/parser': 7.27.0 '@babel/types': 7.27.0 + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@babel/traverse@7.26.9': dependencies: '@babel/code-frame': 7.26.2 @@ -4951,6 +5229,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + '@babel/types@7.26.9': dependencies: '@babel/helper-string-parser': 7.25.9 @@ -4961,6 +5251,11 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.28.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@changesets/apply-release-plan@7.0.12': dependencies: '@changesets/config': 3.1.1 @@ -4975,30 +5270,30 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.7.1 + semver: 7.7.2 - '@changesets/assemble-release-plan@6.0.8': + '@changesets/assemble-release-plan@6.0.9': dependencies: '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 - semver: 7.7.1 + semver: 7.7.2 '@changesets/changelog-git@0.2.1': dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.4': + '@changesets/cli@2.29.5': dependencies: '@changesets/apply-release-plan': 7.0.12 - '@changesets/assemble-release-plan': 6.0.8 + '@changesets/assemble-release-plan': 6.0.9 '@changesets/changelog-git': 0.2.1 '@changesets/config': 3.1.1 '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.12 + '@changesets/get-release-plan': 4.0.13 '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 '@changesets/pre': 2.0.2 @@ -5017,7 +5312,7 @@ snapshots: package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.7.1 + semver: 7.7.2 spawndamnit: 3.0.1 term-size: 2.2.1 @@ -5040,7 +5335,7 @@ snapshots: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.7.1 + semver: 7.7.2 '@changesets/get-github-info@0.6.0': dependencies: @@ -5049,9 +5344,9 @@ snapshots: transitivePeerDependencies: - encoding - '@changesets/get-release-plan@4.0.12': + '@changesets/get-release-plan@4.0.13': dependencies: - '@changesets/assemble-release-plan': 6.0.8 + '@changesets/assemble-release-plan': 6.0.9 '@changesets/config': 3.1.1 '@changesets/pre': 2.0.2 '@changesets/read': 0.6.5 @@ -5229,24 +5524,19 @@ snapshots: '@esbuild/win32-x64@0.25.0': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.28.0(jiti@2.4.2))': - dependencies: - eslint: 9.28.0(jiti@2.4.2) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.7.0(eslint@9.28.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.4.2))': dependencies: - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint-react/ast@1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@eslint-react/ast@1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-react/eff': 1.51.2 - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) string-ts: 2.2.1 ts-pattern: 5.7.1 transitivePeerDependencies: @@ -5254,17 +5544,17 @@ snapshots: - supports-color - typescript - '@eslint-react/core@1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': - dependencies: - '@eslint-react/ast': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.51.2 - '@eslint-react/kit': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core@1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-react/ast': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@eslint-react/kit': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) birecord: 0.1.1 ts-pattern: 5.7.1 transitivePeerDependencies: @@ -5272,60 +5562,60 @@ snapshots: - supports-color - typescript - '@eslint-react/eff@1.51.2': {} - - '@eslint-react/eslint-plugin@1.51.2(eslint@9.28.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3)': - dependencies: - '@eslint-react/eff': 1.51.2 - '@eslint-react/kit': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) - eslint-plugin-react-debug: 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-react-dom: 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-react-hooks-extra: 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-react-naming-convention: 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-react-web-api: 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-react-x: 1.51.2(eslint@9.28.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3) + '@eslint-react/eff@1.52.2': {} + + '@eslint-react/eslint-plugin@1.52.2(eslint@9.30.1(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3)': + dependencies: + '@eslint-react/eff': 1.52.2 + '@eslint-react/kit': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) + eslint-plugin-react-debug: 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-dom: 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-hooks-extra: 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-naming-convention: 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-web-api: 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-x: 1.52.2(eslint@9.30.1(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - supports-color - ts-api-utils - '@eslint-react/kit@1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@eslint-react/kit@1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-react/eff': 1.51.2 - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) ts-pattern: 5.7.1 - zod: 3.25.56 + zod: 3.25.72 transitivePeerDependencies: - eslint - supports-color - typescript - '@eslint-react/shared@1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@eslint-react/shared@1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-react/eff': 1.51.2 - '@eslint-react/kit': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@eslint-react/kit': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) ts-pattern: 5.7.1 - zod: 3.25.56 + zod: 3.25.72 transitivePeerDependencies: - eslint - supports-color - typescript - '@eslint-react/var@1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@eslint-react/var@1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-react/ast': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.51.2 - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/ast': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) string-ts: 2.2.1 ts-pattern: 5.7.1 transitivePeerDependencies: @@ -5333,15 +5623,15 @@ snapshots: - supports-color - typescript - '@eslint/config-array@0.20.0': + '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0 + debug: 4.4.1 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.1': {} + '@eslint/config-helpers@0.3.0': {} '@eslint/core@0.14.0': dependencies: @@ -5350,8 +5640,8 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.0 - espree: 10.3.0 + debug: 4.4.1 + espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 @@ -5361,10 +5651,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.27.0': {} - '@eslint/js@9.28.0': {} + '@eslint/js@9.30.1': {} + '@eslint/object-schema@2.1.6': {} '@eslint/plugin-kit@0.3.1': @@ -5372,7 +5662,7 @@ snapshots: '@eslint/core': 0.14.0 levn: 0.4.1 - '@faker-js/faker@9.8.0': {} + '@faker-js/faker@9.9.0': {} '@gerrit0/mini-shiki@1.27.2': dependencies: @@ -5397,6 +5687,11 @@ snapshots: dependencies: '@sinclair/typebox': 0.27.8 + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -5414,9 +5709,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -5438,23 +5738,23 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@microsoft/api-extractor-model@7.29.6(@types/node@22.15.30)': + '@microsoft/api-extractor-model@7.29.6(@types/node@24.0.13)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.7.0(@types/node@22.15.30) + '@rushstack/node-core-library': 5.7.0(@types/node@24.0.13) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.47.7(@types/node@22.15.30)': + '@microsoft/api-extractor@7.47.7(@types/node@24.0.13)': dependencies: - '@microsoft/api-extractor-model': 7.29.6(@types/node@22.15.30) + '@microsoft/api-extractor-model': 7.29.6(@types/node@24.0.13) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.7.0(@types/node@22.15.30) + '@rushstack/node-core-library': 5.7.0(@types/node@24.0.13) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.14.0(@types/node@22.15.30) - '@rushstack/ts-command-line': 4.22.6(@types/node@22.15.30) + '@rushstack/terminal': 0.14.0(@types/node@24.0.13) + '@rushstack/ts-command-line': 4.22.6(@types/node@24.0.13) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.10 @@ -5498,34 +5798,34 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nx/nx-darwin-arm64@21.1.3': + '@nx/nx-darwin-arm64@21.2.2': optional: true - '@nx/nx-darwin-x64@21.1.3': + '@nx/nx-darwin-x64@21.2.2': optional: true - '@nx/nx-freebsd-x64@21.1.3': + '@nx/nx-freebsd-x64@21.2.2': optional: true - '@nx/nx-linux-arm-gnueabihf@21.1.3': + '@nx/nx-linux-arm-gnueabihf@21.2.2': optional: true - '@nx/nx-linux-arm64-gnu@21.1.3': + '@nx/nx-linux-arm64-gnu@21.2.2': optional: true - '@nx/nx-linux-arm64-musl@21.1.3': + '@nx/nx-linux-arm64-musl@21.2.2': optional: true - '@nx/nx-linux-x64-gnu@21.1.3': + '@nx/nx-linux-x64-gnu@21.2.2': optional: true - '@nx/nx-linux-x64-musl@21.1.3': + '@nx/nx-linux-x64-musl@21.2.2': optional: true - '@nx/nx-win32-arm64-msvc@21.1.3': + '@nx/nx-win32-arm64-msvc@21.2.2': optional: true - '@nx/nx-win32-x64-msvc@21.1.3': + '@nx/nx-win32-x64-msvc@21.2.2': optional: true '@oxc-resolver/binding-darwin-arm64@11.1.0': @@ -5571,74 +5871,77 @@ snapshots: '@publint/pack@0.1.2': {} - '@rolldown/pluginutils@1.0.0-beta.9': {} + '@rolldown/pluginutils@1.0.0-beta.19': {} - '@rollup/pluginutils@5.1.4(rollup@4.35.0)': + '@rollup/pluginutils@5.1.4(rollup@4.44.1)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.35.0 + rollup: 4.44.1 - '@rollup/rollup-android-arm-eabi@4.35.0': + '@rollup/rollup-android-arm-eabi@4.44.1': optional: true - '@rollup/rollup-android-arm64@4.35.0': + '@rollup/rollup-android-arm64@4.44.1': optional: true - '@rollup/rollup-darwin-arm64@4.35.0': + '@rollup/rollup-darwin-arm64@4.44.1': optional: true - '@rollup/rollup-darwin-x64@4.35.0': + '@rollup/rollup-darwin-x64@4.44.1': optional: true - '@rollup/rollup-freebsd-arm64@4.35.0': + '@rollup/rollup-freebsd-arm64@4.44.1': optional: true - '@rollup/rollup-freebsd-x64@4.35.0': + '@rollup/rollup-freebsd-x64@4.44.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.35.0': + '@rollup/rollup-linux-arm-gnueabihf@4.44.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.35.0': + '@rollup/rollup-linux-arm-musleabihf@4.44.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.35.0': + '@rollup/rollup-linux-arm64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.35.0': + '@rollup/rollup-linux-arm64-musl@4.44.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.35.0': + '@rollup/rollup-linux-loongarch64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.35.0': + '@rollup/rollup-linux-riscv64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.35.0': + '@rollup/rollup-linux-riscv64-musl@4.44.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.35.0': + '@rollup/rollup-linux-s390x-gnu@4.44.1': optional: true - '@rollup/rollup-linux-x64-musl@4.35.0': + '@rollup/rollup-linux-x64-gnu@4.44.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.35.0': + '@rollup/rollup-linux-x64-musl@4.44.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.35.0': + '@rollup/rollup-win32-arm64-msvc@4.44.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.35.0': + '@rollup/rollup-win32-ia32-msvc@4.44.1': optional: true - '@rushstack/node-core-library@5.7.0(@types/node@22.15.30)': + '@rollup/rollup-win32-x64-msvc@4.44.1': + optional: true + + '@rushstack/node-core-library@5.7.0(@types/node@24.0.13)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -5649,23 +5952,23 @@ snapshots: resolve: 1.22.10 semver: 7.5.4 optionalDependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.13 '@rushstack/rig-package@0.5.3': dependencies: resolve: 1.22.10 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.14.0(@types/node@22.15.30)': + '@rushstack/terminal@0.14.0(@types/node@24.0.13)': dependencies: - '@rushstack/node-core-library': 5.7.0(@types/node@22.15.30) + '@rushstack/node-core-library': 5.7.0(@types/node@24.0.13) supports-color: 8.1.1 optionalDependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.13 - '@rushstack/ts-command-line@4.22.6(@types/node@22.15.30)': + '@rushstack/ts-command-line@4.22.6(@types/node@24.0.13)': dependencies: - '@rushstack/terminal': 0.14.0(@types/node@22.15.30) + '@rushstack/terminal': 0.14.0(@types/node@24.0.13) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -5702,10 +6005,10 @@ snapshots: '@size-limit/file': 11.2.0(size-limit@11.2.0) size-limit: 11.2.0 - '@stylistic/eslint-plugin@4.4.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@stylistic/eslint-plugin@4.4.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + '@typescript-eslint/utils': 8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) eslint-visitor-keys: 4.2.0 espree: 10.3.0 estraverse: 5.3.0 @@ -5714,9 +6017,9 @@ snapshots: - supports-color - typescript - '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)': + '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': dependencies: - acorn: 8.14.1 + acorn: 8.15.0 '@svitejs/changesets-changelog-github-compact@1.2.0': dependencies: @@ -5725,12 +6028,12 @@ snapshots: transitivePeerDependencies: - encoding - '@tanstack/config@0.18.2(@types/node@22.15.30)(@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0(jiti@2.4.2))(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': + '@tanstack/config@0.19.0(@types/node@24.0.13)(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.30.1(jiti@2.4.2))(rollup@4.44.1)(typescript@5.8.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: - '@tanstack/eslint-config': 0.2.0(@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@tanstack/publish-config': 0.1.1 + '@tanstack/eslint-config': 0.2.0(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@tanstack/publish-config': 0.2.0 '@tanstack/typedoc-config': 0.2.0(typescript@5.8.3) - '@tanstack/vite-config': 0.2.0(@types/node@22.15.30)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + '@tanstack/vite-config': 0.2.0(@types/node@24.0.13)(rollup@4.44.1)(typescript@5.8.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) transitivePeerDependencies: - '@types/node' - '@typescript-eslint/utils' @@ -5741,15 +6044,15 @@ snapshots: - typescript - vite - '@tanstack/eslint-config@0.2.0(@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@tanstack/eslint-config@0.2.0(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint/js': 9.27.0 - '@stylistic/eslint-plugin': 4.4.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-import-x: 4.15.1(@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0(jiti@2.4.2)) - eslint-plugin-n: 17.19.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@eslint/js': 9.28.0 + '@stylistic/eslint-plugin': 4.4.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-import-x: 4.15.1(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-n: 17.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) globals: 16.2.0 - typescript-eslint: 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - vue-eslint-parser: 10.1.3(eslint@9.28.0(jiti@2.4.2)) + typescript-eslint: 8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + vue-eslint-parser: 10.1.3(eslint@9.30.1(jiti@2.4.2)) transitivePeerDependencies: - '@typescript-eslint/utils' - eslint @@ -5757,30 +6060,44 @@ snapshots: - supports-color - typescript - '@tanstack/publish-config@0.1.1': + '@tanstack/publish-config@0.2.0': dependencies: '@commitlint/parse': 19.8.1 jsonfile: 6.1.0 semver: 7.7.2 - simple-git: 3.27.0 + simple-git: 3.28.0 transitivePeerDependencies: - supports-color - '@tanstack/query-core@5.80.6': {} + '@tanstack/query-core@5.82.0': {} - '@tanstack/query-devtools@5.80.0': {} + '@tanstack/query-devtools@5.81.2': {} - '@tanstack/react-query-devtools@5.80.6(@tanstack/react-query@5.80.6(react@19.1.0))(react@19.1.0)': + '@tanstack/react-query-devtools@5.82.0(@tanstack/react-query@5.82.0(react@19.1.0))(react@19.1.0)': dependencies: - '@tanstack/query-devtools': 5.80.0 - '@tanstack/react-query': 5.80.6(react@19.1.0) + '@tanstack/query-devtools': 5.81.2 + '@tanstack/react-query': 5.82.0(react@19.1.0) react: 19.1.0 - '@tanstack/react-query@5.80.6(react@19.1.0)': + '@tanstack/react-query@5.82.0(react@19.1.0)': dependencies: - '@tanstack/query-core': 5.80.6 + '@tanstack/query-core': 5.82.0 react: 19.1.0 + '@tanstack/react-store@0.7.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/store': 0.7.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + use-sync-external-store: 1.5.0(react@19.1.0) + + '@tanstack/solid-store@0.7.3(solid-js@1.9.7)': + dependencies: + '@tanstack/store': 0.7.2 + solid-js: 1.9.7 + + '@tanstack/store@0.7.2': {} + '@tanstack/typedoc-config@0.2.0(typescript@5.8.3)': dependencies: typedoc: 0.27.9(typescript@5.8.3) @@ -5789,12 +6106,12 @@ snapshots: transitivePeerDependencies: - typescript - '@tanstack/vite-config@0.2.0(@types/node@22.15.30)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': + '@tanstack/vite-config@0.2.0(@types/node@24.0.13)(rollup@4.44.1)(typescript@5.8.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: - rollup-plugin-preserve-directives: 0.4.0(rollup@4.35.0) - vite-plugin-dts: 4.2.3(@types/node@22.15.30)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) - vite-plugin-externalize-deps: 0.9.0(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) - vite-tsconfig-paths: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + rollup-plugin-preserve-directives: 0.4.0(rollup@4.44.1) + vite-plugin-dts: 4.2.3(@types/node@24.0.13)(rollup@4.44.1)(typescript@5.8.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite-plugin-externalize-deps: 0.9.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite-tsconfig-paths: 5.1.4(typescript@5.8.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) transitivePeerDependencies: - '@types/node' - rollup @@ -5820,24 +6137,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.27.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.27.0 '@types/chai@5.2.2': dependencies: @@ -5845,12 +6162,14 @@ snapshots: '@types/conventional-commits-parser@5.0.1': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.13 '@types/deep-eql@4.0.2': {} '@types/estree@1.0.6': {} + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -5859,29 +6178,29 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@22.15.30': + '@types/node@24.0.13': dependencies: - undici-types: 6.21.0 + undici-types: 7.8.0 - '@types/react-dom@19.1.6(@types/react@19.1.6)': + '@types/react-dom@19.1.6(@types/react@19.1.8)': dependencies: - '@types/react': 19.1.6 + '@types/react': 19.1.8 - '@types/react@19.1.6': + '@types/react@19.1.8': dependencies: csstype: 3.1.3 '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.33.1 - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -5890,14 +6209,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.33.1 '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.0 - eslint: 9.28.0(jiti@2.4.2) + debug: 4.4.1 + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -5906,102 +6225,128 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) '@typescript-eslint/types': 8.33.1 - debug: 4.4.0 + debug: 4.4.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.31.1': + '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.31.1 - '@typescript-eslint/visitor-keys': 8.31.1 + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color '@typescript-eslint/scope-manager@8.33.1': dependencies: '@typescript-eslint/types': 8.33.1 '@typescript-eslint/visitor-keys': 8.33.1 + '@typescript-eslint/scope-manager@8.35.1': + dependencies: + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 + '@typescript-eslint/tsconfig-utils@8.33.1(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.0 - eslint: 9.28.0(jiti@2.4.2) + '@typescript-eslint/utils': 8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.1 + eslint: 9.30.1(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.31.1': {} - - '@typescript-eslint/types@8.33.1': {} - - '@typescript-eslint/typescript-estree@8.31.1(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.31.1 - '@typescript-eslint/visitor-keys': 8.31.1 - debug: 4.4.0 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.1 - ts-api-utils: 2.0.1(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.1 + eslint: 9.30.1(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color + '@typescript-eslint/types@8.33.1': {} + + '@typescript-eslint/types@8.35.1': {} + '@typescript-eslint/typescript-estree@8.33.1(typescript@5.8.3)': dependencies: '@typescript-eslint/project-service': 8.33.1(typescript@5.8.3) '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) '@typescript-eslint/types': 8.33.1 '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.0 + debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.1 + semver: 7.7.2 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.31.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.31.1 - '@typescript-eslint/types': 8.31.1 - '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.33.1 '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.31.1': + '@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.31.1 - eslint-visitor-keys: 4.2.0 + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color '@typescript-eslint/visitor-keys@8.33.1': dependencies: '@typescript-eslint/types': 8.33.1 eslint-visitor-keys: 4.2.0 + '@typescript-eslint/visitor-keys@8.35.1': + dependencies: + '@typescript-eslint/types': 8.35.1 + eslint-visitor-keys: 4.2.1 + '@unrs/resolver-binding-darwin-arm64@1.7.11': optional: true @@ -6055,57 +6400,58 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.7.11': optional: true - '@vitejs/plugin-react@4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': + '@vitejs/plugin-react@4.6.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: - '@babel/core': 7.26.10 - '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) - '@rolldown/pluginutils': 1.0.0-beta.9 + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@rolldown/pluginutils': 1.0.0-beta.19 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color - '@vitest/expect@3.2.2': + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 - '@vitest/spy': 3.2.2 - '@vitest/utils': 3.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': + '@vitest/mocker@3.2.4(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: - '@vitest/spy': 3.2.2 + '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - '@vitest/pretty-format@3.2.2': + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.2.2': + '@vitest/runner@3.2.4': dependencies: - '@vitest/utils': 3.2.2 + '@vitest/utils': 3.2.4 pathe: 2.0.3 + strip-literal: 3.0.0 - '@vitest/snapshot@3.2.2': + '@vitest/snapshot@3.2.4': dependencies: - '@vitest/pretty-format': 3.2.2 + '@vitest/pretty-format': 3.2.4 magic-string: 0.30.17 pathe: 2.0.3 - '@vitest/spy@3.2.2': + '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.3 - '@vitest/utils@3.2.2': + '@vitest/utils@3.2.4': dependencies: - '@vitest/pretty-format': 3.2.2 - loupe: 3.1.3 + '@vitest/pretty-format': 3.2.4 + loupe: 3.1.4 tinyrainbow: 2.0.0 '@volar/language-core@2.4.12': @@ -6173,8 +6519,14 @@ snapshots: dependencies: acorn: 8.14.1 + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn@8.14.1: {} + acorn@8.15.0: {} + agent-base@7.1.3: {} ajv-draft-04@1.0.0(ajv@8.13.0): @@ -6242,20 +6594,20 @@ snapshots: axobject-query@4.1.0: {} - babel-plugin-jsx-dom-expressions@0.39.7(@babel/core@7.26.9): + babel-plugin-jsx-dom-expressions@0.39.7(@babel/core@7.26.10): dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@babel/helper-module-imports': 7.18.6 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.9) + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) '@babel/types': 7.27.0 html-entities: 2.3.3 parse5: 7.2.1 validate-html-nesting: 1.2.2 - babel-preset-solid@1.9.5(@babel/core@7.26.9): + babel-preset-solid@1.9.5(@babel/core@7.26.10): dependencies: - '@babel/core': 7.26.9 - babel-plugin-jsx-dom-expressions: 0.39.7(@babel/core@7.26.9) + '@babel/core': 7.26.10 + babel-plugin-jsx-dom-expressions: 0.39.7(@babel/core@7.26.10) balanced-match@1.0.2: {} @@ -6605,10 +6957,10 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.28.0(jiti@2.4.2)): + eslint-compat-utils@0.5.1(eslint@9.30.1(jiti@2.4.2)): dependencies: - eslint: 9.28.0(jiti@2.4.2) - semver: 7.7.1 + eslint: 9.30.1(jiti@2.4.2) + semver: 7.7.2 eslint-import-context@0.1.8(unrs-resolver@1.7.11): dependencies: @@ -6626,19 +6978,19 @@ snapshots: - supports-color optional: true - eslint-plugin-es-x@7.8.0(eslint@9.28.0(jiti@2.4.2)): + eslint-plugin-es-x@7.8.0(eslint@9.30.1(jiti@2.4.2)): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 - eslint: 9.28.0(jiti@2.4.2) - eslint-compat-utils: 0.5.1(eslint@9.28.0(jiti@2.4.2)) + eslint: 9.30.1(jiti@2.4.2) + eslint-compat-utils: 0.5.1(eslint@9.30.1(jiti@2.4.2)) - eslint-plugin-import-x@4.15.1(@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0(jiti@2.4.2)): + eslint-plugin-import-x@4.15.1(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.30.1(jiti@2.4.2)): dependencies: '@typescript-eslint/types': 8.33.1 comment-parser: 1.4.1 debug: 4.4.1 - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) eslint-import-context: 0.1.8(unrs-resolver@1.7.11) is-glob: 4.0.3 minimatch: 9.0.5 @@ -6646,53 +6998,53 @@ snapshots: stable-hash-x: 0.1.1 unrs-resolver: 1.7.11 optionalDependencies: - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-n@17.19.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): + eslint-plugin-n@17.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) - '@typescript-eslint/utils': 8.31.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) + '@typescript-eslint/utils': 8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) enhanced-resolve: 5.18.1 - eslint: 9.28.0(jiti@2.4.2) - eslint-plugin-es-x: 7.8.0(eslint@9.28.0(jiti@2.4.2)) - get-tsconfig: 4.10.0 + eslint: 9.30.1(jiti@2.4.2) + eslint-plugin-es-x: 7.8.0(eslint@9.30.1(jiti@2.4.2)) + get-tsconfig: 4.10.1 globals: 15.15.0 ignore: 5.3.2 minimatch: 9.0.5 - semver: 7.7.1 + semver: 7.7.2 ts-declaration-location: 1.0.7(typescript@5.8.3) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-react-compiler@19.1.0-rc.2(eslint@9.28.0(jiti@2.4.2)): + eslint-plugin-react-compiler@19.1.0-rc.2(eslint@9.30.1(jiti@2.4.2)): dependencies: '@babel/core': 7.26.10 '@babel/parser': 7.27.0 '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.10) - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) hermes-parser: 0.25.1 zod: 3.24.2 zod-validation-error: 3.4.0(zod@3.24.2) transitivePeerDependencies: - supports-color - eslint-plugin-react-debug@1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): - dependencies: - '@eslint-react/ast': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.51.2 - '@eslint-react/kit': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + eslint-plugin-react-debug@1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@eslint-react/ast': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@eslint-react/kit': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) string-ts: 2.2.1 ts-pattern: 5.7.1 optionalDependencies: @@ -6700,19 +7052,19 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-dom@1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): - dependencies: - '@eslint-react/ast': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.51.2 - '@eslint-react/kit': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-dom@1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@eslint-react/ast': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@eslint-react/kit': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) compare-versions: 6.1.1 - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) string-ts: 2.2.1 ts-pattern: 5.7.1 optionalDependencies: @@ -6720,19 +7072,19 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks-extra@1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): - dependencies: - '@eslint-react/ast': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.51.2 - '@eslint-react/kit': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + eslint-plugin-react-hooks-extra@1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@eslint-react/ast': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@eslint-react/kit': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) string-ts: 2.2.1 ts-pattern: 5.7.1 optionalDependencies: @@ -6740,23 +7092,23 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@5.2.0(eslint@9.28.0(jiti@2.4.2)): + eslint-plugin-react-hooks@5.2.0(eslint@9.30.1(jiti@2.4.2)): dependencies: - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) - eslint-plugin-react-naming-convention@1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): + eslint-plugin-react-naming-convention@1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.51.2 - '@eslint-react/kit': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + '@eslint-react/ast': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@eslint-react/kit': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) string-ts: 2.2.1 ts-pattern: 5.7.1 optionalDependencies: @@ -6764,18 +7116,18 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-web-api@1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): - dependencies: - '@eslint-react/ast': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.51.2 - '@eslint-react/kit': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + eslint-plugin-react-web-api@1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@eslint-react/ast': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@eslint-react/kit': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) string-ts: 2.2.1 ts-pattern: 5.7.1 optionalDependencies: @@ -6783,21 +7135,21 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-x@1.51.2(eslint@9.28.0(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3): - dependencies: - '@eslint-react/ast': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.51.2 - '@eslint-react/kit': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.51.2(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-x@1.52.2(eslint@9.30.1(jiti@2.4.2))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3): + dependencies: + '@eslint-react/ast': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.52.2 + '@eslint-react/kit': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.52.2(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) compare-versions: 6.1.1 - eslint: 9.28.0(jiti@2.4.2) - is-immutable-type: 5.0.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) + is-immutable-type: 5.0.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) string-ts: 2.2.1 ts-pattern: 5.7.1 optionalDependencies: @@ -6806,30 +7158,37 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)): + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)): dependencies: - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) eslint-scope@8.3.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@4.2.0: {} - eslint@9.28.0(jiti@2.4.2): + eslint-visitor-keys@4.2.1: {} + + eslint@9.30.1(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.28.0 + '@eslint/js': 9.30.1 '@eslint/plugin-kit': 0.3.1 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 @@ -6839,11 +7198,11 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.1 escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -6871,6 +7230,12 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} esquery@1.6.0: @@ -6931,7 +7296,7 @@ snapshots: optionalDependencies: picomatch: 4.0.2 - fdir@6.4.4(picomatch@4.0.2): + fdir@6.4.6(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -7024,10 +7389,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-tsconfig@4.10.0: - dependencies: - resolve-pkg-maps: 1.0.0 - get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -7159,11 +7520,11 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-immutable-type@5.0.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): + is-immutable-type@5.0.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) - ts-api-utils: 2.0.1(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) ts-declaration-location: 1.0.7(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -7179,7 +7540,7 @@ snapshots: is-reference@3.0.3: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 is-subdir@1.2.0: dependencies: @@ -7216,6 +7577,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -7282,10 +7645,10 @@ snapshots: dependencies: json-buffer: 3.0.1 - knip@5.60.2(@types/node@22.15.30)(typescript@5.8.3): + knip@5.61.3(@types/node@24.0.13)(typescript@5.8.3): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 22.15.30 + '@types/node': 24.0.13 fast-glob: 3.3.3 formatly: 0.2.4 jiti: 2.4.2 @@ -7297,8 +7660,8 @@ snapshots: smol-toml: 1.3.4 strip-json-comments: 5.0.2 typescript: 5.8.3 - zod: 3.24.2 - zod-validation-error: 3.4.0(zod@3.24.2) + zod: 3.25.56 + zod-validation-error: 3.4.0(zod@3.25.56) kolorist@1.8.0: {} @@ -7343,6 +7706,8 @@ snapshots: loupe@3.1.3: {} + loupe@3.1.4: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -7433,7 +7798,7 @@ snapshots: muggle-string@0.4.1: {} - nanoid@3.3.9: {} + nanoid@3.3.11: {} nanoid@5.1.3: {} @@ -7463,7 +7828,7 @@ snapshots: nwsapi@2.2.18: {} - nx@21.1.3: + nx@21.2.2: dependencies: '@napi-rs/wasm-runtime': 0.2.4 '@yarnpkg/lockfile': 1.1.0 @@ -7490,7 +7855,7 @@ snapshots: open: 8.4.2 ora: 5.3.0 resolve.exports: 2.0.3 - semver: 7.7.1 + semver: 7.7.2 string-width: 4.2.3 tar-stream: 2.2.0 tmp: 0.2.3 @@ -7501,16 +7866,16 @@ snapshots: yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@nx/nx-darwin-arm64': 21.1.3 - '@nx/nx-darwin-x64': 21.1.3 - '@nx/nx-freebsd-x64': 21.1.3 - '@nx/nx-linux-arm-gnueabihf': 21.1.3 - '@nx/nx-linux-arm64-gnu': 21.1.3 - '@nx/nx-linux-arm64-musl': 21.1.3 - '@nx/nx-linux-x64-gnu': 21.1.3 - '@nx/nx-linux-x64-musl': 21.1.3 - '@nx/nx-win32-arm64-msvc': 21.1.3 - '@nx/nx-win32-x64-msvc': 21.1.3 + '@nx/nx-darwin-arm64': 21.2.2 + '@nx/nx-darwin-x64': 21.2.2 + '@nx/nx-freebsd-x64': 21.2.2 + '@nx/nx-linux-arm-gnueabihf': 21.2.2 + '@nx/nx-linux-arm64-gnu': 21.2.2 + '@nx/nx-linux-arm64-musl': 21.2.2 + '@nx/nx-linux-x64-gnu': 21.2.2 + '@nx/nx-linux-x64-musl': 21.2.2 + '@nx/nx-win32-arm64-msvc': 21.2.2 + '@nx/nx-win32-x64-msvc': 21.2.2 transitivePeerDependencies: - debug @@ -7643,9 +8008,9 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 - postcss@8.5.3: + postcss@8.5.6: dependencies: - nanoid: 3.3.9 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -7653,14 +8018,14 @@ snapshots: premove@4.0.0: {} - prettier-plugin-svelte@3.4.0(prettier@3.5.3)(svelte@5.22.6): + prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.22.6): dependencies: - prettier: 3.5.3 + prettier: 3.6.2 svelte: 5.22.6 prettier@2.8.8: {} - prettier@3.5.3: {} + prettier@3.6.2: {} pretty-format@29.7.0: dependencies: @@ -7685,11 +8050,6 @@ snapshots: queue-microtask@1.2.3: {} - react-dom@19.0.0(react@19.1.0): - dependencies: - react: 19.1.0 - scheduler: 0.25.0 - react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 @@ -7748,35 +8108,36 @@ snapshots: reusify@1.1.0: {} - rollup-plugin-preserve-directives@0.4.0(rollup@4.35.0): + rollup-plugin-preserve-directives@0.4.0(rollup@4.44.1): dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.35.0) + '@rollup/pluginutils': 5.1.4(rollup@4.44.1) magic-string: 0.30.17 - rollup: 4.35.0 + rollup: 4.44.1 - rollup@4.35.0: + rollup@4.44.1: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.35.0 - '@rollup/rollup-android-arm64': 4.35.0 - '@rollup/rollup-darwin-arm64': 4.35.0 - '@rollup/rollup-darwin-x64': 4.35.0 - '@rollup/rollup-freebsd-arm64': 4.35.0 - '@rollup/rollup-freebsd-x64': 4.35.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.35.0 - '@rollup/rollup-linux-arm-musleabihf': 4.35.0 - '@rollup/rollup-linux-arm64-gnu': 4.35.0 - '@rollup/rollup-linux-arm64-musl': 4.35.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.35.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.35.0 - '@rollup/rollup-linux-riscv64-gnu': 4.35.0 - '@rollup/rollup-linux-s390x-gnu': 4.35.0 - '@rollup/rollup-linux-x64-gnu': 4.35.0 - '@rollup/rollup-linux-x64-musl': 4.35.0 - '@rollup/rollup-win32-arm64-msvc': 4.35.0 - '@rollup/rollup-win32-ia32-msvc': 4.35.0 - '@rollup/rollup-win32-x64-msvc': 4.35.0 + '@rollup/rollup-android-arm-eabi': 4.44.1 + '@rollup/rollup-android-arm64': 4.44.1 + '@rollup/rollup-darwin-arm64': 4.44.1 + '@rollup/rollup-darwin-x64': 4.44.1 + '@rollup/rollup-freebsd-arm64': 4.44.1 + '@rollup/rollup-freebsd-x64': 4.44.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.1 + '@rollup/rollup-linux-arm-musleabihf': 4.44.1 + '@rollup/rollup-linux-arm64-gnu': 4.44.1 + '@rollup/rollup-linux-arm64-musl': 4.44.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.1 + '@rollup/rollup-linux-riscv64-gnu': 4.44.1 + '@rollup/rollup-linux-riscv64-musl': 4.44.1 + '@rollup/rollup-linux-s390x-gnu': 4.44.1 + '@rollup/rollup-linux-x64-gnu': 4.44.1 + '@rollup/rollup-linux-x64-musl': 4.44.1 + '@rollup/rollup-win32-arm64-msvc': 4.44.1 + '@rollup/rollup-win32-ia32-msvc': 4.44.1 + '@rollup/rollup-win32-x64-msvc': 4.44.1 fsevents: 2.3.3 rrweb-cssom@0.8.0: {} @@ -7797,8 +8158,6 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.25.0: {} - scheduler@0.26.0: {} semver@6.3.1: {} @@ -7807,8 +8166,6 @@ snapshots: dependencies: lru-cache: 6.0.0 - semver@7.7.1: {} - semver@7.7.2: {} seroval-plugins@1.3.2(seroval@1.3.2): @@ -7823,32 +8180,32 @@ snapshots: shebang-regex@3.0.0: {} - sherif-darwin-arm64@1.5.0: + sherif-darwin-arm64@1.6.1: optional: true - sherif-darwin-x64@1.5.0: + sherif-darwin-x64@1.6.1: optional: true - sherif-linux-arm64@1.5.0: + sherif-linux-arm64@1.6.1: optional: true - sherif-linux-x64@1.5.0: + sherif-linux-x64@1.6.1: optional: true - sherif-windows-arm64@1.5.0: + sherif-windows-arm64@1.6.1: optional: true - sherif-windows-x64@1.5.0: + sherif-windows-x64@1.6.1: optional: true - sherif@1.5.0: + sherif@1.6.1: optionalDependencies: - sherif-darwin-arm64: 1.5.0 - sherif-darwin-x64: 1.5.0 - sherif-linux-arm64: 1.5.0 - sherif-linux-x64: 1.5.0 - sherif-windows-arm64: 1.5.0 - sherif-windows-x64: 1.5.0 + sherif-darwin-arm64: 1.6.1 + sherif-darwin-x64: 1.6.1 + sherif-linux-arm64: 1.6.1 + sherif-linux-x64: 1.6.1 + sherif-windows-arm64: 1.6.1 + sherif-windows-x64: 1.6.1 siginfo@2.0.0: {} @@ -7856,11 +8213,11 @@ snapshots: signal-exit@4.1.0: {} - simple-git@3.27.0: + simple-git@3.28.0: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -7940,6 +8297,10 @@ snapshots: strip-json-comments@5.0.2: {} + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -7954,9 +8315,9 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@jridgewell/sourcemap-codec': 1.5.0 - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.1) - '@types/estree': 1.0.6 - acorn: 8.14.1 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 @@ -7994,17 +8355,12 @@ snapshots: fdir: 6.4.3(picomatch@4.0.2) picomatch: 4.0.2 - tinyglobby@0.2.13: - dependencies: - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 - tinyglobby@0.2.14: dependencies: - fdir: 6.4.4(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - tinypool@1.1.0: {} + tinypool@1.1.1: {} tinyrainbow@2.0.0: {} @@ -8038,10 +8394,6 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@2.0.1(typescript@5.8.3): - dependencies: - typescript: 5.8.3 - ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -8068,7 +8420,7 @@ snapshots: tsx@4.19.3: dependencies: esbuild: 0.25.0 - get-tsconfig: 4.10.0 + get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 optional: true @@ -8095,12 +8447,12 @@ snapshots: typescript: 5.8.3 yaml: 2.7.0 - typescript-eslint@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.33.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -8113,7 +8465,7 @@ snapshots: ufo@1.5.4: {} - undici-types@6.21.0: {} + undici-types@7.8.0: {} undici@6.21.3: {} @@ -8153,17 +8505,21 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + util-deprecate@1.0.2: {} validate-html-nesting@1.2.2: {} - vite-node@3.2.2(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): + vite-node@3.2.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - jiti @@ -8178,84 +8534,84 @@ snapshots: - tsx - yaml - vite-plugin-dts@4.2.3(@types/node@22.15.30)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): + vite-plugin-dts@4.2.3(@types/node@24.0.13)(rollup@4.44.1)(typescript@5.8.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): dependencies: - '@microsoft/api-extractor': 7.47.7(@types/node@22.15.30) - '@rollup/pluginutils': 5.1.4(rollup@4.35.0) + '@microsoft/api-extractor': 7.47.7(@types/node@24.0.13) + '@rollup/pluginutils': 5.1.4(rollup@4.44.1) '@volar/typescript': 2.4.12 '@vue/language-core': 2.1.6(typescript@5.8.3) compare-versions: 6.1.1 - debug: 4.4.0 + debug: 4.4.1 kolorist: 1.8.0 local-pkg: 0.5.1 magic-string: 0.30.17 typescript: 5.8.3 optionalDependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-externalize-deps@0.9.0(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): + vite-plugin-externalize-deps@0.9.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): dependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): + vite-plugin-solid@2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.7)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@types/babel__core': 7.20.5 - babel-preset-solid: 1.9.5(@babel/core@7.26.9) + babel-preset-solid: 1.9.5(@babel/core@7.26.10) merge-anything: 5.1.7 solid-js: 1.9.7 solid-refresh: 0.6.3(solid-js@1.9.7) - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - vitefu: 1.0.6(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vitefu: 1.0.6(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) optionalDependencies: '@testing-library/jest-dom': 6.6.3 transitivePeerDependencies: - supports-color - vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): dependencies: - debug: 4.4.0 + debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.8.3) optionalDependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color - typescript - vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): + vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): dependencies: esbuild: 0.25.0 - fdir: 6.4.4(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - postcss: 8.5.3 - rollup: 4.35.0 - tinyglobby: 0.2.13 + postcss: 8.5.6 + rollup: 4.44.1 + tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.13 fsevents: 2.3.3 jiti: 2.4.2 tsx: 4.19.3 yaml: 2.7.0 - vitefu@1.0.6(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): + vitefu@1.0.6(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): optionalDependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - vitest@3.2.2(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(tsx@4.19.3)(yaml@2.7.0): + vitest@3.2.4(@types/node@24.0.13)(jiti@2.4.2)(jsdom@26.1.0)(tsx@4.19.3)(yaml@2.7.0): dependencies: '@types/chai': 5.2.2 - '@vitest/expect': 3.2.2 - '@vitest/mocker': 3.2.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) - '@vitest/pretty-format': 3.2.2 - '@vitest/runner': 3.2.2 - '@vitest/snapshot': 3.2.2 - '@vitest/spy': 3.2.2 - '@vitest/utils': 3.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 chai: 5.2.0 debug: 4.4.1 expect-type: 1.2.1 @@ -8266,13 +8622,13 @@ snapshots: tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.14 - tinypool: 1.1.0 + tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - vite-node: 3.2.2(@types/node@22.15.30)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite-node: 3.2.4(@types/node@24.0.13)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.13 jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -8290,16 +8646,16 @@ snapshots: vscode-uri@3.1.0: {} - vue-eslint-parser@10.1.3(eslint@9.28.0(jiti@2.4.2)): + vue-eslint-parser@10.1.3(eslint@9.30.1(jiti@2.4.2)): dependencies: - debug: 4.4.0 - eslint: 9.28.0(jiti@2.4.2) + debug: 4.4.1 + eslint: 9.30.1(jiti@2.4.2) eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 espree: 10.3.0 esquery: 1.6.0 lodash: 4.17.21 - semver: 7.7.1 + semver: 7.7.2 transitivePeerDependencies: - supports-color @@ -8386,6 +8742,12 @@ snapshots: dependencies: zod: 3.24.2 + zod-validation-error@3.4.0(zod@3.25.56): + dependencies: + zod: 3.25.56 + zod@3.24.2: {} zod@3.25.56: {} + + zod@3.25.72: {} diff --git a/vitest.workspace.js b/vitest.workspace.js index ea394c9e2..fcb88e206 100644 --- a/vitest.workspace.js +++ b/vitest.workspace.js @@ -1,7 +1,13 @@ -import { defineWorkspace } from 'vitest/config' +import { defineConfig } from 'vitest/config' -export default defineWorkspace([ - './packages/pacer/vite.config.ts', - './packages/react-pacer/vite.config.ts', - './packages/solid-pacer/vite.config.ts', -]) +export default defineConfig({ + test: { + projects: [ + './packages/pacer/vite.config.ts', + './packages/persister/vite.config.ts', + './packages/react-pacer/vite.config.ts', + './packages/react-persister/vite.config.ts', + './packages/solid-pacer/vite.config.ts', + ], + }, +})