diff --git a/.changeset/tricky-pants-hear-react.md b/.changeset/tricky-pants-hear-react.md new file mode 100644 index 000000000..33e9654d4 --- /dev/null +++ b/.changeset/tricky-pants-hear-react.md @@ -0,0 +1,7 @@ +--- +'@tanstack/react-pacer': minor +--- + +- breaking: renamed `useQueuerState` hook to `useQueuedState` +- breaking: changed return signature of `useQueuedState` to include the `addItem` function +- feat: add `useQueuedValue` hook diff --git a/.changeset/tricky-pants-hear-solid.md b/.changeset/tricky-pants-hear-solid.md new file mode 100644 index 000000000..172cc4b82 --- /dev/null +++ b/.changeset/tricky-pants-hear-solid.md @@ -0,0 +1,7 @@ +--- +'@tanstack/solid-pacer': minor +--- + +- breaking: renamed `createQueuerState` hook to `createQueuedState` +- breaking: changed return signature of `createQueuedState` to include the `addItem` function +- feat: add `createQueuedValue` hook diff --git a/.changeset/tricky-pants-hear.md b/.changeset/tricky-pants-hear.md new file mode 100644 index 000000000..87bd52616 --- /dev/null +++ b/.changeset/tricky-pants-hear.md @@ -0,0 +1,15 @@ +--- +'@tanstack/pacer': minor +--- + +- feat: add queuer expiration feature to `AsyncQueuer` and `Queuer` +- feat: add return values and types to `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` +- feat: standardize `onSuccess`, `onSettled`, and `onError` in `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` +- feat: replace `getExecutionCount` with `getSuccessCount`, `getErrorCount`, and `getSettleCount` in `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` +- feat: add `getIsPending`, `getIsExecuting`, and `getLastResult` to `AsyncThrottler` +- feat: add `leading` and `trailing` options to `AsyncThrottler` +- breaking: rename `onExecute` to `onSettled` in `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` +- breaking: Set `started` to `true` by default in `AsyncQueuer` and `Queuer` +- breaking: Simplified generics to just use `TFn` instead of `TFn, TArgs` in debouncers, throttlers, and rate limiters +- fix: fixed leading and trailing edge behavior of `Debouncer` and `AsyncDebouncer` +- fix: fixed `getIsPending` to return correct value in `AsyncDebouncer` diff --git a/README.md b/README.md index 6588a18f4..922b903f6 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Take control of your application's timing with TanStack Pacer's rate limiting, t - Perform deep equality checks between values - Create custom comparison logic for specific needs - **Convenient Hooks** - - Reduce boilerplate code with pre-built hooks like `useDebouncedCallback`, `useThrottledValue`, and `useQueuerState`, and more. + - Reduce boilerplate code with pre-built hooks like `useDebouncedCallback`, `useThrottledValue`, and `useQueuedState`, and more. - **Type Safety** - Full type safety with TypeScript that makes sure that your functions will always be called with the correct arguments - Generics for flexible and reusable utilities diff --git a/docs/config.json b/docs/config.json index 2f0d2295e..170505dcd 100644 --- a/docs/config.json +++ b/docs/config.json @@ -365,7 +365,7 @@ "to": "framework/react/reference/functions/usequeuer" }, { - "label": "useQueuerState", + "label": "useQueuedState", "to": "framework/react/reference/functions/usequeuerstate" }, { @@ -613,16 +613,20 @@ "to": "framework/react/examples/useQueuer" }, { - "label": "useQueuerState", - "to": "framework/react/examples/useQueuerState" + "label": "useQueuedState", + "to": "framework/react/examples/useQueuedState" + }, + { + "label": "useQueuedValue", + "to": "framework/react/examples/useQueuedValue" }, { "label": "useAsyncQueuer", "to": "framework/react/examples/useAsyncQueuer" }, { - "label": "useAsyncQueuerState", - "to": "framework/react/examples/useAsyncQueuerState" + "label": "useAsyncQueuedState", + "to": "framework/react/examples/useAsyncQueuedState" } ] }, @@ -644,6 +648,29 @@ ] } ] + }, + { + "label": "Advanced Examples", + "children": [], + "frameworks": [ + { + "label": "react", + "children": [ + { + "label": "React Query Debounced Prefetch", + "to": "framework/react/examples/react-query-debounced-prefetch" + }, + { + "label": "React Query Throttled Prefetch", + "to": "framework/react/examples/react-query-throttled-prefetch" + }, + { + "label": "React Query Queued Prefetch", + "to": "framework/react/examples/react-query-queued-prefetch" + } + ] + } + ] } ] } diff --git a/docs/framework/react/reference/functions/useasyncdebouncer.md b/docs/framework/react/reference/functions/useasyncdebouncer.md index 9ea08f281..0eae19e83 100644 --- a/docs/framework/react/reference/functions/useasyncdebouncer.md +++ b/docs/framework/react/reference/functions/useasyncdebouncer.md @@ -8,10 +8,10 @@ title: useAsyncDebouncer # Function: useAsyncDebouncer() ```ts -function useAsyncDebouncer(fn, options): AsyncDebouncer +function useAsyncDebouncer(fn, options): AsyncDebouncer ``` -Defined in: [async-debouncer/useAsyncDebouncer.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts#L42) +Defined in: [react-pacer/src/async-debouncer/useAsyncDebouncer.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts#L42) A low-level React hook that creates an `AsyncDebouncer` instance to delay execution of an async function. @@ -26,8 +26,6 @@ wait for user input to settle before making expensive async calls. • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -36,11 +34,11 @@ wait for user input to settle before making expensive async calls. ### options -`AsyncDebouncerOptions`\<`TFn`, `TArgs`\> +`AsyncDebouncerOptions`\<`TFn`\> ## Returns -`AsyncDebouncer`\<`TFn`, `TArgs`\> +`AsyncDebouncer`\<`TFn`\> ## Example diff --git a/docs/framework/react/reference/functions/useasyncqueuerstate.md b/docs/framework/react/reference/functions/useasyncqueuedstate.md similarity index 78% rename from docs/framework/react/reference/functions/useasyncqueuerstate.md rename to docs/framework/react/reference/functions/useasyncqueuedstate.md index f346cca58..3d7944daf 100644 --- a/docs/framework/react/reference/functions/useasyncqueuerstate.md +++ b/docs/framework/react/reference/functions/useasyncqueuedstate.md @@ -1,17 +1,17 @@ --- -id: useAsyncQueuerState -title: useAsyncQueuerState +id: useAsyncQueuedState +title: useAsyncQueuedState --- -# Function: useAsyncQueuerState() +# Function: useAsyncQueuedState() ```ts -function useAsyncQueuerState(options): [() => Promise[], AsyncQueuer] +function useAsyncQueuedState(options): [() => Promise[], AsyncQueuer] ``` -Defined in: [async-queuer/useAsyncQueuerState.ts:53](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-queuer/useAsyncQueuerState.ts#L53) +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) A higher-level React hook that creates an `AsyncQueuer` instance with built-in state management. @@ -50,7 +50,7 @@ The state will automatically update whenever items are: ```tsx // Create a queue with state management -const [queueItems, asyncQueuer] = useAsyncQueuerState({ +const [queueItems, asyncQueuer] = useAsyncQueuedState({ concurrency: 2, maxSize: 100, started: true diff --git a/docs/framework/react/reference/functions/useasyncqueuer.md b/docs/framework/react/reference/functions/useasyncqueuer.md index 1eb1e96b2..cf25200c5 100644 --- a/docs/framework/react/reference/functions/useasyncqueuer.md +++ b/docs/framework/react/reference/functions/useasyncqueuer.md @@ -11,7 +11,7 @@ title: useAsyncQueuer function useAsyncQueuer(options): AsyncQueuer ``` -Defined in: [async-queuer/useAsyncQueuer.ts:55](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-queuer/useAsyncQueuer.ts#L55) +Defined in: [react-pacer/src/async-queuer/useAsyncQueuer.ts:55](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-queuer/useAsyncQueuer.ts#L55) A lower-level React hook that creates an `AsyncQueuer` instance for managing an async queue of items. diff --git a/docs/framework/react/reference/functions/useasyncratelimiter.md b/docs/framework/react/reference/functions/useasyncratelimiter.md index 31a89e577..7be183836 100644 --- a/docs/framework/react/reference/functions/useasyncratelimiter.md +++ b/docs/framework/react/reference/functions/useasyncratelimiter.md @@ -8,10 +8,10 @@ title: useAsyncRateLimiter # Function: useAsyncRateLimiter() ```ts -function useAsyncRateLimiter(fn, options): AsyncRateLimiter +function useAsyncRateLimiter(fn, options): AsyncRateLimiter ``` -Defined in: [async-rate-limiter/useAsyncRateLimiter.ts:43](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts#L43) +Defined in: [react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts:43](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts#L43) A low-level React hook that creates an `AsyncRateLimiter` instance to limit how many times an async function can execute within a time window. @@ -26,8 +26,6 @@ managing resource constraints, or controlling bursts of async operations. • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -36,11 +34,11 @@ managing resource constraints, or controlling bursts of async operations. ### options -`AsyncRateLimiterOptions`\<`TFn`, `TArgs`\> +`AsyncRateLimiterOptions`\<`TFn`\> ## Returns -`AsyncRateLimiter`\<`TFn`, `TArgs`\> +`AsyncRateLimiter`\<`TFn`\> ## Example diff --git a/docs/framework/react/reference/functions/useasyncthrottler.md b/docs/framework/react/reference/functions/useasyncthrottler.md index 73d4c9ca2..6ca0998b7 100644 --- a/docs/framework/react/reference/functions/useasyncthrottler.md +++ b/docs/framework/react/reference/functions/useasyncthrottler.md @@ -8,10 +8,10 @@ title: useAsyncThrottler # Function: useAsyncThrottler() ```ts -function useAsyncThrottler(fn, options): AsyncThrottler +function useAsyncThrottler(fn, options): AsyncThrottler ``` -Defined in: [async-throttler/useAsyncThrottler.ts:44](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts#L44) +Defined in: [react-pacer/src/async-throttler/useAsyncThrottler.ts:44](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts#L44) A low-level React hook that creates an `AsyncThrottler` instance to limit how often an async function can execute. @@ -26,8 +26,6 @@ database operations, or other async tasks. • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -36,11 +34,11 @@ database operations, or other async tasks. ### options -`AsyncThrottlerOptions`\<`TFn`, `TArgs`\> +`AsyncThrottlerOptions`\<`TFn`\> ## Returns -`AsyncThrottler`\<`TFn`, `TArgs`\> +`AsyncThrottler`\<`TFn`\> ## Example diff --git a/docs/framework/react/reference/functions/usedebouncedcallback.md b/docs/framework/react/reference/functions/usedebouncedcallback.md index d8c3ddbcf..d9da81160 100644 --- a/docs/framework/react/reference/functions/usedebouncedcallback.md +++ b/docs/framework/react/reference/functions/usedebouncedcallback.md @@ -8,10 +8,10 @@ title: useDebouncedCallback # Function: useDebouncedCallback() ```ts -function useDebouncedCallback(fn, options): (...args) => void +function useDebouncedCallback(fn, options): (...args) => void ``` -Defined in: [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:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedCallback.ts#L42) A React hook that creates a debounced version of a callback function. This hook is essentially a wrapper around the basic `debounce` function @@ -36,8 +36,6 @@ Consider using the `useDebouncer` hook instead. • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -46,7 +44,7 @@ Consider using the `useDebouncer` hook instead. ### options -`DebouncerOptions`\<`TFn`, `TArgs`\> +`DebouncerOptions`\<`TFn`\> ## Returns @@ -56,7 +54,7 @@ Consider using the `useDebouncer` hook instead. #### args -...`TArgs` +...`Parameters`\<`TFn`\> ### Returns diff --git a/docs/framework/react/reference/functions/usedebouncedstate.md b/docs/framework/react/reference/functions/usedebouncedstate.md index ecd283154..c2be1d8d6 100644 --- a/docs/framework/react/reference/functions/usedebouncedstate.md +++ b/docs/framework/react/reference/functions/usedebouncedstate.md @@ -8,10 +8,10 @@ title: useDebouncedState # Function: useDebouncedState() ```ts -function useDebouncedState(value, options): [TValue, Dispatch>, Debouncer>, [TValue]>] +function useDebouncedState(value, options): [TValue, Dispatch>, Debouncer>>] ``` -Defined in: [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:38](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedState.ts#L38) 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. @@ -38,11 +38,11 @@ The hook returns a tuple containing: ### options -`DebouncerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`SetStateAction`\<`TValue`\>\]\> +`DebouncerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> ## Returns -\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, `Debouncer`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`TValue`\]\>\] +\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, `Debouncer`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] ## Example diff --git a/docs/framework/react/reference/functions/usedebouncedvalue.md b/docs/framework/react/reference/functions/usedebouncedvalue.md index e4f9c9099..6c7871ba2 100644 --- a/docs/framework/react/reference/functions/usedebouncedvalue.md +++ b/docs/framework/react/reference/functions/usedebouncedvalue.md @@ -8,10 +8,10 @@ title: useDebouncedValue # Function: useDebouncedValue() ```ts -function useDebouncedValue(value, options): [TValue, Debouncer>, [TValue]>] +function useDebouncedValue(value, options): [TValue, Debouncer>>] ``` -Defined in: [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:41](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedValue.ts#L41) 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 @@ -25,9 +25,9 @@ This is useful for deriving debounced values from props or state that change fre like search queries or form inputs, where you want to limit how often downstream effects or calculations occur. -The hook returns a tuple containing: -- The current debounced value -- The debouncer instance with control methods +The hook returns the current debounced value and the underlying debouncer instance. +The debouncer instance can be used to access additional functionality like cancellation +and execution counts. ## Type Parameters @@ -41,11 +41,11 @@ The hook returns a tuple containing: ### options -`DebouncerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`SetStateAction`\<`TValue`\>\]\> +`DebouncerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> ## Returns -\[`TValue`, `Debouncer`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`TValue`\]\>\] +\[`TValue`, `Debouncer`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] ## Example diff --git a/docs/framework/react/reference/functions/usedebouncer.md b/docs/framework/react/reference/functions/usedebouncer.md index 94faa3d90..22c162551 100644 --- a/docs/framework/react/reference/functions/usedebouncer.md +++ b/docs/framework/react/reference/functions/usedebouncer.md @@ -8,10 +8,10 @@ title: useDebouncer # Function: useDebouncer() ```ts -function useDebouncer(fn, options): Debouncer +function useDebouncer(fn, options): Debouncer ``` -Defined in: [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:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncer.ts#L42) A React hook that creates and manages a Debouncer instance. @@ -31,8 +31,6 @@ timer resets and starts waiting again. • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -41,11 +39,11 @@ timer resets and starts waiting again. ### options -`DebouncerOptions`\<`TFn`, `TArgs`\> +`DebouncerOptions`\<`TFn`\> ## Returns -`Debouncer`\<`TFn`, `TArgs`\> +`Debouncer`\<`TFn`\> ## Example diff --git a/docs/framework/react/reference/functions/usequeuerstate.md b/docs/framework/react/reference/functions/usequeuedstate.md similarity index 76% rename from docs/framework/react/reference/functions/usequeuerstate.md rename to docs/framework/react/reference/functions/usequeuedstate.md index 51fbde728..f220a392e 100644 --- a/docs/framework/react/reference/functions/usequeuerstate.md +++ b/docs/framework/react/reference/functions/usequeuedstate.md @@ -1,17 +1,17 @@ --- -id: useQueuerState -title: useQueuerState +id: useQueuedState +title: useQueuedState --- -# Function: useQueuerState() +# Function: useQueuedState() ```ts -function useQueuerState(options): [TValue[], Queuer] +function useQueuedState(options): [TValue[], (item, position?, runOnUpdate?) => boolean, Queuer] ``` -Defined in: [queuer/useQueuerState.ts:54](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuerState.ts#L54) +Defined in: [react-pacer/src/queuer/useQueuedState.ts:54](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuedState.ts#L54) A React hook that creates a queuer with managed state, combining React's useState with queuing functionality. This hook provides both the current queue state and queue control methods. @@ -39,13 +39,13 @@ The hook returns a tuple containing: ## Returns -\[`TValue`[], `Queuer`\<`TValue`\>\] +\[`TValue`[], (`item`, `position`?, `runOnUpdate`?) => `boolean`, `Queuer`\<`TValue`\>\] ## Example ```tsx // Basic queue with initial items and priority -const [items, queue] = useQueuerState({ +const [items, queue] = useQueuedState({ initialItems: ['item1', 'item2'], started: true, wait: 1000, diff --git a/docs/framework/react/reference/functions/usequeuedvalue.md b/docs/framework/react/reference/functions/usequeuedvalue.md new file mode 100644 index 000000000..b2b84c35c --- /dev/null +++ b/docs/framework/react/reference/functions/usequeuedvalue.md @@ -0,0 +1,67 @@ +--- +id: useQueuedValue +title: useQueuedValue +--- + + + +# Function: useQueuedValue() + +```ts +function useQueuedValue(initialValue, options): [TValue, Queuer] +``` + +Defined in: [react-pacer/src/queuer/useQueuedValue.ts:40](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuedValue.ts#L40) + +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. + +The queued value will process changes in the order they are received, with optional delays between +processing each change. This is useful for handling state updates that need to be processed +in a specific order, like animations or sequential UI updates. + +The hook returns a tuple containing: +- The current queued value +- The queuer instance with control methods + +## Type Parameters + +• **TValue** + +## Parameters + +### initialValue + +`TValue` + +### options + +`QueuerOptions`\<`TValue`\> = `{}` + +## Returns + +\[`TValue`, `Queuer`\<`TValue`\>\] + +## Example + +```tsx +// Queue state changes with a delay between each +const [value, queuer] = useQueuedValue(initialValue, { + wait: 500, // Wait 500ms between processing each change + started: true // Start processing immediately +}); + +// Add changes to the queue +const handleChange = (newValue) => { + queuer.addItem(newValue); +}; + +// Control the queue +const pauseProcessing = () => { + queuer.stop(); +}; + +const resumeProcessing = () => { + queuer.start(); +}; +``` diff --git a/docs/framework/react/reference/functions/usequeuer.md b/docs/framework/react/reference/functions/usequeuer.md index 67e462780..e42b84583 100644 --- a/docs/framework/react/reference/functions/usequeuer.md +++ b/docs/framework/react/reference/functions/usequeuer.md @@ -11,7 +11,7 @@ title: useQueuer function useQueuer(options): Queuer ``` -Defined in: [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:44](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/queuer/useQueuer.ts#L44) A React hook that creates and manages a Queuer instance. @@ -19,7 +19,7 @@ This is a lower-level hook that provides direct access to the Queuer's functiona any built-in state management. This allows you to integrate it with any state management solution you prefer (useState, Redux, Zustand, etc.) by utilizing the onItemsChange callback. -For a hook with built-in state management, see useQueuerState. +For a hook with built-in state management, see useQueuedState. The Queuer extends the base Queue to add processing capabilities. Items are processed synchronously in order, with optional delays between processing each item. The queuer includes diff --git a/docs/framework/react/reference/functions/useratelimitedcallback.md b/docs/framework/react/reference/functions/useratelimitedcallback.md index 9768a78b0..32f179b97 100644 --- a/docs/framework/react/reference/functions/useratelimitedcallback.md +++ b/docs/framework/react/reference/functions/useratelimitedcallback.md @@ -11,7 +11,7 @@ title: useRateLimitedCallback function useRateLimitedCallback(fn, options): (...args) => boolean ``` -Defined in: [rate-limiter/useRateLimitedCallback.ts:52](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts#L52) +Defined in: [react-pacer/src/rate-limiter/useRateLimitedCallback.ts:52](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts#L52) A React hook that creates a rate-limited version of a callback function. This hook is essentially a wrapper around the basic `rateLimiter` function @@ -55,7 +55,7 @@ Consider using the `useRateLimiter` hook instead. ### options -`RateLimiterOptions`\<`TFn`, `TArgs`\> +`RateLimiterOptions`\<`TFn`\> ## Returns diff --git a/docs/framework/react/reference/functions/useratelimitedstate.md b/docs/framework/react/reference/functions/useratelimitedstate.md index 255e17695..4686d112a 100644 --- a/docs/framework/react/reference/functions/useratelimitedstate.md +++ b/docs/framework/react/reference/functions/useratelimitedstate.md @@ -8,10 +8,10 @@ title: useRateLimitedState # Function: useRateLimitedState() ```ts -function useRateLimitedState(value, options): [TValue, Dispatch>, RateLimiter>, [TValue]>] +function useRateLimitedState(value, options): [TValue, Dispatch>, RateLimiter>>] ``` -Defined in: [rate-limiter/useRateLimitedState.ts:59](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts#L59) +Defined in: [react-pacer/src/rate-limiter/useRateLimitedState.ts:59](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts#L59) 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. @@ -46,11 +46,11 @@ consider using the lower-level useRateLimiter hook instead. ### options -`RateLimiterOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`SetStateAction`\<`TValue`\>\]\> +`RateLimiterOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> ## Returns -\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, `RateLimiter`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`TValue`\]\>\] +\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, `RateLimiter`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] ## Example diff --git a/docs/framework/react/reference/functions/useratelimitedvalue.md b/docs/framework/react/reference/functions/useratelimitedvalue.md index 896ff4641..103c9ef91 100644 --- a/docs/framework/react/reference/functions/useratelimitedvalue.md +++ b/docs/framework/react/reference/functions/useratelimitedvalue.md @@ -8,10 +8,10 @@ title: useRateLimitedValue # Function: useRateLimitedValue() ```ts -function useRateLimitedValue(value, options): [TValue, RateLimiter>, [TValue]>] +function useRateLimitedValue(value, options): [TValue, RateLimiter>>] ``` -Defined in: [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:47](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts#L47) 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. @@ -26,7 +26,9 @@ For smoother update patterns, consider: Rate limiting should primarily be used when you need to enforce strict limits, like API rate limits. -The hook returns both the rate-limited value and the underlying rateLimiter instance for additional control. +The hook returns a tuple containing: +- The rate-limited value that updates according to the configured rate limit +- The rate limiter instance with control methods For more direct control over rate limiting behavior without React state management, consider using the lower-level useRateLimiter hook instead. @@ -43,17 +45,17 @@ consider using the lower-level useRateLimiter hook instead. ### options -`RateLimiterOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`SetStateAction`\<`TValue`\>\]\> +`RateLimiterOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> ## Returns -\[`TValue`, `RateLimiter`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`TValue`\]\>\] +\[`TValue`, `RateLimiter`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] ## Example ```tsx // Basic rate limiting - update at most 5 times per minute -const [rateLimitedValue] = useRateLimitedValue(rawValue, { +const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { limit: 5, window: 60000 }); @@ -66,14 +68,4 @@ const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { console.log(`Update rejected. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); } }); - -// Optionally access rateLimiter methods -const handleSubmit = () => { - const remaining = rateLimiter.getRemainingInWindow(); - if (remaining > 0) { - console.log(`${remaining} updates remaining in this window`); - } else { - console.log('Rate limit reached for this window'); - } -}; ``` diff --git a/docs/framework/react/reference/functions/useratelimiter.md b/docs/framework/react/reference/functions/useratelimiter.md index 9e0fdc536..59953e338 100644 --- a/docs/framework/react/reference/functions/useratelimiter.md +++ b/docs/framework/react/reference/functions/useratelimiter.md @@ -8,10 +8,10 @@ title: useRateLimiter # Function: useRateLimiter() ```ts -function useRateLimiter(fn, options): RateLimiter +function useRateLimiter(fn, options): RateLimiter ``` -Defined in: [rate-limiter/useRateLimiter.ts:48](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimiter.ts#L48) +Defined in: [react-pacer/src/rate-limiter/useRateLimiter.ts:48](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimiter.ts#L48) A low-level React hook that creates a `RateLimiter` instance to enforce rate limits on function execution. @@ -38,8 +38,6 @@ The hook returns an object containing: • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -48,11 +46,11 @@ The hook returns an object containing: ### options -`RateLimiterOptions`\<`TFn`, `TArgs`\> +`RateLimiterOptions`\<`TFn`\> ## Returns -`RateLimiter`\<`TFn`, `TArgs`\> +`RateLimiter`\<`TFn`\> ## Example diff --git a/docs/framework/react/reference/functions/usethrottledcallback.md b/docs/framework/react/reference/functions/usethrottledcallback.md index 1898d3784..429ede263 100644 --- a/docs/framework/react/reference/functions/usethrottledcallback.md +++ b/docs/framework/react/reference/functions/usethrottledcallback.md @@ -11,7 +11,7 @@ title: useThrottledCallback function useThrottledCallback(fn, options): (...args) => void ``` -Defined in: [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:43](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledCallback.ts#L43) A React hook that creates a throttled version of a callback function. This hook is essentially a wrapper around the basic `throttle` function @@ -47,7 +47,7 @@ Consider using the `useThrottler` hook instead. ### options -`ThrottlerOptions`\<`TFn`, `TArgs`\> +`ThrottlerOptions`\<`TFn`\> ## Returns diff --git a/docs/framework/react/reference/functions/usethrottledstate.md b/docs/framework/react/reference/functions/usethrottledstate.md index 780825175..1e4f7e845 100644 --- a/docs/framework/react/reference/functions/usethrottledstate.md +++ b/docs/framework/react/reference/functions/usethrottledstate.md @@ -8,10 +8,10 @@ title: useThrottledState # Function: useThrottledState() ```ts -function useThrottledState(value, options): [TValue, Dispatch>, Throttler>, [TValue]>] +function useThrottledState(value, options): [TValue, Dispatch>, Throttler>>] ``` -Defined in: [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:40](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledState.ts#L40) 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. @@ -39,11 +39,11 @@ consider using the lower-level useThrottler hook instead. ### options -`ThrottlerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`SetStateAction`\<`TValue`\>\]\> +`ThrottlerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> ## Returns -\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, `Throttler`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`TValue`\]\>\] +\[`TValue`, `Dispatch`\<`SetStateAction`\<`TValue`\>\>, `Throttler`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] ## Example diff --git a/docs/framework/react/reference/functions/usethrottledvalue.md b/docs/framework/react/reference/functions/usethrottledvalue.md index cfac412e8..db3d90c3a 100644 --- a/docs/framework/react/reference/functions/usethrottledvalue.md +++ b/docs/framework/react/reference/functions/usethrottledvalue.md @@ -8,10 +8,10 @@ title: useThrottledValue # Function: useThrottledValue() ```ts -function useThrottledValue(value, options): [TValue, Throttler>, [TValue]>] +function useThrottledValue(value, options): [TValue, Throttler>>] ``` -Defined in: [throttler/useThrottledValue.ts:36](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledValue.ts#L36) +Defined in: [react-pacer/src/throttler/useThrottledValue.ts:32](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledValue.ts#L32) 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. @@ -19,8 +19,9 @@ This hook uses React's useState internally to manage the throttled state. Throttling ensures the value updates occur at a controlled rate regardless of how frequently the input value changes. This is useful for rate-limiting expensive re-renders or API calls that depend on rapidly changing values. -The hook returns both the throttled value and the underlying throttler instance for additional control. -The throttled value will update according to the leading/trailing edge behavior specified in the options. +The hook returns a tuple containing: +- The throttled value that updates according to the leading/trailing edge behavior specified in the options +- The throttler instance with control methods For more direct control over throttling behavior without React state management, consider using the lower-level useThrottler hook instead. @@ -37,17 +38,17 @@ consider using the lower-level useThrottler hook instead. ### options -`ThrottlerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`SetStateAction`\<`TValue`\>\]\> +`ThrottlerOptions`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\> ## Returns -\[`TValue`, `Throttler`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>, \[`TValue`\]\>\] +\[`TValue`, `Throttler`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] ## Example ```tsx // Basic throttling - update at most once per second -const [throttledValue] = useThrottledValue(rawValue, { wait: 1000 }); +const [throttledValue, throttler] = useThrottledValue(rawValue, { wait: 1000 }); // With custom leading/trailing behavior const [throttledValue, throttler] = useThrottledValue(rawValue, { @@ -55,9 +56,4 @@ const [throttledValue, throttler] = useThrottledValue(rawValue, { leading: true, // Update immediately on first change trailing: false // Skip trailing edge updates }); - -// Optionally access throttler methods -const handleExecutionCount = () => { - console.log('Executions:', throttler.getExecutionCount()); -}; ``` diff --git a/docs/framework/react/reference/functions/usethrottler.md b/docs/framework/react/reference/functions/usethrottler.md index 2ec0a50e0..24ba9e71e 100644 --- a/docs/framework/react/reference/functions/usethrottler.md +++ b/docs/framework/react/reference/functions/usethrottler.md @@ -8,10 +8,10 @@ title: useThrottler # Function: useThrottler() ```ts -function useThrottler(fn, options): Throttler +function useThrottler(fn, options): Throttler ``` -Defined in: [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:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottler.ts#L42) A low-level React hook that creates a `Throttler` instance that limits how often the provided function can execute. @@ -27,8 +27,6 @@ expensive operations or UI updates. • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -37,11 +35,11 @@ expensive operations or UI updates. ### options -`ThrottlerOptions`\<`TFn`, `TArgs`\> +`ThrottlerOptions`\<`TFn`\> ## Returns -`Throttler`\<`TFn`, `TArgs`\> +`Throttler`\<`TFn`\> ## Example diff --git a/docs/framework/react/reference/index.md b/docs/framework/react/reference/index.md index a71247979..c96613be3 100644 --- a/docs/framework/react/reference/index.md +++ b/docs/framework/react/reference/index.md @@ -10,16 +10,17 @@ title: "@tanstack/react-pacer" ## Functions - [useAsyncDebouncer](functions/useasyncdebouncer.md) +- [useAsyncQueuedState](functions/useasyncqueuedstate.md) - [useAsyncQueuer](functions/useasyncqueuer.md) -- [useAsyncQueuerState](functions/useasyncqueuerstate.md) - [useAsyncRateLimiter](functions/useasyncratelimiter.md) - [useAsyncThrottler](functions/useasyncthrottler.md) - [useDebouncedCallback](functions/usedebouncedcallback.md) - [useDebouncedState](functions/usedebouncedstate.md) - [useDebouncedValue](functions/usedebouncedvalue.md) - [useDebouncer](functions/usedebouncer.md) +- [useQueuedState](functions/usequeuedstate.md) +- [useQueuedValue](functions/usequeuedvalue.md) - [useQueuer](functions/usequeuer.md) -- [useQueuerState](functions/usequeuerstate.md) - [useRateLimitedCallback](functions/useratelimitedcallback.md) - [useRateLimitedState](functions/useratelimitedstate.md) - [useRateLimitedValue](functions/useratelimitedvalue.md) diff --git a/docs/framework/solid/reference/functions/createasyncdebouncer.md b/docs/framework/solid/reference/functions/createasyncdebouncer.md index 55607ad73..fa0bf5b72 100644 --- a/docs/framework/solid/reference/functions/createasyncdebouncer.md +++ b/docs/framework/solid/reference/functions/createasyncdebouncer.md @@ -8,10 +8,10 @@ title: createAsyncDebouncer # Function: createAsyncDebouncer() ```ts -function createAsyncDebouncer(fn, initialOptions): SolidAsyncDebouncer +function createAsyncDebouncer(fn, initialOptions): SolidAsyncDebouncer ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:54](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L54) +Defined in: [async-debouncer/createAsyncDebouncer.ts:59](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L59) A low-level Solid hook that creates an `AsyncDebouncer` instance to delay execution of an async function. @@ -26,8 +26,6 @@ wait for user input to settle before making expensive async calls. • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -36,11 +34,11 @@ wait for user input to settle before making expensive async calls. ### initialOptions -`AsyncDebouncerOptions`\<`TFn`, `TArgs`\> +`AsyncDebouncerOptions`\<`TFn`\> ## Returns -[`SolidAsyncDebouncer`](../interfaces/solidasyncdebouncer.md)\<`TFn`, `TArgs`\> +[`SolidAsyncDebouncer`](../interfaces/solidasyncdebouncer.md)\<`TFn`\> ## Example diff --git a/docs/framework/solid/reference/functions/createasyncqueuer.md b/docs/framework/solid/reference/functions/createasyncqueuer.md index d9502920f..0b379474e 100644 --- a/docs/framework/solid/reference/functions/createasyncqueuer.md +++ b/docs/framework/solid/reference/functions/createasyncqueuer.md @@ -11,7 +11,7 @@ title: createAsyncQueuer function createAsyncQueuer(initialOptions): SolidAsyncQueuer ``` -Defined in: [async-queuer/createAsyncQueuer.ts:108](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L108) +Defined in: [async-queuer/createAsyncQueuer.ts:112](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L112) A lower-level React hook that creates an `AsyncQueuer` instance for managing an async queue of items. diff --git a/docs/framework/solid/reference/functions/createasyncratelimiter.md b/docs/framework/solid/reference/functions/createasyncratelimiter.md index 3cefe3cf5..05d8e82b5 100644 --- a/docs/framework/solid/reference/functions/createasyncratelimiter.md +++ b/docs/framework/solid/reference/functions/createasyncratelimiter.md @@ -8,10 +8,10 @@ title: createAsyncRateLimiter # Function: createAsyncRateLimiter() ```ts -function createAsyncRateLimiter(fn, initialOptions): SolidAsyncRateLimiter +function createAsyncRateLimiter(fn, initialOptions): SolidAsyncRateLimiter ``` -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:60](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L60) +Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:62](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L62) A low-level Solid hook that creates an `AsyncRateLimiter` instance to limit how many times an async function can execute within a time window. @@ -26,8 +26,6 @@ managing resource constraints, or controlling bursts of async operations. • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -36,11 +34,11 @@ managing resource constraints, or controlling bursts of async operations. ### initialOptions -`AsyncRateLimiterOptions`\<`TFn`, `TArgs`\> +`AsyncRateLimiterOptions`\<`TFn`\> ## Returns -[`SolidAsyncRateLimiter`](../interfaces/solidasyncratelimiter.md)\<`TFn`, `TArgs`\> +[`SolidAsyncRateLimiter`](../interfaces/solidasyncratelimiter.md)\<`TFn`\> ## Example diff --git a/docs/framework/solid/reference/functions/createasyncthrottler.md b/docs/framework/solid/reference/functions/createasyncthrottler.md index ea8b18f41..1de9036fe 100644 --- a/docs/framework/solid/reference/functions/createasyncthrottler.md +++ b/docs/framework/solid/reference/functions/createasyncthrottler.md @@ -8,10 +8,10 @@ title: createAsyncThrottler # Function: createAsyncThrottler() ```ts -function createAsyncThrottler(fn, initialOptions): SolidAsyncThrottler +function createAsyncThrottler(fn, initialOptions): SolidAsyncThrottler ``` -Defined in: [async-throttler/createAsyncThrottler.ts:61](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L61) +Defined in: [async-throttler/createAsyncThrottler.ts:67](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L67) A low-level Solid hook that creates an `AsyncThrottler` instance to limit how often an async function can execute. @@ -26,8 +26,6 @@ database operations, or other async tasks. • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -36,11 +34,11 @@ database operations, or other async tasks. ### initialOptions -`AsyncThrottlerOptions`\<`TFn`, `TArgs`\> +`AsyncThrottlerOptions`\<`TFn`\> ## Returns -[`SolidAsyncThrottler`](../interfaces/solidasyncthrottler.md)\<`TFn`, `TArgs`\> +[`SolidAsyncThrottler`](../interfaces/solidasyncthrottler.md)\<`TFn`\> ## Example diff --git a/docs/framework/solid/reference/functions/createdebouncedsignal.md b/docs/framework/solid/reference/functions/createdebouncedsignal.md index 1018f8b64..6bb180b0c 100644 --- a/docs/framework/solid/reference/functions/createdebouncedsignal.md +++ b/docs/framework/solid/reference/functions/createdebouncedsignal.md @@ -8,7 +8,7 @@ title: createDebouncedSignal # Function: createDebouncedSignal() ```ts -function createDebouncedSignal(value, initialOptions): [Accessor, Setter, SolidDebouncer, [Accessor]>] +function createDebouncedSignal(value, initialOptions): [Accessor, Setter, SolidDebouncer>] ``` Defined in: [debouncer/createDebouncedSignal.ts:46](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts#L46) @@ -38,11 +38,11 @@ The hook returns a tuple containing: ### initialOptions -`DebouncerOptions`\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\> +`DebouncerOptions`\<`Setter`\<`TValue`\>\> ## Returns -\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidDebouncer`](../interfaces/soliddebouncer.md)\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\>\] +\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidDebouncer`](../interfaces/soliddebouncer.md)\<`Setter`\<`TValue`\>\>\] ## Example diff --git a/docs/framework/solid/reference/functions/createdebouncedvalue.md b/docs/framework/solid/reference/functions/createdebouncedvalue.md index b8154734b..2ca6c35a9 100644 --- a/docs/framework/solid/reference/functions/createdebouncedvalue.md +++ b/docs/framework/solid/reference/functions/createdebouncedvalue.md @@ -8,10 +8,10 @@ title: createDebouncedValue # Function: createDebouncedValue() ```ts -function createDebouncedValue(value, initialOptions): [Accessor, SolidDebouncer, [Accessor]>] +function createDebouncedValue(value, initialOptions): [Accessor, SolidDebouncer>] ``` -Defined in: [debouncer/createDebouncedValue.ts:47](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncedValue.ts#L47) +Defined in: [debouncer/createDebouncedValue.ts:41](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncedValue.ts#L41) 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 @@ -26,8 +26,8 @@ like search queries or form inputs, where you want to limit how often downstream or calculations occur. The hook returns a tuple containing: -- The current debounced value (as an Accessor) -- The debouncer instance with control methods and state signals +- An Accessor that provides the current debounced value +- The debouncer instance with control methods ## Type Parameters @@ -41,11 +41,11 @@ The hook returns a tuple containing: ### initialOptions -`DebouncerOptions`\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\> +`DebouncerOptions`\<`Setter`\<`TValue`\>\> ## Returns -\[`Accessor`\<`TValue`\>, [`SolidDebouncer`](../interfaces/soliddebouncer.md)\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\>\] +\[`Accessor`\<`TValue`\>, [`SolidDebouncer`](../interfaces/soliddebouncer.md)\<`Setter`\<`TValue`\>\>\] ## Example @@ -61,12 +61,6 @@ createEffect(() => { fetchSearchResults(debouncedQuery()); }); -// Access debouncer state via signals -console.log('Executions:', debouncer.executionCount()); -console.log('Is pending:', debouncer.isPending()); - -// Handle input changes -const handleChange = (e) => { - setSearchQuery(e.target.value); -}; +// Control the debouncer +debouncer.cancel(); // Cancel any pending updates ``` diff --git a/docs/framework/solid/reference/functions/createdebouncer.md b/docs/framework/solid/reference/functions/createdebouncer.md index 9f7436758..74a3251c3 100644 --- a/docs/framework/solid/reference/functions/createdebouncer.md +++ b/docs/framework/solid/reference/functions/createdebouncer.md @@ -8,10 +8,10 @@ title: createDebouncer # Function: createDebouncer() ```ts -function createDebouncer(fn, initialOptions): SolidDebouncer +function createDebouncer(fn, initialOptions): SolidDebouncer ``` -Defined in: [debouncer/createDebouncer.ts:55](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L55) +Defined in: [debouncer/createDebouncer.ts:53](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L53) A Solid hook that creates and manages a Debouncer instance. @@ -31,8 +31,6 @@ timer resets and starts waiting again. • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -41,11 +39,11 @@ timer resets and starts waiting again. ### initialOptions -`DebouncerOptions`\<`TFn`, `TArgs`\> +`DebouncerOptions`\<`TFn`\> ## Returns -[`SolidDebouncer`](../interfaces/soliddebouncer.md)\<`TFn`, `TArgs`\> +[`SolidDebouncer`](../interfaces/soliddebouncer.md)\<`TFn`\> ## Example diff --git a/docs/framework/solid/reference/functions/createqueuer.md b/docs/framework/solid/reference/functions/createqueuer.md index 6c256fe4b..77cb39dfc 100644 --- a/docs/framework/solid/reference/functions/createqueuer.md +++ b/docs/framework/solid/reference/functions/createqueuer.md @@ -11,7 +11,7 @@ title: createQueuer function createQueuer(initialOptions): SolidQueuer ``` -Defined in: [queuer/createQueuer.ts:98](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L98) +Defined in: [queuer/createQueuer.ts:102](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L102) A Solid hook that creates and manages a Queuer instance. diff --git a/docs/framework/solid/reference/functions/createratelimitedsignal.md b/docs/framework/solid/reference/functions/createratelimitedsignal.md index 2732cdd40..2984da2bd 100644 --- a/docs/framework/solid/reference/functions/createratelimitedsignal.md +++ b/docs/framework/solid/reference/functions/createratelimitedsignal.md @@ -8,7 +8,7 @@ title: createRateLimitedSignal # Function: createRateLimitedSignal() ```ts -function createRateLimitedSignal(value, initialOptions): [Accessor, Setter, SolidRateLimiter, [Accessor]>] +function createRateLimitedSignal(value, initialOptions): [Accessor, Setter, SolidRateLimiter>] ``` Defined in: [rate-limiter/createRateLimitedSignal.ts:57](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts#L57) @@ -46,11 +46,11 @@ consider using the lower-level createRateLimiter hook instead. ### initialOptions -`RateLimiterOptions`\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\> +`RateLimiterOptions`\<`Setter`\<`TValue`\>\> ## Returns -\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidRateLimiter`](../interfaces/solidratelimiter.md)\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\>\] +\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidRateLimiter`](../interfaces/solidratelimiter.md)\<`Setter`\<`TValue`\>\>\] ## Example diff --git a/docs/framework/solid/reference/functions/createratelimitedvalue.md b/docs/framework/solid/reference/functions/createratelimitedvalue.md index 0f9648c22..3923e208c 100644 --- a/docs/framework/solid/reference/functions/createratelimitedvalue.md +++ b/docs/framework/solid/reference/functions/createratelimitedvalue.md @@ -8,10 +8,10 @@ title: createRateLimitedValue # Function: createRateLimitedValue() ```ts -function createRateLimitedValue(value, initialOptions): [Accessor, SolidRateLimiter, [Accessor]>] +function createRateLimitedValue(value, initialOptions): [Accessor, SolidRateLimiter>] ``` -Defined in: [rate-limiter/createRateLimitedValue.ts:54](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts#L54) +Defined in: [rate-limiter/createRateLimitedValue.ts:43](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts#L43) 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. @@ -26,7 +26,9 @@ For smoother update patterns, consider: Rate limiting should primarily be used when you need to enforce strict limits, like API rate limits. -The hook returns both the rate-limited value and the underlying rateLimiter instance for additional control. +The hook returns a tuple containing: +- An accessor function that provides the rate-limited value +- The rate limiter instance with control methods For more direct control over rate limiting behavior without Solid state management, consider using the lower-level createRateLimiter hook instead. @@ -43,37 +45,24 @@ consider using the lower-level createRateLimiter hook instead. ### initialOptions -`RateLimiterOptions`\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\> +`RateLimiterOptions`\<`Setter`\<`TValue`\>\> ## Returns -\[`Accessor`\<`TValue`\>, [`SolidRateLimiter`](../interfaces/solidratelimiter.md)\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\>\] +\[`Accessor`\<`TValue`\>, [`SolidRateLimiter`](../interfaces/solidratelimiter.md)\<`Setter`\<`TValue`\>\>\] ## Example ```tsx // Basic rate limiting - update at most 5 times per minute -const [rateLimitedValue] = createRateLimitedValue(rawValue, { +const [rateLimitedValue, rateLimiter] = createRateLimitedValue(rawValue, { limit: 5, window: 60000 }); -// With rejection callback -const [rateLimitedValue, rateLimiter] = createRateLimitedValue(rawValue, { - limit: 3, - window: 5000, - onReject: (rateLimiter) => { - console.log(`Update rejected. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); - } -}); +// Use the rate-limited value +console.log(rateLimitedValue()); // Access the current rate-limited value -// Optionally access rateLimiter state via signals -const handleSubmit = () => { - const remaining = rateLimiter.remainingInWindow(); - if (remaining > 0) { - console.log(`${remaining} updates remaining in this window`); - } else { - console.log('Rate limit reached for this window'); - } -}; +// Control the rate limiter +rateLimiter.reset(); // Reset the rate limit window ``` diff --git a/docs/framework/solid/reference/functions/createratelimiter.md b/docs/framework/solid/reference/functions/createratelimiter.md index 1cf8d339b..65a2faa12 100644 --- a/docs/framework/solid/reference/functions/createratelimiter.md +++ b/docs/framework/solid/reference/functions/createratelimiter.md @@ -8,10 +8,10 @@ title: createRateLimiter # Function: createRateLimiter() ```ts -function createRateLimiter(fn, initialOptions): SolidRateLimiter +function createRateLimiter(fn, initialOptions): SolidRateLimiter ``` -Defined in: [rate-limiter/createRateLimiter.ts:63](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L63) +Defined in: [rate-limiter/createRateLimiter.ts:61](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L61) A low-level Solid hook that creates a `RateLimiter` instance to enforce rate limits on function execution. @@ -31,8 +31,6 @@ For smoother execution patterns: • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -41,11 +39,11 @@ For smoother execution patterns: ### initialOptions -`RateLimiterOptions`\<`TFn`, `TArgs`\> +`RateLimiterOptions`\<`TFn`\> ## Returns -[`SolidRateLimiter`](../interfaces/solidratelimiter.md)\<`TFn`, `TArgs`\> +[`SolidRateLimiter`](../interfaces/solidratelimiter.md)\<`TFn`\> ## Example diff --git a/docs/framework/solid/reference/functions/createthrottledsignal.md b/docs/framework/solid/reference/functions/createthrottledsignal.md index 440279d25..e2e972bee 100644 --- a/docs/framework/solid/reference/functions/createthrottledsignal.md +++ b/docs/framework/solid/reference/functions/createthrottledsignal.md @@ -8,7 +8,7 @@ title: createThrottledSignal # Function: createThrottledSignal() ```ts -function createThrottledSignal(value, initialOptions): [Accessor, Setter, SolidThrottler, [Accessor]>] +function createThrottledSignal(value, initialOptions): [Accessor, Setter, SolidThrottler>] ``` Defined in: [throttler/createThrottledSignal.ts:41](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottledSignal.ts#L41) @@ -39,11 +39,11 @@ consider using the lower-level createThrottler hook instead. ### initialOptions -`ThrottlerOptions`\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\> +`ThrottlerOptions`\<`Setter`\<`TValue`\>\> ## Returns -\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidThrottler`](../interfaces/solidthrottler.md)\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\>\] +\[`Accessor`\<`TValue`\>, `Setter`\<`TValue`\>, [`SolidThrottler`](../interfaces/solidthrottler.md)\<`Setter`\<`TValue`\>\>\] ## Example diff --git a/docs/framework/solid/reference/functions/createthrottledvalue.md b/docs/framework/solid/reference/functions/createthrottledvalue.md index 2390936f4..4d74281ad 100644 --- a/docs/framework/solid/reference/functions/createthrottledvalue.md +++ b/docs/framework/solid/reference/functions/createthrottledvalue.md @@ -8,10 +8,10 @@ title: createThrottledValue # Function: createThrottledValue() ```ts -function createThrottledValue(value, initialOptions): [Accessor, SolidThrottler, [Accessor]>] +function createThrottledValue(value, initialOptions): [Accessor, SolidThrottler>] ``` -Defined in: [throttler/createThrottledValue.ts:39](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottledValue.ts#L39) +Defined in: [throttler/createThrottledValue.ts:35](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottledValue.ts#L35) 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. @@ -19,7 +19,10 @@ This hook uses Solid's createSignal internally to manage the throttled state. Throttling ensures the value updates occur at a controlled rate regardless of how frequently the input value changes. This is useful for rate-limiting expensive re-renders or API calls that depend on rapidly changing values. -The hook returns both the throttled value and the underlying throttler instance for additional control. +The hook returns a tuple containing: +- An accessor function that provides the throttled value +- The throttler instance with control methods + The throttled value will update according to the leading/trailing edge behavior specified in the options. For more direct control over throttling behavior without Solid state management, @@ -37,28 +40,21 @@ consider using the lower-level createThrottler hook instead. ### initialOptions -`ThrottlerOptions`\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\> +`ThrottlerOptions`\<`Setter`\<`TValue`\>\> ## Returns -\[`Accessor`\<`TValue`\>, [`SolidThrottler`](../interfaces/solidthrottler.md)\<`Setter`\<`TValue`\>, \[`Accessor`\<`TValue`\>\]\>\] +\[`Accessor`\<`TValue`\>, [`SolidThrottler`](../interfaces/solidthrottler.md)\<`Setter`\<`TValue`\>\>\] ## Example ```tsx // Basic throttling - update at most once per second -const [throttledValue] = createThrottledValue(rawValue, { wait: 1000 }); - -// With custom leading/trailing behavior -const [throttledValue, throttler] = createThrottledValue(rawValue, { - wait: 1000, - leading: true, // Update immediately on first change - trailing: false // Skip trailing edge updates -}); - -// Access throttler state via signals -console.log('Executions:', throttler.executionCount()); -console.log('Is pending:', throttler.isPending()); -console.log('Last execution:', throttler.lastExecutionTime()); -console.log('Next execution:', throttler.nextExecutionTime()); +const [throttledValue, throttler] = createThrottledValue(rawValue, { wait: 1000 }); + +// Use the throttled value +console.log(throttledValue()); // Access the current throttled value + +// Control the throttler +throttler.cancel(); // Cancel any pending updates ``` diff --git a/docs/framework/solid/reference/functions/createthrottler.md b/docs/framework/solid/reference/functions/createthrottler.md index e1fb8ce51..63c783b1a 100644 --- a/docs/framework/solid/reference/functions/createthrottler.md +++ b/docs/framework/solid/reference/functions/createthrottler.md @@ -8,10 +8,10 @@ title: createThrottler # Function: createThrottler() ```ts -function createThrottler(fn, initialOptions): SolidThrottler +function createThrottler(fn, initialOptions): SolidThrottler ``` -Defined in: [throttler/createThrottler.ts:61](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L61) +Defined in: [throttler/createThrottler.ts:59](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L59) A low-level Solid hook that creates a `Throttler` instance that limits how often the provided function can execute. @@ -27,8 +27,6 @@ expensive operations or UI updates. • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -37,11 +35,11 @@ expensive operations or UI updates. ### initialOptions -`ThrottlerOptions`\<`TFn`, `TArgs`\> +`ThrottlerOptions`\<`TFn`\> ## Returns -[`SolidThrottler`](../interfaces/solidthrottler.md)\<`TFn`, `TArgs`\> +[`SolidThrottler`](../interfaces/solidthrottler.md)\<`TFn`\> ## Example diff --git a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md index 4bb02fe4f..ee5169ed4 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md +++ b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md @@ -5,29 +5,32 @@ 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) ## Extends -- `Omit`\<`AsyncDebouncer`\<`TFn`, `TArgs`\>, `"getExecutionCount"` \| `"getIsPending"`\> +- `Omit`\<`AsyncDebouncer`\<`TFn`\>, + \| `"getErrorCount"` + \| `"getIsPending"` + \| `"getLastResult"` + \| `"getSettleCount"` + \| `"getSuccessCount"`\> ## Type Parameters • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties -### executionCount +### errorCount ```ts -executionCount: Accessor; +errorCount: Accessor; ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:15](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L15) +Defined in: [async-debouncer/createAsyncDebouncer.ts:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L17) *** @@ -37,4 +40,34 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:15](https://github.com/TanS isPending: Accessor; ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:16](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L16) +Defined in: [async-debouncer/createAsyncDebouncer.ts:18](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L18) + +*** + +### 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) + +*** + +### settleCount + +```ts +settleCount: 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; +``` + +Defined in: [async-debouncer/createAsyncDebouncer.ts:21](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L21) diff --git a/docs/framework/solid/reference/interfaces/solidasyncqueuer.md b/docs/framework/solid/reference/interfaces/solidasyncqueuer.md index cc75d2e68..6e573aa16 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncqueuer.md +++ b/docs/framework/solid/reference/interfaces/solidasyncqueuer.md @@ -116,7 +116,7 @@ Signal version of `getIsRunning` ### peek ```ts -peek: Accessor; +peek: Accessor Promise>; ``` Defined in: [async-queuer/createAsyncQueuer.ts:52](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L52) @@ -137,12 +137,24 @@ Signal version of `getPendingItems` *** +### rejectionCount + +```ts +rejectionCount: Accessor; +``` + +Defined in: [async-queuer/createAsyncQueuer.ts:60](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L60) + +Signal version of `getRejectionCount` + +*** + ### size ```ts size: Accessor; ``` -Defined in: [async-queuer/createAsyncQueuer.ts:60](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L60) +Defined in: [async-queuer/createAsyncQueuer.ts:64](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts#L64) Signal version of `getSize` diff --git a/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md b/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md index 789a5c4b5..3cfca7680 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md +++ b/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md @@ -5,14 +5,16 @@ 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) ## Extends -- `Omit`\<`AsyncRateLimiter`\<`TFn`, `TArgs`\>, - \| `"getExecutionCount"` +- `Omit`\<`AsyncRateLimiter`\<`TFn`\>, + \| `"getSuccessCount"` + \| `"getSettleCount"` + \| `"getErrorCount"` \| `"getRejectionCount"` \| `"getRemainingInWindow"` \| `"getMsUntilNextWindow"`\> @@ -21,17 +23,15 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:8](https://github.com/ • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties -### executionCount +### errorCount ```ts -executionCount: Accessor; +errorCount: 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:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L20) *** @@ -41,7 +41,7 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:18](https://github.com msUntilNextWindow: 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) +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) *** @@ -51,7 +51,7 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:21](https://github.com rejectionCount: 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) +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) *** @@ -61,4 +61,24 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:19](https://github.com remainingInWindow: 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) +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) + +*** + +### successCount + +```ts +successCount: 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) diff --git a/docs/framework/solid/reference/interfaces/solidasyncthrottler.md b/docs/framework/solid/reference/interfaces/solidasyncthrottler.md index 7d131a0de..2e8462ca9 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncthrottler.md +++ b/docs/framework/solid/reference/interfaces/solidasyncthrottler.md @@ -5,15 +5,19 @@ 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) ## Extends -- `Omit`\<`AsyncThrottler`\<`TFn`, `TArgs`\>, - \| `"getExecutionCount"` +- `Omit`\<`AsyncThrottler`\<`TFn`\>, + \| `"getSuccessCount"` + \| `"getSettleCount"` + \| `"getErrorCount"` \| `"getIsPending"` + \| `"getIsExecuting"` + \| `"getLastResult"` \| `"getLastExecutionTime"` \| `"getNextExecutionTime"`\> @@ -21,17 +25,25 @@ Defined in: [async-throttler/createAsyncThrottler.ts:8](https://github.com/TanSt • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties -### executionCount +### 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 -executionCount: Accessor; +isExecuting: Accessor; ``` -Defined in: [async-throttler/createAsyncThrottler.ts:18](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L18) +Defined in: [async-throttler/createAsyncThrottler.ts:24](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L24) *** @@ -41,7 +53,7 @@ Defined in: [async-throttler/createAsyncThrottler.ts:18](https://github.com/TanS isPending: Accessor; ``` -Defined in: [async-throttler/createAsyncThrottler.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L19) +Defined in: [async-throttler/createAsyncThrottler.ts:23](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L23) *** @@ -51,7 +63,17 @@ Defined in: [async-throttler/createAsyncThrottler.ts:19](https://github.com/TanS lastExecutionTime: Accessor; ``` -Defined in: [async-throttler/createAsyncThrottler.ts:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L20) +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) *** @@ -61,4 +83,24 @@ Defined in: [async-throttler/createAsyncThrottler.ts:20](https://github.com/TanS 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) + +*** + +### settleCount + +```ts +settleCount: 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 + +```ts +successCount: Accessor; +``` + +Defined in: [async-throttler/createAsyncThrottler.ts:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L20) diff --git a/docs/framework/solid/reference/interfaces/soliddebouncer.md b/docs/framework/solid/reference/interfaces/soliddebouncer.md index 1f29fb494..fa3285f86 100644 --- a/docs/framework/solid/reference/interfaces/soliddebouncer.md +++ b/docs/framework/solid/reference/interfaces/soliddebouncer.md @@ -5,7 +5,7 @@ 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) @@ -13,14 +13,12 @@ An extension of the Debouncer class that adds Solid signals to access the intern ## Extends -- `Omit`\<`Debouncer`\<`TFn`, `TArgs`\>, `"getExecutionCount"` \| `"getIsPending"`\> +- `Omit`\<`Debouncer`\<`TFn`\>, `"getExecutionCount"` \| `"getIsPending"`\> ## Type Parameters • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### executionCount @@ -29,7 +27,7 @@ An extension of the Debouncer class that adds Solid signals to access the intern executionCount: Accessor; ``` -Defined in: [debouncer/createDebouncer.ts:15](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L15) +Defined in: [debouncer/createDebouncer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L13) *** @@ -39,4 +37,4 @@ Defined in: [debouncer/createDebouncer.ts:15](https://github.com/TanStack/pacer/ isPending: Accessor; ``` -Defined in: [debouncer/createDebouncer.ts:16](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L16) +Defined in: [debouncer/createDebouncer.ts:14](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncer.ts#L14) diff --git a/docs/framework/solid/reference/interfaces/solidqueuer.md b/docs/framework/solid/reference/interfaces/solidqueuer.md index 50dcda9c9..4497cba7b 100644 --- a/docs/framework/solid/reference/interfaces/solidqueuer.md +++ b/docs/framework/solid/reference/interfaces/solidqueuer.md @@ -111,12 +111,24 @@ Signal version of `getPeek` *** +### rejectionCount + +```ts +rejectionCount: 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:50](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L50) +Defined in: [queuer/createQueuer.ts:54](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/queuer/createQueuer.ts#L54) Signal version of `getSize` diff --git a/docs/framework/solid/reference/interfaces/solidratelimiter.md b/docs/framework/solid/reference/interfaces/solidratelimiter.md index c517cff25..f8cbc71b6 100644 --- a/docs/framework/solid/reference/interfaces/solidratelimiter.md +++ b/docs/framework/solid/reference/interfaces/solidratelimiter.md @@ -5,13 +5,13 @@ 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) ## Extends -- `Omit`\<`RateLimiter`\<`TFn`, `TArgs`\>, +- `Omit`\<`RateLimiter`\<`TFn`\>, \| `"getExecutionCount"` \| `"getMsUntilNextWindow"` \| `"getRejectionCount"` @@ -21,8 +21,6 @@ Defined in: [rate-limiter/createRateLimiter.ts:8](https://github.com/TanStack/pa • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### executionCount @@ -31,7 +29,7 @@ Defined in: [rate-limiter/createRateLimiter.ts:8](https://github.com/TanStack/pa executionCount: Accessor; ``` -Defined in: [rate-limiter/createRateLimiter.ts:18](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L18) +Defined in: [rate-limiter/createRateLimiter.ts:16](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L16) *** @@ -41,7 +39,7 @@ Defined in: [rate-limiter/createRateLimiter.ts:18](https://github.com/TanStack/p msUntilNextWindow: Accessor; ``` -Defined in: [rate-limiter/createRateLimiter.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L19) +Defined in: [rate-limiter/createRateLimiter.ts:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L17) *** @@ -51,7 +49,7 @@ Defined in: [rate-limiter/createRateLimiter.ts:19](https://github.com/TanStack/p rejectionCount: Accessor; ``` -Defined in: [rate-limiter/createRateLimiter.ts:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L20) +Defined in: [rate-limiter/createRateLimiter.ts:18](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L18) *** @@ -61,4 +59,4 @@ Defined in: [rate-limiter/createRateLimiter.ts:20](https://github.com/TanStack/p remainingInWindow: Accessor; ``` -Defined in: [rate-limiter/createRateLimiter.ts:21](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L21) +Defined in: [rate-limiter/createRateLimiter.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L19) diff --git a/docs/framework/solid/reference/interfaces/solidthrottler.md b/docs/framework/solid/reference/interfaces/solidthrottler.md index 7aba89a65..e7b17ad12 100644 --- a/docs/framework/solid/reference/interfaces/solidthrottler.md +++ b/docs/framework/solid/reference/interfaces/solidthrottler.md @@ -5,7 +5,7 @@ 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) @@ -13,7 +13,7 @@ An extension of the Throttler class that adds Solid signals to access the intern ## Extends -- `Omit`\<`Throttler`\<`TFn`, `TArgs`\>, +- `Omit`\<`Throttler`\<`TFn`\>, \| `"getExecutionCount"` \| `"getIsPending"` \| `"getLastExecutionTime"` @@ -23,8 +23,6 @@ An extension of the Throttler class that adds Solid signals to access the intern • **TFn** *extends* `AnyFunction` -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### executionCount @@ -33,7 +31,7 @@ An extension of the Throttler class that adds Solid signals to access the intern executionCount: Accessor; ``` -Defined in: [throttler/createThrottler.ts:21](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L21) +Defined in: [throttler/createThrottler.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L19) *** @@ -43,7 +41,7 @@ Defined in: [throttler/createThrottler.ts:21](https://github.com/TanStack/pacer/ isPending: Accessor; ``` -Defined in: [throttler/createThrottler.ts:22](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L22) +Defined in: [throttler/createThrottler.ts:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L20) *** @@ -53,7 +51,7 @@ Defined in: [throttler/createThrottler.ts:22](https://github.com/TanStack/pacer/ lastExecutionTime: Accessor; ``` -Defined in: [throttler/createThrottler.ts:23](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L23) +Defined in: [throttler/createThrottler.ts:21](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L21) *** @@ -63,4 +61,4 @@ Defined in: [throttler/createThrottler.ts:23](https://github.com/TanStack/pacer/ nextExecutionTime: Accessor; ``` -Defined in: [throttler/createThrottler.ts:24](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L24) +Defined in: [throttler/createThrottler.ts:22](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottler.ts#L22) diff --git a/docs/guides/async-queueing.md b/docs/guides/async-queueing.md index 7b009be22..4a9cdd626 100644 --- a/docs/guides/async-queueing.md +++ b/docs/guides/async-queueing.md @@ -387,4 +387,4 @@ console.log(queue.getPendingItems().length) // Tasks waiting to be processed ### Framework Adapters -Each framework adapter builds convenient hooks and functions around the async queuer classes. Hooks like `useAsyncQueuer` or `useAsyncQueuerState` are small wrappers that can cut down on the boilerplate needed in your own code for some common use cases. +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/debouncing.md b/docs/guides/debouncing.md index 1af64dabf..c316f36f1 100644 --- a/docs/guides/debouncing.md +++ b/docs/guides/debouncing.md @@ -113,7 +113,7 @@ Common patterns: ### Max Wait Time -The TanStack Pacer Debouncer does NOT have a `maxWait` option like other debouncing libraries. If you need to let executions run over a more spread out period of time, consider using the [throttling](../guides/throttling) technique instead. +The TanStack Pacer Debouncer purposely does NOT have a `maxWait` option like other debouncing libraries. If you need to let executions run over a more spread out period of time, consider using the [throttling](../guides/throttling) technique instead. ### Enabling/Disabling @@ -166,16 +166,20 @@ The `onExecute` callback is called after each successful execution of the deboun #### Asynchronous Debouncer Callbacks -The asynchronous `AsyncDebouncer` supports additional callbacks for error handling: +The asynchronous `AsyncDebouncer` has a different set of callbacks compared to the synchronous version. ```ts const asyncDebouncer = new AsyncDebouncer(async (value) => { await saveToAPI(value) }, { wait: 500, - onExecute: (debouncer) => { + onSuccess: (result, debouncer) => { // Called after each successful execution - console.log('Async function executed', debouncer.getExecutionCount()) + console.log('Async function executed', debouncer.getSuccessCount()) + }, + onSettled: (debouncer) => { + // Called after each execution attempt + console.log('Async function settled', debouncer.getSettledCount()) }, onError: (error) => { // Called if the async function throws an error @@ -184,35 +188,71 @@ const asyncDebouncer = new AsyncDebouncer(async (value) => { }) ``` -The `onExecute` callback works the same way as in the synchronous debouncer, while the `onError` callback allows you to handle errors gracefully without breaking the debouncing chain. These callbacks are particularly useful for tracking execution counts, updating UI state, handling errors, performing cleanup operations, and logging execution metrics. +The `onSuccess` callback is called after each successful execution of the debounced function, while the `onError` callback is called if the async function throws an error. The `onSettled` callback is called after each execution attempt, regardless of success or failure. These callbacks are particularly useful for tracking execution counts, updating UI state, handling errors, performing cleanup operations, and logging execution metrics. ### Asynchronous Debouncing -For async functions or when you need error handling, use the `AsyncDebouncer` or `asyncDebounce`: +The async debouncer provides a powerful way to handle asynchronous operations with debouncing, offering several key advantages over the synchronous version. While the synchronous debouncer is great for UI events and immediate feedback, the async version is specifically designed for handling API calls, database operations, and other asynchronous tasks. -```ts -import { asyncDebounce } from '@tanstack/pacer' +#### Key Differences from Synchronous Debouncing + +1. **Return Value Handling** +Unlike the synchronous debouncer which returns void, the async version allows you to capture and use the return value from your debounced function. This is particularly useful when you need to work with the results of API calls or other async operations. The `maybeExecute` method returns a Promise that resolves with the function's return value, allowing you to await the result and handle it appropriately. + +2. **Enhanced Callback System** +The async debouncer provides a more sophisticated callback system compared to the synchronous version's single `onExecute` callback. This system includes: +- `onSuccess`: Called when the async function completes successfully, providing both the result and the debouncer instance +- `onError`: Called when the async function throws an error, providing both the error and the debouncer instance +- `onSettled`: Called after every execution attempt, regardless of success or failure + +3. **Execution Tracking** +The async debouncer provides comprehensive execution tracking through several methods: +- `getSuccessCount()`: Number of successful executions +- `getErrorCount()`: Number of failed executions +- `getSettledCount()`: Total number of settled executions (success + error) + +4. **Sequential Execution** +The async debouncer ensures that subsequent executions wait for the previous call to complete before starting. This prevents out-of-order execution and guarantees that each call processes the most up-to-date data. This is particularly important when dealing with operations that depend on the results of previous calls or when maintaining data consistency is critical. + +For example, if you're updating a user's profile and then immediately fetching their updated data, the async debouncer will ensure the fetch operation waits for the update to complete, preventing race conditions where you might get stale data. +#### Basic Usage Example + +Here's a basic example showing how to use the async debouncer for a search operation: + +```ts const debouncedSearch = asyncDebounce( async (searchTerm: string) => { const results = await fetchSearchResults(searchTerm) - updateUI(results) + return results }, { wait: 500, - onError: (error) => { + onSuccess: (results, debouncer) => { + console.log('Search succeeded:', results) + }, + onError: (error, debouncer) => { console.error('Search failed:', error) } } ) -// Will only make one API call after typing stops -searchInput.addEventListener('input', async (e) => { - await debouncedSearch(e.target.value) -}) +// Usage +const results = await debouncedSearch('query') ``` -The async version provides Promise-based execution tracking, error handling through the `onError` callback, proper cleanup of pending async operations, and an awaitable `maybeExecute` method. +#### Advanced Patterns + +The async debouncer can be combined with various patterns to solve complex problems: + +1. **State Management Integration** +When using the async debouncer with state management systems (like React's useState or Solid's createSignal), you can create powerful patterns for handling loading states, error states, and data updates. The debouncer's callbacks provide perfect hooks for updating UI state based on the success or failure of operations. + +2. **Race Condition Prevention** +The single-flight mutation pattern naturally prevents race conditions in many scenarios. When multiple parts of your application try to update the same resource simultaneously, the debouncer ensures that only the most recent update actually occurs, while still providing results to all callers. + +3. **Error Recovery** +The async debouncer's error handling capabilities make it ideal for implementing retry logic and error recovery patterns. You can use the `onError` callback to implement custom error handling strategies, such as exponential backoff or fallback mechanisms. ### Framework Adapters @@ -239,7 +279,7 @@ const handleSearch = useDebouncedCallback( // State-based hook for reactive state management const [instantState, setInstantState] = useState('') -const [debouncedState, setDebouncedState] = useDebouncedValue( +const [debouncedValue] = useDebouncedValue( instantState, // Value to debounce { wait: 500 } ) diff --git a/docs/guides/queueing.md b/docs/guides/queueing.md index 66dc7909d..2b4dc2718 100644 --- a/docs/guides/queueing.md +++ b/docs/guides/queueing.md @@ -3,11 +3,11 @@ title: Queueing Guide id: queueing --- -Unlike [Rate Limiting](../guides/rate-limiting), [Throttling](../guides/throttling), and [Debouncing](../guides/debouncing) which drop executions when they occur too frequently, queuers ensure that every operation is processed. They provide a way to manage and control the flow of operations without losing any requests. This makes them ideal for scenarios where data loss is unacceptable. This guide will cover the Queueing concepts of TanStack Pacer. +Unlike [Rate Limiting](../guides/rate-limiting), [Throttling](../guides/throttling), and [Debouncing](../guides/debouncing) which drop executions when they occur too frequently, queuers can be configured to ensure that every operation is processed. They provide a way to manage and control the flow of operations without losing any requests. This makes them ideal for scenarios where data loss is unacceptable. Queueing can also be set to have a maximum size, which can be useful for preventing memory leaks or other issues. This guide will cover the Queueing concepts of TanStack Pacer. ## Queueing Concept -Queueing ensures that every operation is eventually processed, even if they come in faster than they can be handled. Unlike the other execution control techniques that drop excess operations, queueing buffers operations in an ordered list and processes them according to specific rules. This makes queueing the only "lossless" execution control technique in TanStack Pacer. +Queueing ensures that every operation is eventually processed, even if they come in faster than they can be handled. Unlike the other execution control techniques that drop excess operations, queueing buffers operations in an ordered list and processes them according to specific rules. This makes queueing the only "lossless" execution control technique in TanStack Pacer, unless a `maxSize` is specified which can cause items to be rejected when the buffer is full. ### Queueing Visualization @@ -27,15 +27,17 @@ Executed: ✅ ✅ ✅ ✅ ✅ ✅ ### When to Use Queueing -Queueing is particularly important when you need to ensure that every operation is processed, even if it means introducing some delay. This makes it ideal for scenarios where data consistency and completeness are more important than immediate execution. +Queueing is particularly important when you need to ensure that every operation is processed, even if it means introducing some delay. This makes it ideal for scenarios where data consistency and completeness are more important than immediate execution. When using a `maxSize`, it can also serve as a buffer to prevent overwhelming a system with too many pending operations. Common use cases include: +- Pre-fetching data before it's needed without overloading the system - Processing user interactions in a UI where every action must be recorded - Handling database operations that need to maintain data consistency - Managing API requests that must all complete successfully - Coordinating background tasks that can't be dropped - Animation sequences where every frame matters - Form submissions where every entry needs to be saved +- Buffering data streams with a fixed capacity using `maxSize` ### When Not to Use Queueing @@ -60,6 +62,7 @@ import { queue } from '@tanstack/pacer' // Create a queue that processes items every second const processItems = queue({ wait: 1000, + maxSize: 10, // Optional: limit queue size to prevent memory or time issues onItemsChange: (queuer) => { console.log('Current queue:', queuer.getAllItems()) } @@ -83,6 +86,7 @@ import { Queuer } from '@tanstack/pacer' // Create a queue that processes items every second const queue = new Queuer({ wait: 1000, // Wait 1 second between processing items + maxSize: 5, // Optional: limit queue size to prevent memory or time issues onItemsChange: (queuer) => { console.log('Current queue:', queuer.getAllItems()) } @@ -109,20 +113,22 @@ What makes TanStack Pacer's Queuer unique is its ability to adapt to different u #### FIFO Queue (First In, First Out) -The default behavior where items are processed in the order they were added. This is the most common queue type and follows the principle that the first item added should be the first one processed. +The default behavior where items are processed in the order they were added. This is the most common queue type and follows the principle that the first item added should be the first one processed. When using `maxSize`, new items will be rejected if the queue is full. ```text -FIFO Queue Visualization: +FIFO Queue Visualization (with maxSize=3): -Entry → [A][B][C][D] → Exit - ⬇️ ⬆️ +Entry → [A][B][C] → Exit + ⬇️ ⬆️ New items Items are added here processed here + (rejected if full) Timeline: [1 second per tick] -Calls: ⬇️ ⬇️ ⬇️ ⬇️ +Calls: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ Queue: [ABC] [BC] [C] [] Processed: A B C +Rejected: D E ``` FIFO queues are ideal for: @@ -143,22 +149,23 @@ queue.addItem(2) // [1, 2] #### LIFO Stack (Last In, First Out) -By specifying 'back' as the position for both adding and retrieving items, the queuer behaves like a stack. In a stack, the most recently added item is the first one to be processed. +By specifying 'back' as the position for both adding and retrieving items, the queuer behaves like a stack. In a stack, the most recently added item is the first one to be processed. When using `maxSize`, new items will be rejected if the stack is full. ```text -LIFO Stack Visualization: +LIFO Stack Visualization (with maxSize=3): ⬆️ Process - [D] ← Most recently added - [C] + [C] ← Most recently added [B] [A] ← First added ⬇️ Entry + (rejected if full) Timeline: [1 second per tick] -Calls: ⬇️ ⬇️ ⬇️ ⬇️ +Calls: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ Queue: [ABC] [AB] [A] [] Processed: C B A +Rejected: D E ``` Stack behavior is particularly useful for: @@ -181,20 +188,22 @@ stack.getNextItem('back') // get next item from back of queue instead of front #### Priority Queue -Priority queues add another dimension to queue ordering by allowing items to be sorted based on their priority rather than just their insertion order. Each item is assigned a priority value, and the queue automatically maintains the items in priority order. +Priority queues add another dimension to queue ordering by allowing items to be sorted based on their priority rather than just their insertion order. Each item is assigned a priority value, and the queue automatically maintains the items in priority order. When using `maxSize`, lower priority items may be rejected if the queue is full. ```text -Priority Queue Visualization: +Priority Queue Visualization (with maxSize=3): -Entry → [P:5][P:3][P:2][P:1] → Exit +Entry → [P:5][P:3][P:2] → Exit ⬇️ ⬆️ High Priority Low Priority items here processed last + (rejected if full) Timeline: [1 second per tick] Calls: ⬇️(P:2) ⬇️(P:5) ⬇️(P:1) ⬇️(P:3) Queue: [2] [5,2] [5,2,1] [3,2,1] [2,1] [1] [] Processed: 5 - 3 2 1 +Rejected: 4 ``` Priority queues are essential for: @@ -267,6 +276,39 @@ queue.onItemsChange((item) => { }) ``` +### Item Expiration + +The Queuer supports automatic expiration of items that have been in the queue too long. This is useful for preventing stale data from being processed or for implementing timeouts on queued operations. + +```ts +const queue = new Queuer({ + expirationDuration: 5000, // Items expire after 5 seconds + onExpire: (item, queuer) => { + console.log('Item expired:', item) + } +}) + +// Or use a custom expiration check +const queue = new Queuer({ + getIsExpired: (item, addedAt) => { + // Custom expiration logic + return Date.now() - addedAt > 5000 + }, + onExpire: (item, queuer) => { + console.log('Item expired:', item) + } +}) + +// Check expiration statistics +console.log(queue.getExpirationCount()) // Number of items that have expired +``` + +Expiration features are particularly useful for: +- Preventing stale data from being processed +- Implementing timeouts on queued operations +- Managing memory usage by automatically removing old items +- Handling temporary data that should only be valid for a limited time + ### Rejection Handling When a queue reaches its maximum size (set by `maxSize` option), new items will be rejected. The Queuer provides ways to handle and monitor these rejections: diff --git a/docs/guides/rate-limiting.md b/docs/guides/rate-limiting.md index 73bc78ac1..3fbc417d1 100644 --- a/docs/guides/rate-limiting.md +++ b/docs/guides/rate-limiting.md @@ -179,15 +179,37 @@ The `onExecute` and `onReject` callbacks work the same way as in the synchronous ### Asynchronous Rate Limiting -Use the `AsyncRateLimiter` when: -- Your rate-limited function returns a Promise -- You need to handle errors from the async function -- You want to ensure proper rate limiting even if the async function takes time to complete +The async rate limiter provides a powerful way to handle asynchronous operations with rate limiting, offering several key advantages over the synchronous version. While the synchronous rate limiter is great for UI events and immediate feedback, the async version is specifically designed for handling API calls, database operations, and other asynchronous tasks. -```ts -import { asyncRateLimit } from '@tanstack/pacer' +#### Key Differences from Synchronous Rate Limiting + +1. **Return Value Handling** +Unlike the synchronous rate limiter which returns a boolean indicating success, the async version allows you to capture and use the return value from your rate-limited function. This is particularly useful when you need to work with the results of API calls or other async operations. The `maybeExecute` method returns a Promise that resolves with the function's return value, allowing you to await the result and handle it appropriately. + +2. **Enhanced Callback System** +The async rate limiter provides a more sophisticated callback system compared to the synchronous version's callbacks. This system includes: +- `onExecute`: Called after each successful execution, providing the rate limiter instance +- `onReject`: Called when an execution is rejected due to rate limiting, providing the rate limiter instance +- `onError`: Called if the async function throws an error, providing both the error and the rate limiter instance + +3. **Execution Tracking** +The async rate limiter provides comprehensive execution tracking through several methods: +- `getExecutionCount()`: Number of successful executions +- `getRejectionCount()`: Number of rejected executions +- `getRemainingInWindow()`: Number of executions remaining in current window +- `getMsUntilNextWindow()`: Milliseconds until the next window starts + +4. **Sequential Execution** +The async rate limiter ensures that subsequent executions wait for the previous call to complete before starting. This prevents out-of-order execution and guarantees that each call processes the most up-to-date data. This is particularly important when dealing with operations that depend on the results of previous calls or when maintaining data consistency is critical. + +For example, if you're updating a user's profile and then immediately fetching their updated data, the async rate limiter will ensure the fetch operation waits for the update to complete, preventing race conditions where you might get stale data. + +#### Basic Usage Example + +Here's a basic example showing how to use the async rate limiter for an API operation: -const rateLimited = asyncRateLimit( +```ts +const rateLimitedApi = asyncRateLimit( async (id: string) => { const response = await fetch(`/api/data/${id}`) return response.json() @@ -195,17 +217,34 @@ const rateLimited = asyncRateLimit( { limit: 5, window: 1000, - onError: (error) => { + onExecute: (limiter) => { + console.log('API call succeeded:', limiter.getExecutionCount()) + }, + onReject: (limiter) => { + console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`) + }, + onError: (error, limiter) => { console.error('API call failed:', error) } } ) -// Returns a Promise - resolves to true if executed, false if rejected -const wasExecuted = await rateLimited('123') +// Usage +const result = await rateLimitedApi('123') ``` -The async version provides Promise-based execution tracking, error handling through the `onError` callback, proper cleanup of pending async operations, and an awaitable `maybeExecute` method. +#### Advanced Patterns + +The async rate limiter can be combined with various patterns to solve complex problems: + +1. **State Management Integration** +When using the async rate limiter with state management systems (like React's useState or Solid's createSignal), you can create powerful patterns for handling loading states, error states, and data updates. The rate limiter's callbacks provide perfect hooks for updating UI state based on the success or failure of operations. + +2. **Race Condition Prevention** +The rate limiting pattern naturally prevents race conditions in many scenarios. When multiple parts of your application try to update the same resource simultaneously, the rate limiter ensures that updates occur within the configured limits, while still providing results to all callers. + +3. **Error Recovery** +The async rate limiter's error handling capabilities make it ideal for implementing retry logic and error recovery patterns. You can use the `onError` callback to implement custom error handling strategies, such as exponential backoff or fallback mechanisms. ### Framework Adapters @@ -232,7 +271,7 @@ const handleFetch = useRateLimitedCallback( // State-based hook for reactive state management const [instantState, setInstantState] = useState('') -const [rateLimitedState, setRateLimitedState] = useRateLimitedValue( +const [rateLimitedValue] = useRateLimitedValue( instantState, // Value to rate limit { limit: 5, window: 1000 } ) diff --git a/docs/guides/throttling.md b/docs/guides/throttling.md index 817f318f0..0f0827bd3 100644 --- a/docs/guides/throttling.md +++ b/docs/guides/throttling.md @@ -168,29 +168,67 @@ The `onExecute` callback works the same way as in the synchronous throttler, whi ### Asynchronous Throttling -For async functions or when you need error handling, use the `AsyncThrottler` or `asyncThrottle`: +The async throttler provides a powerful way to handle asynchronous operations with throttling, offering several key advantages over the synchronous version. While the synchronous throttler is great for UI events and immediate feedback, the async version is specifically designed for handling API calls, database operations, and other asynchronous tasks. -```ts -import { asyncThrottle } from '@tanstack/pacer' +#### Key Differences from Synchronous Throttling + +1. **Return Value Handling** +Unlike the synchronous throttler which returns void, the async version allows you to capture and use the return value from your throttled function. This is particularly useful when you need to work with the results of API calls or other async operations. The `maybeExecute` method returns a Promise that resolves with the function's return value, allowing you to await the result and handle it appropriately. + +2. **Enhanced Callback System** +The async throttler provides a more sophisticated callback system compared to the synchronous version's single `onExecute` callback. This system includes: +- `onSuccess`: Called when the async function completes successfully, providing both the result and the throttler instance +- `onError`: Called when the async function throws an error, providing both the error and the throttler instance +- `onSettled`: Called after every execution attempt, regardless of success or failure + +3. **Execution Tracking** +The async throttler provides comprehensive execution tracking through several methods: +- `getSuccessCount()`: Number of successful executions +- `getErrorCount()`: Number of failed executions +- `getSettledCount()`: Total number of settled executions (success + error) + +4. **Sequential Execution** +The async throttler ensures that subsequent executions wait for the previous call to complete before starting. This prevents out-of-order execution and guarantees that each call processes the most up-to-date data. This is particularly important when dealing with operations that depend on the results of previous calls or when maintaining data consistency is critical. + +For example, if you're updating a user's profile and then immediately fetching their updated data, the async throttler will ensure the fetch operation waits for the update to complete, preventing race conditions where you might get stale data. + +#### Basic Usage Example -const throttledFetch = asyncThrottle( - async (id: string) => { - const response = await fetch(`/api/data/${id}`) - return response.json() +Here's a basic example showing how to use the async throttler for a search operation: + +```ts +const throttledSearch = asyncThrottle( + async (searchTerm: string) => { + const results = await fetchSearchResults(searchTerm) + return results }, { - wait: 1000, - onError: (error) => { - console.error('API call failed:', error) + wait: 500, + onSuccess: (results, throttler) => { + console.log('Search succeeded:', results) + }, + onError: (error, throttler) => { + console.error('Search failed:', error) } } ) -// Will only make one API call per second -await throttledFetch('123') +// Usage +const results = await throttledSearch('query') ``` -The async version provides Promise-based execution tracking, error handling through the `onError` callback, proper cleanup of pending async operations, and an awaitable `maybeExecute` method. +#### Advanced Patterns + +The async throttler can be combined with various patterns to solve complex problems: + +1. **State Management Integration** +When using the async throttler with state management systems (like React's useState or Solid's createSignal), you can create powerful patterns for handling loading states, error states, and data updates. The throttler's callbacks provide perfect hooks for updating UI state based on the success or failure of operations. + +2. **Race Condition Prevention** +The throttling pattern naturally prevents race conditions in many scenarios. When multiple parts of your application try to update the same resource simultaneously, the throttler ensures that updates occur at a controlled rate, while still providing results to all callers. + +3. **Error Recovery** +The async throttler's error handling capabilities make it ideal for implementing retry logic and error recovery patterns. You can use the `onError` callback to implement custom error handling strategies, such as exponential backoff or fallback mechanisms. ### Framework Adapters @@ -217,7 +255,7 @@ const handleUpdate = useThrottledCallback( // State-based hook for reactive state management const [instantState, setInstantState] = useState(0) -const [throttledState, setThrottledState] = useThrottledValue( +const [throttledValue] = useThrottledValue( instantState, // Value to throttle { wait: 200 } ) diff --git a/docs/overview.md b/docs/overview.md index 0b192cd6b..69f52688f 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -28,15 +28,20 @@ Many of the ideas (and code) for TanStack Pacer are not new. In fact, many of th - **Queuing** - Queue functions to be executed in a specific order - Choose from FIFO, LIFO, and Priority queue implementations - - Control processing with configurable wait times or concurrency limits + - Control processing speed with configurable wait times or concurrency limits - Manage queue execution with start/stop capabilities - - Synchronous or Asynchronous Queue utilities with promise support and success, settled, and error, handling + - Expire items from the queue after a configurable duration +- **Async or Sync Variations** + - Choose between synchronous and asynchronous versions of each utility + - Enforce single-flight execution of functions if needed in the async variations of the utilities + - Optional error, success, and settled handling for async variations - **Comparison Utilities** - Perform deep equality checks between values - Create custom comparison logic for specific needs - **Convenient Hooks** - - Reduce boilerplate code with pre-built hooks like `useDebouncedCallback`, `useThrottledValue`, and `useQueuerState`, and more. + - 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. + - Works with each framework's default state management solutions, or with whatever custom state management library that you prefer. - **Type Safety** - Full type safety with TypeScript that makes sure that your functions will always be called with the correct arguments - Generics for flexible and reusable utilities diff --git a/docs/reference/classes/asyncdebouncer.md b/docs/reference/classes/asyncdebouncer.md index bbec0fc40..f94b7331b 100644 --- a/docs/reference/classes/asyncdebouncer.md +++ b/docs/reference/classes/asyncdebouncer.md @@ -5,9 +5,9 @@ title: AsyncDebouncer -# Class: AsyncDebouncer\ +# Class: AsyncDebouncer\ -Defined in: [async-debouncer.ts:71](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L71) +Defined in: [async-debouncer.ts:73](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L73) A class that creates an async debounced function. @@ -35,17 +35,15 @@ inputElement.addEventListener('input', () => { • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Constructors ### new AsyncDebouncer() ```ts -new AsyncDebouncer(fn, initialOptions): AsyncDebouncer +new AsyncDebouncer(fn, initialOptions): AsyncDebouncer ``` -Defined in: [async-debouncer.ts:83](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L83) +Defined in: [async-debouncer.ts:86](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L86) #### Parameters @@ -55,11 +53,11 @@ Defined in: [async-debouncer.ts:83](https://github.com/TanStack/pacer/blob/main/ ##### initialOptions -[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`, `TArgs`\> +[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`\> #### Returns -[`AsyncDebouncer`](asyncdebouncer.md)\<`TFn`, `TArgs`\> +[`AsyncDebouncer`](asyncdebouncer.md)\<`TFn`\> ## Methods @@ -69,9 +67,9 @@ Defined in: [async-debouncer.ts:83](https://github.com/TanStack/pacer/blob/main/ cancel(): void ``` -Defined in: [async-debouncer.ts:172](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L172) +Defined in: [async-debouncer.ts:195](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L195) -Cancels any pending execution +Cancels any pending execution or aborts any execution in progress #### Returns @@ -79,15 +77,15 @@ Cancels any pending execution *** -### getExecutionCount() +### getErrorCount() ```ts -getExecutionCount(): number +getErrorCount(): number ``` -Defined in: [async-debouncer.ts:188](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L188) +Defined in: [async-debouncer.ts:224](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L224) -Returns the number of times the function has been executed +Returns the number of times the function has errored #### Returns @@ -95,15 +93,31 @@ Returns the number of times the function has been executed *** +### getIsExecuting() + +```ts +getIsExecuting(): boolean +``` + +Defined in: [async-debouncer.ts:238](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L238) + +Returns `true` if there is currently an execution in progress + +#### Returns + +`boolean` + +*** + ### getIsPending() ```ts getIsPending(): boolean ``` -Defined in: [async-debouncer.ts:195](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L195) +Defined in: [async-debouncer.ts:231](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L231) -Returns `true` if there is a pending execution +Returns `true` if there is a pending execution queued up for trailing execution #### Returns @@ -111,29 +125,77 @@ Returns `true` if there is a pending execution *** +### getLastResult() + +```ts +getLastResult(): undefined | ReturnType +``` + +Defined in: [async-debouncer.ts:203](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L203) + +Returns the last result of the debounced function + +#### Returns + +`undefined` \| `ReturnType`\<`TFn`\> + +*** + ### getOptions() ```ts -getOptions(): Required> +getOptions(): Required> ``` -Defined in: [async-debouncer.ts:110](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L110) +Defined in: [async-debouncer.ts:112](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L112) Returns the current debouncer options #### Returns -`Required`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`, `TArgs`\>\> +`Required`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`\>\> + +*** + +### getSettleCount() + +```ts +getSettleCount(): number +``` + +Defined in: [async-debouncer.ts:217](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L217) + +Returns the number of times the function has settled (completed or errored) + +#### Returns + +`number` + +*** + +### getSuccessCount() + +```ts +getSuccessCount(): number +``` + +Defined in: [async-debouncer.ts:210](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L210) + +Returns the number of times the function has been executed successfully + +#### Returns + +`number` *** ### maybeExecute() ```ts -maybeExecute(...args): Promise +maybeExecute(...args): Promise> ``` -Defined in: [async-debouncer.ts:118](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L118) +Defined in: [async-debouncer.ts:120](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L120) Attempts to execute the debounced function If a call is already in progress, it will be queued @@ -142,21 +204,21 @@ If a call is already in progress, it will be queued ##### args -...`TArgs` +...`Parameters`\<`TFn`\> #### Returns -`Promise`\<`void`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> *** ### setOptions() ```ts -setOptions(newOptions): Required> +setOptions(newOptions): void ``` -Defined in: [async-debouncer.ts:97](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L97) +Defined in: [async-debouncer.ts:100](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L100) Updates the debouncer options Returns the new options state @@ -165,8 +227,8 @@ Returns the new options state ##### newOptions -`Partial`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`, `TArgs`\>\> +`Partial`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`\>\> #### Returns -`Required`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`, `TArgs`\>\> +`void` diff --git a/docs/reference/classes/asyncqueuer.md b/docs/reference/classes/asyncqueuer.md index a441d35af..2cc91d632 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:105](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L105) +Defined in: [async-queuer.ts:123](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L123) A flexible asynchronous queue that processes tasks with configurable concurrency control. @@ -18,6 +18,7 @@ Features: - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior - Pause/resume task processing - Task cancellation +- Item expiration to clear stale items from the queue Tasks are processed concurrently up to the configured concurrency limit. When a task completes, the next pending task is processed if below the concurrency limit. @@ -50,7 +51,7 @@ asyncQueuer.onSuccess((result) => { new AsyncQueuer(initialOptions): AsyncQueuer ``` -Defined in: [async-queuer.ts:117](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L117) +Defined in: [async-queuer.ts:137](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L137) #### Parameters @@ -73,7 +74,7 @@ addItem( runOnUpdate): Promise ``` -Defined in: [async-queuer.ts:254](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L254) +Defined in: [async-queuer.ts:324](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L324) Adds a task to the queuer @@ -103,7 +104,7 @@ Adds a task to the queuer clear(): void ``` -Defined in: [async-queuer.ts:234](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L234) +Defined in: [async-queuer.ts:304](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L304) Removes all items from the queuer @@ -119,7 +120,7 @@ Removes all items from the queuer getActiveItems(): () => Promise[] ``` -Defined in: [async-queuer.ts:386](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L386) +Defined in: [async-queuer.ts:462](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L462) Returns the active items @@ -135,7 +136,7 @@ Returns the active items getAllItems(): () => Promise[] ``` -Defined in: [async-queuer.ts:379](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L379) +Defined in: [async-queuer.ts:455](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L455) Returns a copy of all items in the queuer @@ -151,7 +152,7 @@ Returns a copy of all items in the queuer getExecutionCount(): number ``` -Defined in: [async-queuer.ts:400](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L400) +Defined in: [async-queuer.ts:476](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L476) Returns the number of items that have been removed from the queuer @@ -161,13 +162,29 @@ Returns the number of items that have been removed from the queuer *** +### getExpirationCount() + +```ts +getExpirationCount(): number +``` + +Defined in: [async-queuer.ts:538](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L538) + +Returns the number of items that have expired from the queuer + +#### Returns + +`number` + +*** + ### getIsEmpty() ```ts getIsEmpty(): boolean ``` -Defined in: [async-queuer.ts:358](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L358) +Defined in: [async-queuer.ts:434](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L434) Returns true if the queuer is empty @@ -183,7 +200,7 @@ Returns true if the queuer is empty getIsFull(): boolean ``` -Defined in: [async-queuer.ts:365](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L365) +Defined in: [async-queuer.ts:441](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L441) Returns true if the queuer is full @@ -199,7 +216,7 @@ Returns true if the queuer is full getIsIdle(): boolean ``` -Defined in: [async-queuer.ts:421](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L421) +Defined in: [async-queuer.ts:497](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L497) Returns true if the queuer is running but has no items to process @@ -215,7 +232,7 @@ Returns true if the queuer is running but has no items to process getIsRunning(): boolean ``` -Defined in: [async-queuer.ts:414](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L414) +Defined in: [async-queuer.ts:490](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L490) Returns true if the queuer is running @@ -231,7 +248,7 @@ Returns true if the queuer is running getNextItem(position): undefined | () => Promise ``` -Defined in: [async-queuer.ts:324](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L324) +Defined in: [async-queuer.ts:398](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L398) Removes and returns an item from the queuer @@ -253,7 +270,7 @@ Removes and returns an item from the queuer getOptions(): Required> ``` -Defined in: [async-queuer.ts:142](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L142) +Defined in: [async-queuer.ts:159](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L159) Returns the current queuer options @@ -269,7 +286,7 @@ Returns the current queuer options getPeek(position): undefined | () => Promise ``` -Defined in: [async-queuer.ts:346](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L346) +Defined in: [async-queuer.ts:422](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L422) Returns an item without removing it @@ -291,7 +308,7 @@ Returns an item without removing it getPendingItems(): () => Promise[] ``` -Defined in: [async-queuer.ts:393](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L393) +Defined in: [async-queuer.ts:469](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L469) Returns the pending items @@ -307,7 +324,7 @@ Returns the pending items getRejectionCount(): number ``` -Defined in: [async-queuer.ts:407](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L407) +Defined in: [async-queuer.ts:483](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L483) Returns the number of items that have been rejected from the queuer @@ -323,7 +340,7 @@ Returns the number of items that have been rejected from the queuer getSize(): number ``` -Defined in: [async-queuer.ts:372](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L372) +Defined in: [async-queuer.ts:448](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L448) Returns the current size of the queuer @@ -339,7 +356,7 @@ Returns the current size of the queuer onError(cb): () => void ``` -Defined in: [async-queuer.ts:440](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L440) +Defined in: [async-queuer.ts:516](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L516) Adds a callback to be called when a task errors @@ -365,7 +382,7 @@ Adds a callback to be called when a task errors onSettled(cb): () => void ``` -Defined in: [async-queuer.ts:450](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L450) +Defined in: [async-queuer.ts:526](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L526) Adds a callback to be called when a task is settled @@ -391,7 +408,7 @@ Adds a callback to be called when a task is settled onSuccess(cb): () => void ``` -Defined in: [async-queuer.ts:428](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L428) +Defined in: [async-queuer.ts:504](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L504) Adds a callback to be called when a task succeeds @@ -417,7 +434,7 @@ Adds a callback to be called when a task succeeds reset(withInitialItems?): void ``` -Defined in: [async-queuer.ts:242](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L242) +Defined in: [async-queuer.ts:312](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L312) Resets the queuer to its initial state @@ -436,10 +453,10 @@ Resets the queuer to its initial state ### setOptions() ```ts -setOptions(newOptions): AsyncQueuerOptions +setOptions(newOptions): void ``` -Defined in: [async-queuer.ts:132](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L132) +Defined in: [async-queuer.ts:152](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L152) Updates the queuer options Returns the new options state @@ -452,7 +469,7 @@ Returns the new options state #### Returns -[`AsyncQueuerOptions`](../interfaces/asyncqueueroptions.md)\<`TValue`\> +`void` *** @@ -462,7 +479,7 @@ Returns the new options state start(): Promise ``` -Defined in: [async-queuer.ts:202](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L202) +Defined in: [async-queuer.ts:272](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L272) Starts the queuer and processes items @@ -478,7 +495,7 @@ Starts the queuer and processes items stop(): void ``` -Defined in: [async-queuer.ts:225](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L225) +Defined in: [async-queuer.ts:295](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L295) Stops the queuer from processing items diff --git a/docs/reference/classes/asyncratelimiter.md b/docs/reference/classes/asyncratelimiter.md index 4f9b6be77..4c551ff76 100644 --- a/docs/reference/classes/asyncratelimiter.md +++ b/docs/reference/classes/asyncratelimiter.md @@ -5,9 +5,9 @@ title: AsyncRateLimiter -# Class: AsyncRateLimiter\ +# Class: AsyncRateLimiter\ -Defined in: [async-rate-limiter.ts:71](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L71) +Defined in: [async-rate-limiter.ts:76](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L76) A class that creates an async rate-limited function. @@ -38,17 +38,15 @@ await rateLimiter.maybeExecute('123'); • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Constructors ### new AsyncRateLimiter() ```ts -new AsyncRateLimiter(fn, initialOptions): AsyncRateLimiter +new AsyncRateLimiter(fn, initialOptions): AsyncRateLimiter ``` -Defined in: [async-rate-limiter.ts:80](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L80) +Defined in: [async-rate-limiter.ts:85](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L85) #### Parameters @@ -58,23 +56,23 @@ Defined in: [async-rate-limiter.ts:80](https://github.com/TanStack/pacer/blob/ma ##### initialOptions -[`AsyncRateLimiterOptions`](../interfaces/asyncratelimiteroptions.md)\<`TFn`, `TArgs`\> +[`AsyncRateLimiterOptions`](../interfaces/asyncratelimiteroptions.md)\<`TFn`\> #### Returns -[`AsyncRateLimiter`](asyncratelimiter.md)\<`TFn`, `TArgs`\> +[`AsyncRateLimiter`](asyncratelimiter.md)\<`TFn`\> ## Methods -### getExecutionCount() +### getErrorCount() ```ts -getExecutionCount(): number +getErrorCount(): number ``` -Defined in: [async-rate-limiter.ts:179](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L179) +Defined in: [async-rate-limiter.ts:209](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L209) -Returns the number of times the function has been executed +Returns the number of times the function has errored #### Returns @@ -88,7 +86,7 @@ Returns the number of times the function has been executed getMsUntilNextWindow(): number ``` -Defined in: [async-rate-limiter.ts:201](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L201) +Defined in: [async-rate-limiter.ts:188](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L188) Returns the number of milliseconds until the next execution will be possible @@ -101,16 +99,16 @@ Returns the number of milliseconds until the next execution will be possible ### getOptions() ```ts -getOptions(): Required> +getOptions(): Required> ``` -Defined in: [async-rate-limiter.ts:107](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L107) +Defined in: [async-rate-limiter.ts:106](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L106) Returns the current rate limiter options #### Returns -`Required`\<[`AsyncRateLimiterOptions`](../interfaces/asyncratelimiteroptions.md)\<`TFn`, `TArgs`\>\> +`Required`\<[`AsyncRateLimiterOptions`](../interfaces/asyncratelimiteroptions.md)\<`TFn`\>\> *** @@ -120,7 +118,7 @@ Returns the current rate limiter options getRejectionCount(): number ``` -Defined in: [async-rate-limiter.ts:186](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L186) +Defined in: [async-rate-limiter.ts:216](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L216) Returns the number of times the function has been rejected @@ -136,7 +134,7 @@ Returns the number of times the function has been rejected getRemainingInWindow(): number ``` -Defined in: [async-rate-limiter.ts:193](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L193) +Defined in: [async-rate-limiter.ts:180](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L180) Returns the number of remaining executions allowed in the current window @@ -146,13 +144,45 @@ Returns the number of remaining executions allowed in the current window *** +### getSettleCount() + +```ts +getSettleCount(): number +``` + +Defined in: [async-rate-limiter.ts:202](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L202) + +Returns the number of times the function has been settled + +#### Returns + +`number` + +*** + +### getSuccessCount() + +```ts +getSuccessCount(): number +``` + +Defined in: [async-rate-limiter.ts:195](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L195) + +Returns the number of times the function has been executed + +#### Returns + +`number` + +*** + ### maybeExecute() ```ts -maybeExecute(...args): Promise +maybeExecute(...args): Promise> ``` -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:126](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L126) 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. @@ -162,11 +192,11 @@ If execution is allowed, waits for any previous execution to complete before pro ##### args -...`TArgs` +...`Parameters`\<`TFn`\> #### Returns -`Promise`\<`boolean`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> #### Example @@ -188,7 +218,7 @@ await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected reset(): void ``` -Defined in: [async-rate-limiter.ts:208](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L208) +Defined in: [async-rate-limiter.ts:223](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L223) Resets the rate limiter state @@ -201,10 +231,10 @@ Resets the rate limiter state ### setOptions() ```ts -setOptions(newOptions): AsyncRateLimiterOptions +setOptions(newOptions): void ``` -Defined in: [async-rate-limiter.ts:94](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L94) +Defined in: [async-rate-limiter.ts:99](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L99) Updates the rate limiter options Returns the new options state @@ -213,8 +243,8 @@ Returns the new options state ##### newOptions -`Partial`\<[`AsyncRateLimiterOptions`](../interfaces/asyncratelimiteroptions.md)\<`TFn`, `TArgs`\>\> +`Partial`\<[`AsyncRateLimiterOptions`](../interfaces/asyncratelimiteroptions.md)\<`TFn`\>\> #### Returns -[`AsyncRateLimiterOptions`](../interfaces/asyncratelimiteroptions.md)\<`TFn`, `TArgs`\> +`void` diff --git a/docs/reference/classes/asyncthrottler.md b/docs/reference/classes/asyncthrottler.md index c46a1586d..b3edb067b 100644 --- a/docs/reference/classes/asyncthrottler.md +++ b/docs/reference/classes/asyncthrottler.md @@ -5,9 +5,9 @@ title: AsyncThrottler -# Class: AsyncThrottler\ +# Class: AsyncThrottler\ -Defined in: [async-throttler.ts:59](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L59) +Defined in: [async-throttler.ts:76](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L76) A class that creates an async throttled function. @@ -35,17 +35,15 @@ inputElement.addEventListener('input', () => { • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Constructors ### new AsyncThrottler() ```ts -new AsyncThrottler(fn, initialOptions): AsyncThrottler +new AsyncThrottler(fn, initialOptions): AsyncThrottler ``` -Defined in: [async-throttler.ts:72](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L72) +Defined in: [async-throttler.ts:89](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L89) #### Parameters @@ -55,11 +53,11 @@ Defined in: [async-throttler.ts:72](https://github.com/TanStack/pacer/blob/main/ ##### initialOptions -[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`, `TArgs`\> +[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`\> #### Returns -[`AsyncThrottler`](asyncthrottler.md)\<`TFn`, `TArgs`\> +[`AsyncThrottler`](asyncthrottler.md)\<`TFn`\> ## Methods @@ -69,9 +67,9 @@ Defined in: [async-throttler.ts:72](https://github.com/TanStack/pacer/blob/main/ cancel(): void ``` -Defined in: [async-throttler.ts:169](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L169) +Defined in: [async-throttler.ts:187](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L187) -Cancels any pending execution +Cancels any pending execution or aborts any execution in progress #### Returns @@ -79,15 +77,15 @@ Cancels any pending execution *** -### getExecutionCount() +### getErrorCount() ```ts -getExecutionCount(): number +getErrorCount(): number ``` -Defined in: [async-throttler.ts:181](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L181) +Defined in: [async-throttler.ts:237](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L237) -Returns the number of times the function has been executed +Returns the number of times the function has errored #### Returns @@ -95,13 +93,29 @@ Returns the number of times the function has been executed *** +### getIsExecuting() + +```ts +getIsExecuting(): boolean +``` + +Defined in: [async-throttler.ts:251](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L251) + +Returns the current executing state + +#### Returns + +`boolean` + +*** + ### getIsPending() ```ts getIsPending(): boolean ``` -Defined in: [async-throttler.ts:202](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L202) +Defined in: [async-throttler.ts:244](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L244) Returns the current pending state @@ -117,7 +131,7 @@ Returns the current pending state getLastExecutionTime(): number ``` -Defined in: [async-throttler.ts:188](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L188) +Defined in: [async-throttler.ts:202](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L202) Returns the last execution time @@ -127,13 +141,29 @@ Returns the last execution time *** +### getLastResult() + +```ts +getLastResult(): undefined | ReturnType +``` + +Defined in: [async-throttler.ts:216](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L216) + +Returns the last result of the debounced function + +#### Returns + +`undefined` \| `ReturnType`\<`TFn`\> + +*** + ### getNextExecutionTime() ```ts getNextExecutionTime(): number ``` -Defined in: [async-throttler.ts:195](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L195) +Defined in: [async-throttler.ts:209](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L209) Returns the next execution time @@ -146,26 +176,58 @@ Returns the next execution time ### getOptions() ```ts -getOptions(): Required> +getOptions(): Required> ``` -Defined in: [async-throttler.ts:99](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L99) +Defined in: [async-throttler.ts:115](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L115) Returns the current options #### Returns -`Required`\<[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`, `TArgs`\>\> +`Required`\<[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`\>\> + +*** + +### getSettleCount() + +```ts +getSettleCount(): number +``` + +Defined in: [async-throttler.ts:230](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L230) + +Returns the number of times the function has settled (completed or errored) + +#### Returns + +`number` + +*** + +### getSuccessCount() + +```ts +getSuccessCount(): number +``` + +Defined in: [async-throttler.ts:223](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L223) + +Returns the number of times the function has been executed successfully + +#### Returns + +`number` *** ### maybeExecute() ```ts -maybeExecute(...args): Promise +maybeExecute(...args): Promise> ``` -Defined in: [async-throttler.ts:107](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L107) +Defined in: [async-throttler.ts:123](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L123) Attempts to execute the throttled function If a call is already in progress, it may be blocked or queued depending on the `wait` option @@ -174,21 +236,21 @@ If a call is already in progress, it may be blocked or queued depending on the ` ##### args -...`TArgs` +...`Parameters`\<`TFn`\> #### Returns -`Promise`\<`void`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> *** ### setOptions() ```ts -setOptions(newOptions): Required> +setOptions(newOptions): void ``` -Defined in: [async-throttler.ts:86](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L86) +Defined in: [async-throttler.ts:103](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L103) Updates the throttler options Returns the new options state @@ -197,8 +259,8 @@ Returns the new options state ##### newOptions -`Partial`\<[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`, `TArgs`\>\> +`Partial`\<[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`\>\> #### Returns -`Required`\<[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`, `TArgs`\>\> +`void` diff --git a/docs/reference/classes/debouncer.md b/docs/reference/classes/debouncer.md index 908f03e2f..8cc48a0d3 100644 --- a/docs/reference/classes/debouncer.md +++ b/docs/reference/classes/debouncer.md @@ -5,9 +5,9 @@ title: Debouncer -# Class: Debouncer\ +# Class: Debouncer\ -Defined in: [debouncer.ts:67](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L67) +Defined in: [debouncer.ts:65](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L65) A class that creates a debounced function. @@ -36,17 +36,15 @@ inputElement.addEventListener('input', () => { • **TFn** *extends* [`AnyFunction`](../type-aliases/anyfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Constructors ### new Debouncer() ```ts -new Debouncer(fn, initialOptions): Debouncer +new Debouncer(fn, initialOptions): Debouncer ``` -Defined in: [debouncer.ts:74](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L74) +Defined in: [debouncer.ts:72](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L72) #### Parameters @@ -56,11 +54,11 @@ Defined in: [debouncer.ts:74](https://github.com/TanStack/pacer/blob/main/packag ##### initialOptions -[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`, `TArgs`\> +[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`\> #### Returns -[`Debouncer`](debouncer.md)\<`TFn`, `TArgs`\> +[`Debouncer`](debouncer.md)\<`TFn`\> ## Methods @@ -70,7 +68,7 @@ Defined in: [debouncer.ts:74](https://github.com/TanStack/pacer/blob/main/packag cancel(): void ``` -Defined in: [debouncer.ts:151](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L151) +Defined in: [debouncer.ts:144](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L144) Cancels any pending execution @@ -86,7 +84,7 @@ Cancels any pending execution getExecutionCount(): number ``` -Defined in: [debouncer.ts:162](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L162) +Defined in: [debouncer.ts:155](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L155) Returns the number of times the function has been executed @@ -102,7 +100,7 @@ Returns the number of times the function has been executed getIsPending(): boolean ``` -Defined in: [debouncer.ts:169](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L169) +Defined in: [debouncer.ts:162](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L162) Returns `true` if debouncing @@ -115,16 +113,16 @@ Returns `true` if debouncing ### getOptions() ```ts -getOptions(): Required> +getOptions(): Required> ``` -Defined in: [debouncer.ts:107](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L107) +Defined in: [debouncer.ts:98](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L98) Returns the current debouncer options #### Returns -`Required`\<[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`, `TArgs`\>\> +`Required`\<[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`\>\> *** @@ -134,7 +132,7 @@ Returns the current debouncer options maybeExecute(...args): void ``` -Defined in: [debouncer.ts:115](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L115) +Defined in: [debouncer.ts:106](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L106) Attempts to execute the debounced function If a call is already in progress, it will be queued @@ -143,7 +141,7 @@ If a call is already in progress, it will be queued ##### args -...`TArgs` +...`Parameters`\<`TFn`\> #### Returns @@ -154,10 +152,10 @@ If a call is already in progress, it will be queued ### setOptions() ```ts -setOptions(newOptions): Required> +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:86](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L86) Updates the debouncer options Returns the new options state @@ -166,8 +164,8 @@ Returns the new options state ##### newOptions -`Partial`\<[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`, `TArgs`\>\> +`Partial`\<[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`\>\> #### Returns -`Required`\<[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`, `TArgs`\>\> +`void` diff --git a/docs/reference/classes/queuer.md b/docs/reference/classes/queuer.md index 4373580cc..ca0c02307 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:122](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L122) +Defined in: [queuer.ts:144](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L144) A flexible queue data structure that defaults to FIFO (First In First Out) behavior with optional position overrides for stack-like or double-ended operations. @@ -37,6 +37,11 @@ Processing behavior: - wait: configurable delay between processing items - onItemsChange/onGetNextItem: callbacks for monitoring queuer state +Supports item expiration to clear stale items from the queuer +- expirationDuration: maximum time in milliseconds that an item can stay in the queue +- getIsExpired: function to override default expiration behavior +- onExpire: callback for when an item expires + ## Example ```ts @@ -70,7 +75,7 @@ priorityQueue.addItem(2); // [3, 2, 1] new Queuer(initialOptions): Queuer ``` -Defined in: [queuer.ts:131](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L131) +Defined in: [queuer.ts:155](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L155) #### Parameters @@ -93,7 +98,7 @@ addItem( runOnUpdate): boolean ``` -Defined in: [queuer.ts:231](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L231) +Defined in: [queuer.ts:306](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L306) Adds an item to the queuer and starts processing if not already running @@ -125,7 +130,7 @@ true if item was added, false if queuer is full clear(): void ``` -Defined in: [queuer.ts:210](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L210) +Defined in: [queuer.ts:285](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L285) Removes all items from the queuer @@ -141,7 +146,7 @@ Removes all items from the queuer getAllItems(): TValue[] ``` -Defined in: [queuer.ts:347](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L347) +Defined in: [queuer.ts:428](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L428) Returns a copy of all items in the queuer @@ -157,7 +162,7 @@ Returns a copy of all items in the queuer getExecutionCount(): number ``` -Defined in: [queuer.ts:354](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L354) +Defined in: [queuer.ts:435](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L435) Returns the number of items that have been removed from the queuer @@ -167,13 +172,29 @@ Returns the number of items that have been removed from the queuer *** +### getExpirationCount() + +```ts +getExpirationCount(): number +``` + +Defined in: [queuer.ts:449](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L449) + +Returns the number of items that have expired from the queuer + +#### Returns + +`number` + +*** + ### getIsEmpty() ```ts getIsEmpty(): boolean ``` -Defined in: [queuer.ts:326](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L326) +Defined in: [queuer.ts:407](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L407) Returns true if the queuer is empty @@ -189,7 +210,7 @@ Returns true if the queuer is empty getIsFull(): boolean ``` -Defined in: [queuer.ts:333](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L333) +Defined in: [queuer.ts:414](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L414) Returns true if the queuer is full @@ -205,7 +226,7 @@ Returns true if the queuer is full getIsIdle(): boolean ``` -Defined in: [queuer.ts:375](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L375) +Defined in: [queuer.ts:463](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L463) Returns true if the queuer is running but has no items to process @@ -221,7 +242,7 @@ Returns true if the queuer is running but has no items to process getIsRunning(): boolean ``` -Defined in: [queuer.ts:368](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L368) +Defined in: [queuer.ts:456](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L456) Returns true if the queuer is running @@ -237,7 +258,7 @@ Returns true if the queuer is running getNextItem(position): undefined | TValue ``` -Defined in: [queuer.ts:284](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L284) +Defined in: [queuer.ts:363](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L363) Removes and returns an item from the queuer using shift (default) or pop @@ -268,7 +289,7 @@ queuer.getNextItem('back') getOptions(): Required> ``` -Defined in: [queuer.ts:156](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L156) +Defined in: [queuer.ts:177](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L177) Returns the current queuer options @@ -284,7 +305,7 @@ Returns the current queuer options getPeek(position): undefined | TValue ``` -Defined in: [queuer.ts:314](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L314) +Defined in: [queuer.ts:395](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L395) Returns an item without removing it @@ -315,7 +336,7 @@ queuer.getPeek('back') getRejectionCount(): number ``` -Defined in: [queuer.ts:361](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L361) +Defined in: [queuer.ts:442](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L442) Returns the number of items that have been rejected from the queuer @@ -331,7 +352,7 @@ Returns the number of items that have been rejected from the queuer getSize(): number ``` -Defined in: [queuer.ts:340](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L340) +Defined in: [queuer.ts:421](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L421) Returns the current size of the queuer @@ -347,7 +368,7 @@ Returns the current size of the queuer reset(withInitialItems?): void ``` -Defined in: [queuer.ts:218](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L218) +Defined in: [queuer.ts:293](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L293) Resets the queuer to its initial state @@ -366,10 +387,10 @@ Resets the queuer to its initial state ### setOptions() ```ts -setOptions(newOptions): QueuerOptions +setOptions(newOptions): void ``` -Defined in: [queuer.ts:146](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L146) +Defined in: [queuer.ts:170](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L170) Updates the queuer options Returns the new options state @@ -382,7 +403,7 @@ Returns the new options state #### Returns -[`QueuerOptions`](../interfaces/queueroptions.md)\<`TValue`\> +`void` *** @@ -392,7 +413,7 @@ Returns the new options state start(): void ``` -Defined in: [queuer.ts:198](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L198) +Defined in: [queuer.ts:273](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L273) Starts the queuer and processes items @@ -408,7 +429,7 @@ Starts the queuer and processes items stop(): void ``` -Defined in: [queuer.ts:189](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L189) +Defined in: [queuer.ts:264](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L264) Stops the queuer from processing items diff --git a/docs/reference/classes/ratelimiter.md b/docs/reference/classes/ratelimiter.md index 51c4af531..d690bae43 100644 --- a/docs/reference/classes/ratelimiter.md +++ b/docs/reference/classes/ratelimiter.md @@ -5,9 +5,9 @@ title: RateLimiter -# Class: RateLimiter\ +# Class: RateLimiter\ -Defined in: [rate-limiter.ts:66](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L66) +Defined in: [rate-limiter.ts:63](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L63) A class that creates a rate-limited function. @@ -38,17 +38,15 @@ rateLimiter.maybeExecute('123'); • **TFn** *extends* [`AnyFunction`](../type-aliases/anyfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Constructors ### new RateLimiter() ```ts -new RateLimiter(fn, initialOptions): RateLimiter +new RateLimiter(fn, initialOptions): RateLimiter ``` -Defined in: [rate-limiter.ts:75](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L75) +Defined in: [rate-limiter.ts:69](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L69) #### Parameters @@ -58,11 +56,11 @@ Defined in: [rate-limiter.ts:75](https://github.com/TanStack/pacer/blob/main/pac ##### initialOptions -[`RateLimiterOptions`](../interfaces/ratelimiteroptions.md)\<`TFn`, `TArgs`\> +[`RateLimiterOptions`](../interfaces/ratelimiteroptions.md)\<`TFn`\> #### Returns -[`RateLimiter`](ratelimiter.md)\<`TFn`, `TArgs`\> +[`RateLimiter`](ratelimiter.md)\<`TFn`\> ## Methods @@ -72,7 +70,7 @@ Defined in: [rate-limiter.ts:75](https://github.com/TanStack/pacer/blob/main/pac getExecutionCount(): number ``` -Defined in: [rate-limiter.ts:161](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L161) +Defined in: [rate-limiter.ts:149](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L149) Returns the number of times the function has been executed @@ -88,7 +86,7 @@ Returns the number of times the function has been executed getMsUntilNextWindow(): number ``` -Defined in: [rate-limiter.ts:183](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L183) +Defined in: [rate-limiter.ts:171](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L171) Returns the number of milliseconds until the next execution will be possible @@ -101,16 +99,16 @@ Returns the number of milliseconds until the next execution will be possible ### getOptions() ```ts -getOptions(): Required> +getOptions(): Required> ``` -Defined in: [rate-limiter.ts:102](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L102) +Defined in: [rate-limiter.ts:90](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L90) Returns the current rate limiter options #### Returns -`Required`\<[`RateLimiterOptions`](../interfaces/ratelimiteroptions.md)\<`TFn`, `TArgs`\>\> +`Required`\<[`RateLimiterOptions`](../interfaces/ratelimiteroptions.md)\<`TFn`\>\> *** @@ -120,7 +118,7 @@ Returns the current rate limiter options getRejectionCount(): number ``` -Defined in: [rate-limiter.ts:168](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L168) +Defined in: [rate-limiter.ts:156](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L156) Returns the number of times the function has been rejected @@ -136,7 +134,7 @@ Returns the number of times the function has been rejected getRemainingInWindow(): number ``` -Defined in: [rate-limiter.ts:175](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L175) +Defined in: [rate-limiter.ts:163](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L163) Returns the number of remaining executions allowed in the current window @@ -152,7 +150,7 @@ Returns the number of remaining executions allowed in the current window maybeExecute(...args): boolean ``` -Defined in: [rate-limiter.ts:121](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L121) +Defined in: [rate-limiter.ts:109](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L109) 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. @@ -161,7 +159,7 @@ Will reject execution if the number of calls in the current window exceeds the l ##### args -...`TArgs` +...`Parameters`\<`TFn`\> #### Returns @@ -187,7 +185,7 @@ rateLimiter.maybeExecute('arg1', 'arg2'); // false reset(): void ``` -Defined in: [rate-limiter.ts:191](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L191) +Defined in: [rate-limiter.ts:179](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L179) Resets the rate limiter state @@ -200,10 +198,10 @@ Resets the rate limiter state ### setOptions() ```ts -setOptions(newOptions): RateLimiterOptions +setOptions(newOptions): void ``` -Defined in: [rate-limiter.ts:89](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L89) +Defined in: [rate-limiter.ts:83](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L83) Updates the rate limiter options Returns the new options state @@ -212,8 +210,8 @@ Returns the new options state ##### newOptions -`Partial`\<[`RateLimiterOptions`](../interfaces/ratelimiteroptions.md)\<`TFn`, `TArgs`\>\> +`Partial`\<[`RateLimiterOptions`](../interfaces/ratelimiteroptions.md)\<`TFn`\>\> #### Returns -[`RateLimiterOptions`](../interfaces/ratelimiteroptions.md)\<`TFn`, `TArgs`\> +`void` diff --git a/docs/reference/classes/throttler.md b/docs/reference/classes/throttler.md index 64b441ff4..e807f2571 100644 --- a/docs/reference/classes/throttler.md +++ b/docs/reference/classes/throttler.md @@ -5,9 +5,9 @@ title: Throttler -# Class: Throttler\ +# Class: Throttler\ -Defined in: [throttler.ts:70](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L70) +Defined in: [throttler.ts:67](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L67) A class that creates a throttled function. @@ -40,17 +40,15 @@ throttler.maybeExecute('123'); // Throttled • **TFn** *extends* [`AnyFunction`](../type-aliases/anyfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Constructors ### new Throttler() ```ts -new Throttler(fn, initialOptions): Throttler +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:74](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L74) #### Parameters @@ -60,11 +58,11 @@ Defined in: [throttler.ts:78](https://github.com/TanStack/pacer/blob/main/packag ##### initialOptions -[`ThrottlerOptions`](../interfaces/throttleroptions.md)\<`TFn`, `TArgs`\> +[`ThrottlerOptions`](../interfaces/throttleroptions.md)\<`TFn`\> #### Returns -[`Throttler`](throttler.md)\<`TFn`, `TArgs`\> +[`Throttler`](throttler.md)\<`TFn`\> ## Methods @@ -74,7 +72,7 @@ Defined in: [throttler.ts:78](https://github.com/TanStack/pacer/blob/main/packag cancel(): void ``` -Defined in: [throttler.ts:178](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L178) +Defined in: [throttler.ts:171](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L171) Cancels any pending trailing execution and clears internal state. @@ -96,7 +94,7 @@ Has no effect if there is no pending execution. getExecutionCount(): number ``` -Defined in: [throttler.ts:190](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L190) +Defined in: [throttler.ts:196](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L196) Returns the number of times the function has been executed @@ -112,7 +110,7 @@ Returns the number of times the function has been executed getIsPending(): boolean ``` -Defined in: [throttler.ts:197](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L197) +Defined in: [throttler.ts:203](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L203) Returns `true` if there is a pending execution @@ -128,7 +126,7 @@ Returns `true` if there is a pending execution getLastExecutionTime(): number ``` -Defined in: [throttler.ts:204](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L204) +Defined in: [throttler.ts:182](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L182) Returns the last execution time @@ -144,7 +142,7 @@ Returns the last execution time getNextExecutionTime(): number ``` -Defined in: [throttler.ts:211](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L211) +Defined in: [throttler.ts:189](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L189) Returns the next execution time @@ -157,16 +155,16 @@ Returns the next execution time ### getOptions() ```ts -getOptions(): Required> +getOptions(): Required> ``` -Defined in: [throttler.ts:105](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L105) +Defined in: [throttler.ts:100](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L100) Returns the current throttler options #### Returns -`Required`\<[`ThrottlerOptions`](../interfaces/throttleroptions.md)\<`TFn`, `TArgs`\>\> +`Required`\<[`ThrottlerOptions`](../interfaces/throttleroptions.md)\<`TFn`\>\> *** @@ -176,7 +174,7 @@ Returns the current throttler options maybeExecute(...args): void ``` -Defined in: [throttler.ts:131](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L131) +Defined in: [throttler.ts:126](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L126) Attempts to execute the throttled function. The execution behavior depends on the throttler options: @@ -192,7 +190,7 @@ Attempts to execute the throttled function. The execution behavior depends on th ##### args -...`TArgs` +...`Parameters`\<`TFn`\> #### Returns @@ -215,10 +213,10 @@ throttled.maybeExecute('c', 'd'); ### setOptions() ```ts -setOptions(newOptions): Required> +setOptions(newOptions): void ``` -Defined in: [throttler.ts:92](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L92) +Defined in: [throttler.ts:88](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L88) Updates the throttler options Returns the new options state @@ -227,8 +225,8 @@ Returns the new options state ##### newOptions -`Partial`\<[`ThrottlerOptions`](../interfaces/throttleroptions.md)\<`TFn`, `TArgs`\>\> +`Partial`\<[`ThrottlerOptions`](../interfaces/throttleroptions.md)\<`TFn`\>\> #### Returns -`Required`\<[`ThrottlerOptions`](../interfaces/throttleroptions.md)\<`TFn`, `TArgs`\>\> +`void` diff --git a/docs/reference/functions/asyncdebounce.md b/docs/reference/functions/asyncdebounce.md index 50ec16491..9c42ae372 100644 --- a/docs/reference/functions/asyncdebounce.md +++ b/docs/reference/functions/asyncdebounce.md @@ -8,10 +8,10 @@ title: asyncDebounce # Function: asyncDebounce() ```ts -function asyncDebounce(fn, initialOptions): (...args) => Promise +function asyncDebounce(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-debouncer.ts:219](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L219) +Defined in: [async-debouncer.ts:260](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L260) 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. @@ -21,8 +21,6 @@ If called again during the wait period, the timer resets and a new wait period b • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -31,7 +29,7 @@ If called again during the wait period, the timer resets and a new wait period b ### initialOptions -`Omit`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`, `TArgs`\>, `"enabled"`\> +`Omit`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`\>, `"enabled"`\> ## Returns @@ -44,11 +42,11 @@ If a call is already in progress, it will be queued #### args -...`TArgs` +...`Parameters`\<`TFn`\> ### Returns -`Promise`\<`void`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> ## Example diff --git a/docs/reference/functions/asyncqueue.md b/docs/reference/functions/asyncqueue.md index 23734bfba..532cf5728 100644 --- a/docs/reference/functions/asyncqueue.md +++ b/docs/reference/functions/asyncqueue.md @@ -11,7 +11,7 @@ title: asyncQueue function asyncQueue(options): (fn, position, runOnUpdate) => Promise ``` -Defined in: [async-queuer.ts:477](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L477) +Defined in: [async-queuer.ts:560](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L560) Creates a new AsyncQueuer instance with the given options and returns a bound addItem function. The queuer is automatically started and ready to process items. @@ -24,7 +24,7 @@ The queuer is automatically started and ready to process items. ### options -[`AsyncQueuerOptions`](../interfaces/asyncqueueroptions.md)\<`TValue`\> = `{}` +`Omit`\<[`AsyncQueuerOptions`](../interfaces/asyncqueueroptions.md)\<`TValue`\>, `"started"`\> = `{}` Configuration options for the AsyncQueuer diff --git a/docs/reference/functions/asyncratelimit.md b/docs/reference/functions/asyncratelimit.md index e976f9d74..1b58d0bc7 100644 --- a/docs/reference/functions/asyncratelimit.md +++ b/docs/reference/functions/asyncratelimit.md @@ -8,10 +8,10 @@ title: asyncRateLimit # Function: asyncRateLimit() ```ts -function asyncRateLimit(fn, initialOptions): (...args) => Promise +function asyncRateLimit(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-rate-limiter.ts:245](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L245) +Defined in: [async-rate-limiter.ts:262](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L262) Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window. @@ -27,8 +27,6 @@ need to enforce a hard limit on the number of executions within a time period. • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -37,7 +35,7 @@ need to enforce a hard limit on the number of executions within a time period. ### initialOptions -`Omit`\<[`AsyncRateLimiterOptions`](../interfaces/asyncratelimiteroptions.md)\<`TFn`, `TArgs`\>, `"enabled"`\> +`Omit`\<[`AsyncRateLimiterOptions`](../interfaces/asyncratelimiteroptions.md)\<`TFn`\>, `"enabled"`\> ## Returns @@ -51,11 +49,11 @@ If execution is allowed, waits for any previous execution to complete before pro #### args -...`TArgs` +...`Parameters`\<`TFn`\> ### Returns -`Promise`\<`boolean`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> ### Example diff --git a/docs/reference/functions/asyncthrottle.md b/docs/reference/functions/asyncthrottle.md index faa76312c..465db7b44 100644 --- a/docs/reference/functions/asyncthrottle.md +++ b/docs/reference/functions/asyncthrottle.md @@ -8,10 +8,10 @@ title: asyncThrottle # Function: asyncThrottle() ```ts -function asyncThrottle(fn, initialOptions): (...args) => Promise +function asyncThrottle(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-throttler.ts:223](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L223) +Defined in: [async-throttler.ts:272](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L272) 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. @@ -21,8 +21,6 @@ If called while executing, it will wait until execution completes before schedul • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -31,7 +29,7 @@ If called while executing, it will wait until execution completes before schedul ### initialOptions -`Omit`\<[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`, `TArgs`\>, `"enabled"`\> +`Omit`\<[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`\>, `"enabled"`\> ## Returns @@ -44,11 +42,11 @@ If a call is already in progress, it may be blocked or queued depending on the ` #### args -...`TArgs` +...`Parameters`\<`TFn`\> ### Returns -`Promise`\<`void`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> ## Example diff --git a/docs/reference/functions/bindinstancemethods.md b/docs/reference/functions/bindinstancemethods.md index 85a651616..427397eca 100644 --- a/docs/reference/functions/bindinstancemethods.md +++ b/docs/reference/functions/bindinstancemethods.md @@ -8,7 +8,7 @@ title: bindInstanceMethods # Function: bindInstanceMethods() ```ts -function bindInstanceMethods(instance): any +function bindInstanceMethods(instance): T ``` Defined in: [utils.ts:1](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/utils.ts#L1) @@ -25,4 +25,4 @@ Defined in: [utils.ts:1](https://github.com/TanStack/pacer/blob/main/packages/pa ## Returns -`any` +`T` diff --git a/docs/reference/functions/debounce.md b/docs/reference/functions/debounce.md index 5f99aead4..b5341b9c0 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:194](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L194) +Defined in: [debouncer.ts:187](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L187) 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. @@ -34,20 +34,17 @@ before allowing another execution. ### initialOptions -`Omit`\<[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`, `Parameters`\<`TFn`\>\>, `"enabled"`\> +`Omit`\<[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`\>, `"enabled"`\> ## Returns `Function` -Attempts to execute the debounced function -If a call is already in progress, it will be queued - ### Parameters #### args -...`Parameters` +...`Parameters`\<`TFn`\> ### Returns diff --git a/docs/reference/functions/queue.md b/docs/reference/functions/queue.md index 8c347c503..a80b3aba0 100644 --- a/docs/reference/functions/queue.md +++ b/docs/reference/functions/queue.md @@ -11,7 +11,7 @@ title: queue function queue(options): (item, position, runOnUpdate) => boolean ``` -Defined in: [queuer.ts:408](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L408) +Defined in: [queuer.ts:496](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L496) Creates a queue that processes items in a queuer immediately upon addition. Items are processed sequentially in FIFO order by default. diff --git a/docs/reference/functions/ratelimit.md b/docs/reference/functions/ratelimit.md index 3ca5f4054..0c6836671 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:228](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L228) +Defined in: [rate-limiter.ts:216](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L216) Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window. @@ -35,7 +35,7 @@ need to enforce a hard limit on the number of executions within a time period. ### initialOptions -`Omit`\<[`RateLimiterOptions`](../interfaces/ratelimiteroptions.md)\<`TFn`, `Parameters`\<`TFn`\>\>, `"enabled"`\> +`Omit`\<[`RateLimiterOptions`](../interfaces/ratelimiteroptions.md)\<`TFn`\>, `"enabled"`\> ## Returns @@ -48,7 +48,7 @@ Will reject execution if the number of calls in the current window exceeds the l #### args -...`Parameters` +...`Parameters`\<`TFn`\> ### Returns diff --git a/docs/reference/functions/throttle.md b/docs/reference/functions/throttle.md index 05b577f4d..d529b83e6 100644 --- a/docs/reference/functions/throttle.md +++ b/docs/reference/functions/throttle.md @@ -8,10 +8,10 @@ title: throttle # Function: throttle() ```ts -function throttle(fn, initialOptions): (...args) => void +function throttle(fn, initialOptions): (...args) => void ``` -Defined in: [throttler.ts:242](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L242) +Defined in: [throttler.ts:234](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L234) Creates a throttled function that limits how often the provided function can execute. @@ -29,8 +29,6 @@ limits, consider using rateLimit(). • **TFn** *extends* [`AnyFunction`](../type-aliases/anyfunction.md) -• **TArgs** *extends* `any`[] - ## Parameters ### fn @@ -39,7 +37,7 @@ limits, consider using rateLimit(). ### initialOptions -`Omit`\<[`ThrottlerOptions`](../interfaces/throttleroptions.md)\<`TFn`, `TArgs`\>, `"enabled"`\> +`Omit`\<[`ThrottlerOptions`](../interfaces/throttleroptions.md)\<`TFn`\>, `"enabled"`\> ## Returns @@ -59,7 +57,7 @@ Attempts to execute the throttled function. The execution behavior depends on th #### args -...`TArgs` +...`Parameters`\<`TFn`\> ### Returns diff --git a/docs/reference/interfaces/asyncdebounceroptions.md b/docs/reference/interfaces/asyncdebounceroptions.md index 7d8199619..6b14b52b9 100644 --- a/docs/reference/interfaces/asyncdebounceroptions.md +++ b/docs/reference/interfaces/asyncdebounceroptions.md @@ -5,7 +5,7 @@ title: AsyncDebouncerOptions -# Interface: AsyncDebouncerOptions\ +# Interface: AsyncDebouncerOptions\ Defined in: [async-debouncer.ts:6](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L6) @@ -15,8 +15,6 @@ Options for configuring an async debounced function • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### enabled? @@ -25,7 +23,7 @@ Options for configuring an async debounced function optional enabled: boolean; ``` -Defined in: [async-debouncer.ts:14](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L14) +Defined in: [async-debouncer.ts:11](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L11) Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions. Defaults to true. @@ -38,7 +36,7 @@ Defaults to true. optional leading: boolean; ``` -Defined in: [async-debouncer.ts:19](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L19) +Defined in: [async-debouncer.ts:16](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L16) Whether to execute on the leading edge of the timeout. Defaults to false. @@ -48,10 +46,10 @@ Defaults to false. ### onError()? ```ts -optional onError: (error) => void; +optional onError: (error, debouncer) => void; ``` -Defined in: [async-debouncer.ts:23](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L23) +Defined in: [async-debouncer.ts:20](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L20) Optional error handler for when the debounced function throws @@ -61,27 +59,57 @@ Optional error handler for when the debounced function throws `unknown` +##### debouncer + +[`AsyncDebouncer`](../classes/asyncdebouncer.md)\<`TFn`\> + #### Returns `void` *** -### onExecute()? +### onSettled()? ```ts -optional onExecute: (debouncer) => void; +optional onSettled: (debouncer) => void; ``` -Defined in: [async-debouncer.ts:27](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L27) +Defined in: [async-debouncer.ts:24](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L24) -Optional function to call when the debounced function is executed +Optional callback to call when the debounced function is executed #### Parameters ##### debouncer -[`AsyncDebouncer`](../classes/asyncdebouncer.md)\<`TFn`, `TArgs`\> +[`AsyncDebouncer`](../classes/asyncdebouncer.md)\<`TFn`\> + +#### Returns + +`void` + +*** + +### onSuccess()? + +```ts +optional onSuccess: (result, debouncer) => void; +``` + +Defined in: [async-debouncer.ts:28](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L28) + +Optional callback to call when the debounced function is executed + +#### Parameters + +##### result + +`ReturnType`\<`TFn`\> + +##### debouncer + +[`AsyncDebouncer`](../classes/asyncdebouncer.md)\<`TFn`\> #### Returns @@ -95,7 +123,7 @@ Optional function to call when the debounced function is executed optional trailing: boolean; ``` -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:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L33) Whether to execute on the trailing edge of the timeout. Defaults to true. @@ -108,7 +136,7 @@ Defaults to true. wait: number; ``` -Defined in: [async-debouncer.ts:37](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L37) +Defined in: [async-debouncer.ts:38](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L38) Delay in milliseconds to wait after the last call before executing Defaults to 0ms diff --git a/docs/reference/interfaces/asyncqueueroptions.md b/docs/reference/interfaces/asyncqueueroptions.md index 665bb354b..9d8fcd48f 100644 --- a/docs/reference/interfaces/asyncqueueroptions.md +++ b/docs/reference/interfaces/asyncqueueroptions.md @@ -45,13 +45,53 @@ Maximum number of concurrent tasks to process *** +### expirationDuration? + +```ts +optional expirationDuration: number; +``` + +Defined in: [async-queuer.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L17) + +Maximum time in milliseconds that an item can stay in the queue +If not provided, items will never expire + +*** + +### getIsExpired()? + +```ts +optional getIsExpired: (item, addedAt) => boolean; +``` + +Defined in: [async-queuer.ts:22](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L22) + +Function to determine if an item has expired +If provided, this overrides the expirationDuration behavior + +#### Parameters + +##### item + +() => `Promise`\<`TValue`\> + +##### addedAt + +`number` + +#### Returns + +`boolean` + +*** + ### getItemsFrom? ```ts optional getItemsFrom: QueuePosition; ``` -Defined in: [async-queuer.ts:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L17) +Defined in: [async-queuer.ts:27](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L27) Default position to get items from during processing @@ -69,7 +109,7 @@ Default position to get items from during processing optional getPriority: (item) => number; ``` -Defined in: [async-queuer.ts:23](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L23) +Defined in: [async-queuer.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L33) Function to determine priority of items in the queuer Higher priority items will be processed first @@ -93,7 +133,7 @@ If not provided, will use static priority values attached to tasks optional initialItems: () => Promise & object[]; ``` -Defined in: [async-queuer.ts:27](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L27) +Defined in: [async-queuer.ts:37](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L37) Initial items to populate the queuer with @@ -105,19 +145,45 @@ Initial items to populate the queuer with optional maxSize: number; ``` -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:41](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L41) Maximum number of items allowed in the queuer *** +### onExpire()? + +```ts +optional onExpire: (item, queuer) => void; +``` + +Defined in: [async-queuer.ts:64](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L64) + +Callback fired whenever an item expires in the queuer + +#### Parameters + +##### item + +() => `Promise`\<`TValue`\> + +##### queuer + +[`AsyncQueuer`](../classes/asyncqueuer.md)\<`TValue`\> + +#### Returns + +`void` + +*** + ### onGetNextItem()? ```ts optional onGetNextItem: (item, queuer) => void; ``` -Defined in: [async-queuer.ts:35](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L35) +Defined in: [async-queuer.ts:45](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L45) Callback fired whenever an item is removed from the queuer @@ -143,7 +209,7 @@ Callback fired whenever an item is removed from the queuer optional onIsRunningChange: (queuer) => void; ``` -Defined in: [async-queuer.ts:42](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L42) +Defined in: [async-queuer.ts:52](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L52) Callback fired whenever the queuer's running state changes @@ -165,7 +231,7 @@ Callback fired whenever the queuer's running state changes optional onItemsChange: (queuer) => void; ``` -Defined in: [async-queuer.ts:46](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L46) +Defined in: [async-queuer.ts:56](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L56) Callback fired whenever an item is added or removed from the queuer @@ -187,7 +253,7 @@ Callback fired whenever an item is added or removed from the queuer optional onReject: (item, queuer) => void; ``` -Defined in: [async-queuer.ts:50](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L50) +Defined in: [async-queuer.ts:60](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L60) Callback fired whenever an item is rejected from being added to the queuer @@ -213,9 +279,9 @@ Callback fired whenever an item is rejected from being added to the queuer optional started: boolean; ``` -Defined in: [async-queuer.ts:54](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L54) +Defined in: [async-queuer.ts:68](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L68) -Whether the queuer should start processing tasks immediately +Whether the queuer should start processing tasks immediately or not. *** @@ -225,6 +291,6 @@ Whether the queuer should start processing tasks immediately optional wait: number; ``` -Defined in: [async-queuer.ts:58](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L58) +Defined in: [async-queuer.ts:72](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L72) Time in milliseconds to wait between processing items diff --git a/docs/reference/interfaces/asyncratelimiteroptions.md b/docs/reference/interfaces/asyncratelimiteroptions.md index 247683cdb..0319b4d6c 100644 --- a/docs/reference/interfaces/asyncratelimiteroptions.md +++ b/docs/reference/interfaces/asyncratelimiteroptions.md @@ -5,7 +5,7 @@ title: AsyncRateLimiterOptions -# Interface: AsyncRateLimiterOptions\ +# Interface: AsyncRateLimiterOptions\ Defined in: [async-rate-limiter.ts:6](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L6) @@ -15,8 +15,6 @@ Options for configuring an async rate-limited function • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### enabled? @@ -25,7 +23,7 @@ Options for configuring an async rate-limited function optional enabled: boolean; ``` -Defined in: [async-rate-limiter.ts:14](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L14) +Defined in: [async-rate-limiter.ts:11](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L11) Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions. Defaults to true. @@ -38,7 +36,7 @@ Defaults to true. limit: 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:15](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L15) Maximum number of executions allowed within the time window @@ -47,10 +45,10 @@ Maximum number of executions allowed within the time window ### onError()? ```ts -optional onError: (error) => void; +optional onError: (error, rateLimiter) => void; ``` -Defined in: [async-rate-limiter.ts:22](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L22) +Defined in: [async-rate-limiter.ts:19](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L19) Optional error handler for when the rate-limited function throws @@ -60,19 +58,45 @@ Optional error handler for when the rate-limited function throws `unknown` +##### rateLimiter + +[`AsyncRateLimiter`](../classes/asyncratelimiter.md)\<`TFn`\> + +#### Returns + +`void` + +*** + +### onReject()? + +```ts +optional onReject: (rateLimiter) => void; +``` + +Defined in: [async-rate-limiter.ts:34](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L34) + +Optional callback function that is called when an execution is rejected due to rate limiting + +#### Parameters + +##### rateLimiter + +[`AsyncRateLimiter`](../classes/asyncratelimiter.md)\<`TFn`\> + #### Returns `void` *** -### onExecute()? +### onSettled()? ```ts -optional onExecute: (rateLimiter) => void; +optional onSettled: (rateLimiter) => void; ``` -Defined in: [async-rate-limiter.ts:26](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L26) +Defined in: [async-rate-limiter.ts:23](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L23) Optional function to call when the rate-limited function is executed @@ -80,7 +104,7 @@ Optional function to call when the rate-limited function is executed ##### rateLimiter -[`AsyncRateLimiter`](../classes/asyncratelimiter.md)\<`TFn`, `TArgs`\> +[`AsyncRateLimiter`](../classes/asyncratelimiter.md)\<`TFn`\> #### Returns @@ -88,21 +112,25 @@ Optional function to call when the rate-limited function is executed *** -### onReject()? +### onSuccess()? ```ts -optional onReject: (rateLimiter) => void; +optional onSuccess: (result, rateLimiter) => void; ``` -Defined in: [async-rate-limiter.ts:30](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L30) +Defined in: [async-rate-limiter.ts:27](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L27) -Optional callback function that is called when an execution is rejected due to rate limiting +Optional function to call when the rate-limited function is executed #### Parameters +##### result + +`ReturnType`\<`TFn`\> + ##### rateLimiter -[`AsyncRateLimiter`](../classes/asyncratelimiter.md)\<`TFn`, `TArgs`\> +[`AsyncRateLimiter`](../classes/asyncratelimiter.md)\<`TFn`\> #### Returns @@ -116,6 +144,6 @@ Optional callback function that is called when an execution is rejected due to r window: number; ``` -Defined in: [async-rate-limiter.ts:34](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L34) +Defined in: [async-rate-limiter.ts:38](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L38) Time window in milliseconds within which the limit applies diff --git a/docs/reference/interfaces/asyncthrottleroptions.md b/docs/reference/interfaces/asyncthrottleroptions.md index 79eac2bfb..dc1d6106e 100644 --- a/docs/reference/interfaces/asyncthrottleroptions.md +++ b/docs/reference/interfaces/asyncthrottleroptions.md @@ -5,7 +5,7 @@ title: AsyncThrottlerOptions -# Interface: AsyncThrottlerOptions\ +# Interface: AsyncThrottlerOptions\ Defined in: [async-throttler.ts:6](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L6) @@ -15,8 +15,6 @@ Options for configuring an async throttled function • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### enabled? @@ -25,20 +23,33 @@ Options for configuring an async throttled function optional enabled: boolean; ``` -Defined in: [async-throttler.ts:14](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L14) +Defined in: [async-throttler.ts:11](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L11) Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions. Defaults to true. *** +### leading? + +```ts +optional leading: boolean; +``` + +Defined in: [async-throttler.ts:16](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L16) + +Whether to execute the function immediately when called +Defaults to true + +*** + ### onError()? ```ts -optional onError: (error) => void; +optional onError: (error, asyncThrottler) => void; ``` -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:20](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L20) Optional error handler for when the throttled function throws @@ -48,27 +59,57 @@ Optional error handler for when the throttled function throws `unknown` +##### asyncThrottler + +[`AsyncThrottler`](../classes/asyncthrottler.md)\<`TFn`\> + +#### Returns + +`void` + +*** + +### onSettled()? + +```ts +optional onSettled: (asyncThrottler) => void; +``` + +Defined in: [async-throttler.ts:24](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L24) + +Optional function to call when the throttled function is executed + +#### Parameters + +##### asyncThrottler + +[`AsyncThrottler`](../classes/asyncthrottler.md)\<`TFn`\> + #### Returns `void` *** -### onExecute()? +### onSuccess()? ```ts -optional onExecute: (throttler) => void; +optional onSuccess: (result, asyncThrottler) => void; ``` -Defined in: [async-throttler.ts:22](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L22) +Defined in: [async-throttler.ts:28](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L28) Optional function to call when the throttled function is executed #### Parameters -##### throttler +##### result + +`ReturnType`\<`TFn`\> -[`AsyncThrottler`](../classes/asyncthrottler.md)\<`TFn`, `TArgs`\> +##### asyncThrottler + +[`AsyncThrottler`](../classes/asyncthrottler.md)\<`TFn`\> #### Returns @@ -76,13 +117,26 @@ Optional function to call when the throttled function is executed *** +### trailing? + +```ts +optional trailing: boolean; +``` + +Defined in: [async-throttler.ts:36](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L36) + +Whether to execute the function on the trailing edge of the wait period +Defaults to true + +*** + ### wait ```ts wait: number; ``` -Defined in: [async-throttler.ts:27](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L27) +Defined in: [async-throttler.ts:41](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L41) Time window in milliseconds during which the function can only be executed once Defaults to 0ms diff --git a/docs/reference/interfaces/debounceroptions.md b/docs/reference/interfaces/debounceroptions.md index c79478a90..16f6f0b22 100644 --- a/docs/reference/interfaces/debounceroptions.md +++ b/docs/reference/interfaces/debounceroptions.md @@ -5,7 +5,7 @@ title: DebouncerOptions -# Interface: DebouncerOptions\ +# Interface: DebouncerOptions\ Defined in: [debouncer.ts:6](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L6) @@ -15,8 +15,6 @@ Options for configuring a debounced function • **TFn** *extends* [`AnyFunction`](../type-aliases/anyfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### enabled? @@ -25,7 +23,7 @@ Options for configuring a debounced function optional enabled: boolean; ``` -Defined in: [debouncer.ts:14](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L14) +Defined in: [debouncer.ts:11](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L11) Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions. Defaults to true. @@ -38,9 +36,10 @@ Defaults to true. 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:17](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L17) Whether to execute on the leading edge of the timeout. +The first call will execute immediately and the rest will wait the delay. Defaults to false. *** @@ -51,7 +50,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:21](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L21) Callback function that is called after the function is executed @@ -59,7 +58,7 @@ Callback function that is called after the function is executed ##### debouncer -[`Debouncer`](../classes/debouncer.md)\<`TFn`, `TArgs`\> +[`Debouncer`](../classes/debouncer.md)\<`TFn`\> #### Returns @@ -73,7 +72,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:26](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L26) Whether to execute on the trailing edge of the timeout. Defaults to true. @@ -86,7 +85,7 @@ Defaults to true. wait: number; ``` -Defined in: [debouncer.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L33) +Defined in: [debouncer.ts:31](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L31) Delay in milliseconds before executing the function Defaults to 0ms diff --git a/docs/reference/interfaces/queueroptions.md b/docs/reference/interfaces/queueroptions.md index 07d3b69c9..57429e087 100644 --- a/docs/reference/interfaces/queueroptions.md +++ b/docs/reference/interfaces/queueroptions.md @@ -35,13 +35,53 @@ Default position to add items to the queuer *** +### expirationDuration? + +```ts +optional expirationDuration: number; +``` + +Defined in: [queuer.ts:14](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L14) + +Maximum time in milliseconds that an item can stay in the queue +If not provided, items will never expire + +*** + +### getIsExpired()? + +```ts +optional getIsExpired: (item, addedAt) => boolean; +``` + +Defined in: [queuer.ts:19](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L19) + +Function to determine if an item has expired +If provided, this overrides the expirationDuration behavior + +#### Parameters + +##### item + +`TValue` + +##### addedAt + +`number` + +#### Returns + +`boolean` + +*** + ### getItemsFrom? ```ts optional getItemsFrom: QueuePosition; ``` -Defined in: [queuer.ts:14](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L14) +Defined in: [queuer.ts:24](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L24) Default position to get items from during processing @@ -59,7 +99,7 @@ Default position to get items from during processing optional getPriority: (item) => number; ``` -Defined in: [queuer.ts:19](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L19) +Defined in: [queuer.ts:29](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L29) Function to determine priority of items in the queuer Higher priority items will be processed first @@ -82,7 +122,7 @@ Higher priority items will be processed first optional initialItems: TValue[]; ``` -Defined in: [queuer.ts:23](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L23) +Defined in: [queuer.ts:33](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L33) Initial items to populate the queuer with @@ -94,19 +134,45 @@ Initial items to populate the queuer with optional maxSize: number; ``` -Defined in: [queuer.ts:27](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L27) +Defined in: [queuer.ts:37](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L37) Maximum number of items allowed in the queuer *** +### onExpire()? + +```ts +optional onExpire: (item, queuer) => void; +``` + +Defined in: [queuer.ts:41](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L41) + +Callback fired whenever an item expires in the queuer + +#### Parameters + +##### item + +`TValue` + +##### queuer + +[`Queuer`](../classes/queuer.md)\<`TValue`\> + +#### Returns + +`void` + +*** + ### onGetNextItem()? ```ts optional onGetNextItem: (item, queuer) => void; ``` -Defined in: [queuer.ts:31](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L31) +Defined in: [queuer.ts:45](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L45) Callback fired whenever an item is removed from the queuer @@ -132,7 +198,7 @@ Callback fired whenever an item is removed from the queuer optional onIsRunningChange: (queuer) => void; ``` -Defined in: [queuer.ts:35](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L35) +Defined in: [queuer.ts:49](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L49) Callback fired whenever the queuer's running state changes @@ -154,7 +220,7 @@ Callback fired whenever the queuer's running state changes optional onItemsChange: (queuer) => void; ``` -Defined in: [queuer.ts:39](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L39) +Defined in: [queuer.ts:53](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L53) Callback fired whenever an item is added or removed from the queuer @@ -176,7 +242,7 @@ Callback fired whenever an item is added or removed from the queuer optional onReject: (item, queuer) => void; ``` -Defined in: [queuer.ts:43](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L43) +Defined in: [queuer.ts:57](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L57) Callback fired whenever an item is rejected from being added to the queuer @@ -202,7 +268,7 @@ Callback fired whenever an item is rejected from being added to the queuer optional started: boolean; ``` -Defined in: [queuer.ts:47](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L47) +Defined in: [queuer.ts:61](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L61) Whether the queuer should start processing tasks immediately @@ -214,6 +280,6 @@ Whether the queuer should start processing tasks immediately optional wait: number; ``` -Defined in: [queuer.ts:51](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L51) +Defined in: [queuer.ts:65](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L65) Time in milliseconds to wait between processing items diff --git a/docs/reference/interfaces/ratelimiteroptions.md b/docs/reference/interfaces/ratelimiteroptions.md index c1ca45156..8958fffd6 100644 --- a/docs/reference/interfaces/ratelimiteroptions.md +++ b/docs/reference/interfaces/ratelimiteroptions.md @@ -5,7 +5,7 @@ title: RateLimiterOptions -# Interface: RateLimiterOptions\ +# Interface: RateLimiterOptions\ Defined in: [rate-limiter.ts:6](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L6) @@ -15,8 +15,6 @@ Options for configuring a rate-limited function • **TFn** *extends* [`AnyFunction`](../type-aliases/anyfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### enabled? @@ -25,7 +23,7 @@ Options for configuring a rate-limited function optional enabled: boolean; ``` -Defined in: [rate-limiter.ts:14](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L14) +Defined in: [rate-limiter.ts:11](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L11) Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions. Defaults to true. @@ -38,7 +36,7 @@ Defaults to true. limit: number; ``` -Defined in: [rate-limiter.ts:18](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L18) +Defined in: [rate-limiter.ts:15](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L15) Maximum number of executions allowed within the time window @@ -50,7 +48,7 @@ Maximum number of executions allowed within the time window optional onExecute: (rateLimiter) => void; ``` -Defined in: [rate-limiter.ts:22](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L22) +Defined in: [rate-limiter.ts:19](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L19) Callback function that is called after the function is executed @@ -58,7 +56,7 @@ Callback function that is called after the function is executed ##### rateLimiter -[`RateLimiter`](../classes/ratelimiter.md)\<`TFn`, `TArgs`\> +[`RateLimiter`](../classes/ratelimiter.md)\<`TFn`\> #### Returns @@ -72,7 +70,7 @@ Callback function that is called after the function is executed optional onReject: (rateLimiter) => void; ``` -Defined in: [rate-limiter.ts:26](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L26) +Defined in: [rate-limiter.ts:23](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L23) Optional callback function that is called when an execution is rejected due to rate limiting @@ -80,7 +78,7 @@ Optional callback function that is called when an execution is rejected due to r ##### rateLimiter -[`RateLimiter`](../classes/ratelimiter.md)\<`TFn`, `TArgs`\> +[`RateLimiter`](../classes/ratelimiter.md)\<`TFn`\> #### Returns @@ -94,6 +92,6 @@ Optional callback function that is called when an execution is rejected due to r window: 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:27](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L27) Time window in milliseconds within which the limit applies diff --git a/docs/reference/interfaces/throttleroptions.md b/docs/reference/interfaces/throttleroptions.md index f1241805b..d97f2c0b8 100644 --- a/docs/reference/interfaces/throttleroptions.md +++ b/docs/reference/interfaces/throttleroptions.md @@ -5,7 +5,7 @@ title: ThrottlerOptions -# Interface: ThrottlerOptions\ +# Interface: ThrottlerOptions\ Defined in: [throttler.ts:6](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L6) @@ -15,8 +15,6 @@ Options for configuring a throttled function • **TFn** *extends* [`AnyFunction`](../type-aliases/anyfunction.md) -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### enabled? @@ -25,7 +23,7 @@ Options for configuring a throttled function optional enabled: boolean; ``` -Defined in: [throttler.ts:14](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L14) +Defined in: [throttler.ts:11](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L11) Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions. Defaults to true. @@ -38,7 +36,7 @@ Defaults to true. optional leading: boolean; ``` -Defined in: [throttler.ts:19](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L19) +Defined in: [throttler.ts:16](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L16) Whether to execute on the leading edge of the timeout. Defaults to true. @@ -51,7 +49,7 @@ Defaults to true. optional onExecute: (throttler) => void; ``` -Defined in: [throttler.ts:23](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L23) +Defined in: [throttler.ts:20](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L20) Callback function that is called after the function is executed @@ -59,7 +57,7 @@ Callback function that is called after the function is executed ##### throttler -[`Throttler`](../classes/throttler.md)\<`TFn`, `TArgs`\> +[`Throttler`](../classes/throttler.md)\<`TFn`\> #### Returns @@ -73,7 +71,7 @@ Callback function that is called after the function is executed optional trailing: boolean; ``` -Defined in: [throttler.ts:28](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L28) +Defined in: [throttler.ts:25](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L25) Whether to execute on the trailing edge of the timeout. Defaults to true. @@ -86,6 +84,6 @@ Defaults to true. wait: number; ``` -Defined in: [throttler.ts:32](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L32) +Defined in: [throttler.ts:29](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L29) Time window in milliseconds during which the function can only be executed once diff --git a/docs/reference/type-aliases/anyasyncfunction.md b/docs/reference/type-aliases/anyasyncfunction.md index bf8a5e345..19b6a17d7 100644 --- a/docs/reference/type-aliases/anyasyncfunction.md +++ b/docs/reference/type-aliases/anyasyncfunction.md @@ -5,30 +5,22 @@ title: AnyAsyncFunction -# Type Alias: AnyAsyncFunction()\ +# Type Alias: AnyAsyncFunction() ```ts -type AnyAsyncFunction = (...args) => Promise; +type AnyAsyncFunction = (...args) => Promise; ``` -Defined in: [types.ts:15](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/types.ts#L15) +Defined in: [types.ts:9](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/types.ts#L9) Represents an asynchronous function that can be called with any arguments and returns a promise. -## Type Parameters - -• **TArgs** *extends* `any`[] = `any`[] - -The type of the arguments the function can be called with. - ## Parameters ### args -...`TArgs` +...`any`[] ## Returns `Promise`\<`any`\> - -A promise that resolves to the return value of the function. diff --git a/docs/reference/type-aliases/anyfunction.md b/docs/reference/type-aliases/anyfunction.md index 14372d406..039ac0e6e 100644 --- a/docs/reference/type-aliases/anyfunction.md +++ b/docs/reference/type-aliases/anyfunction.md @@ -5,30 +5,22 @@ title: AnyFunction -# Type Alias: AnyFunction()\ +# Type Alias: AnyFunction() ```ts -type AnyFunction = (...args) => any; +type AnyFunction = (...args) => any; ``` -Defined in: [types.ts:6](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/types.ts#L6) +Defined in: [types.ts:4](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/types.ts#L4) Represents a function that can be called with any arguments and returns any value. -## Type Parameters - -• **TArgs** *extends* `any`[] = `any`[] - -The type of the arguments the function can be called with. - ## Parameters ### args -...`TArgs` +...`any`[] ## Returns `any` - -The return value of the function. diff --git a/docs/reference/type-aliases/queueposition.md b/docs/reference/type-aliases/queueposition.md index 5b569b030..e1dc4628b 100644 --- a/docs/reference/type-aliases/queueposition.md +++ b/docs/reference/type-aliases/queueposition.md @@ -11,6 +11,6 @@ title: QueuePosition type QueuePosition = "front" | "back"; ``` -Defined in: [queuer.ts:71](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L71) +Defined in: [queuer.ts:88](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L88) Position type for addItem and getNextItem operations diff --git a/examples/react/asyncDebounce/package.json b/examples/react/asyncDebounce/package.json index 87d31b9ad..385e7bc2d 100644 --- a/examples/react/asyncDebounce/package.json +++ b/examples/react/asyncDebounce/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/asyncDebounce/src/index.tsx b/examples/react/asyncDebounce/src/index.tsx index a84f35354..67196180a 100644 --- a/examples/react/asyncDebounce/src/index.tsx +++ b/examples/react/asyncDebounce/src/index.tsx @@ -44,6 +44,7 @@ function SearchApp() {

TanStack Pacer asyncDebounce Example

{ diff --git a/examples/react/asyncRateLimit/package.json b/examples/react/asyncRateLimit/package.json index bdecbd373..a56fa7615 100644 --- a/examples/react/asyncRateLimit/package.json +++ b/examples/react/asyncRateLimit/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/asyncRateLimit/src/index.tsx b/examples/react/asyncRateLimit/src/index.tsx index 3b9d3caa2..a845428e5 100644 --- a/examples/react/asyncRateLimit/src/index.tsx +++ b/examples/react/asyncRateLimit/src/index.tsx @@ -50,6 +50,7 @@ function SearchApp() {

TanStack Pacer asyncRateLimit Example

{ diff --git a/examples/react/asyncThrottle/package.json b/examples/react/asyncThrottle/package.json index 6c6e5e2dc..56665f3a9 100644 --- a/examples/react/asyncThrottle/package.json +++ b/examples/react/asyncThrottle/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/asyncThrottle/src/index.tsx b/examples/react/asyncThrottle/src/index.tsx index 6b17da80f..d63914d38 100644 --- a/examples/react/asyncThrottle/src/index.tsx +++ b/examples/react/asyncThrottle/src/index.tsx @@ -44,6 +44,7 @@ function SearchApp() {

TanStack Pacer asyncThrottle Example

{ diff --git a/examples/react/debounce/package.json b/examples/react/debounce/package.json index 0efde63ef..b5eda09c2 100644 --- a/examples/react/debounce/package.json +++ b/examples/react/debounce/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/debounce/src/index.tsx b/examples/react/debounce/src/index.tsx index 35d97a14f..03ac882f4 100644 --- a/examples/react/debounce/src/index.tsx +++ b/examples/react/debounce/src/index.tsx @@ -11,6 +11,7 @@ function App1() { const debouncedSetCount = useCallback( debounce(setDebouncedCount, { wait: 500, + // leading: true, // optional, defaults to false }), [], ) @@ -69,6 +70,7 @@ function App2() {

TanStack Pacer debounce Example 2

TanStack Pacer queue Example 2
TanStack Pacer rateLimit Example 2
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/useAsyncQueuerState/public/emblem-light.svg b/examples/react/react-query-debounced-prefetch/public/emblem-light.svg similarity index 100% rename from examples/react/useAsyncQueuerState/public/emblem-light.svg rename to examples/react/react-query-debounced-prefetch/public/emblem-light.svg diff --git a/examples/react/react-query-debounced-prefetch/src/index.tsx b/examples/react/react-query-debounced-prefetch/src/index.tsx new file mode 100644 index 000000000..ec9e8299c --- /dev/null +++ b/examples/react/react-query-debounced-prefetch/src/index.tsx @@ -0,0 +1,144 @@ +import React, { useEffect } from 'react' +import ReactDOM from 'react-dom/client' +import { + QueryClient, + QueryClientProvider, + useQuery, +} from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { useDebouncedValue } from '@tanstack/react-pacer' + +// Fetch all posts +const fetchPosts = async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts') + return response.json() +} + +// Fetch a single post +const fetchPost = async (id: number) => { + await new Promise((resolve) => setTimeout(resolve, 1000)) // Simulate a slow response + const response = await fetch( + `https://jsonplaceholder.typicode.com/posts/${id}`, + ) + return response.json() +} + +function PostList({ + setSelectedPostId, +}: { + setSelectedPostId: (id: number) => void +}) { + // Simple query that fetches all posts + const { data: posts, isLoading } = useQuery({ + queryKey: ['posts'], + queryFn: fetchPosts, + }) + + // keep track of the hovered post id that could be prefetched + const [currentHoveredPostId, setCurrentHoveredPostId] = React.useState< + number | null + >(null) + + // debounce the hovered post id to avoid excessive prefetches + const [debouncedHoveredPostId] = useDebouncedValue(currentHoveredPostId, { + wait: 100, // adjust this value to see the difference + }) + + // perform the prefetch when the debounced hovered post id changes + useEffect(() => { + if (debouncedHoveredPostId) { + queryClient.ensureQueryData({ + queryKey: ['post', debouncedHoveredPostId], + queryFn: () => fetchPost(debouncedHoveredPostId), + }) + } + }, [debouncedHoveredPostId]) + + const handleMouseEnter = (postId: number) => { + setCurrentHoveredPostId(postId) // update the hovered post id + } + + if (isLoading) return
Loading posts...
+ + return ( + + ) +} + +function PostDetail({ postId }: { postId: number }) { + const { data: post, isLoading } = useQuery({ + queryKey: ['post', postId], + queryFn: () => fetchPost(postId), + }) + + if (isLoading) return
Loading post...
+ + return ( +
+

{post?.title}

+

{post?.body}

+
+ ) +} + +function App() { + const [selectedPostId, setSelectedPostId] = React.useState( + null, + ) + + return ( +
+

TanStack Pacer/Query Debounced Prefetch Example

+

Hover over a post title to prefetch its content

+

+ This example shows how to prefetch a query when the user hovers over a + post. +

+

+ The debounced query key is created after a debounce to avoid excessive + prefetches. +

+
+ + {selectedPostId && } +
+
+ ) +} + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10_000, + }, + }, +}) + +const root = ReactDOM.createRoot(document.getElementById('root')!) +root.render( + + + + , +) diff --git a/examples/react/useAsyncQueuerState/tsconfig.json b/examples/react/react-query-debounced-prefetch/tsconfig.json similarity index 100% rename from examples/react/useAsyncQueuerState/tsconfig.json rename to examples/react/react-query-debounced-prefetch/tsconfig.json diff --git a/examples/react/useAsyncQueuerState/vite.config.ts b/examples/react/react-query-debounced-prefetch/vite.config.ts similarity index 100% rename from examples/react/useAsyncQueuerState/vite.config.ts rename to examples/react/react-query-debounced-prefetch/vite.config.ts diff --git a/examples/react/useQueuerState/.eslintrc.cjs b/examples/react/react-query-queued-prefetch/.eslintrc.cjs similarity index 100% rename from examples/react/useQueuerState/.eslintrc.cjs rename to examples/react/react-query-queued-prefetch/.eslintrc.cjs diff --git a/examples/react/useQueuerState/.gitignore b/examples/react/react-query-queued-prefetch/.gitignore similarity index 100% rename from examples/react/useQueuerState/.gitignore rename to examples/react/react-query-queued-prefetch/.gitignore diff --git a/examples/react/useQueuerState/README.md b/examples/react/react-query-queued-prefetch/README.md similarity index 100% rename from examples/react/useQueuerState/README.md rename to examples/react/react-query-queued-prefetch/README.md diff --git a/examples/react/useQueuerState/index.html b/examples/react/react-query-queued-prefetch/index.html similarity index 100% rename from examples/react/useQueuerState/index.html rename to examples/react/react-query-queued-prefetch/index.html diff --git a/examples/react/react-query-queued-prefetch/package.json b/examples/react/react-query-queued-prefetch/package.json new file mode 100644 index 000000000..c79047d2c --- /dev/null +++ b/examples/react/react-query-queued-prefetch/package.json @@ -0,0 +1,36 @@ +{ + "name": "@tanstack/pacer-example-react-query-queued-prefetch", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3005", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/react-pacer": "^0.2.0", + "@tanstack/react-query": "^5.74.4", + "@tanstack/react-query-devtools": "^5.74.4", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "vite": "^6.3.3" + }, + "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/useQueuerState/public/emblem-light.svg b/examples/react/react-query-queued-prefetch/public/emblem-light.svg similarity index 100% rename from examples/react/useQueuerState/public/emblem-light.svg rename to examples/react/react-query-queued-prefetch/public/emblem-light.svg diff --git a/examples/react/react-query-queued-prefetch/src/index.tsx b/examples/react/react-query-queued-prefetch/src/index.tsx new file mode 100644 index 000000000..865ea21cf --- /dev/null +++ b/examples/react/react-query-queued-prefetch/src/index.tsx @@ -0,0 +1,148 @@ +import React, { useEffect } from 'react' +import ReactDOM from 'react-dom/client' +import { + QueryClient, + QueryClientProvider, + useQuery, +} from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { useQueuedValue } from '@tanstack/react-pacer' + +// Fetch all posts +const fetchPosts = async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts') + return response.json() +} + +// Fetch a single post +const fetchPost = async (id: number) => { + await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate a slow response + const response = await fetch( + `https://jsonplaceholder.typicode.com/posts/${id}`, + ) + return response.json() +} + +function PostList({ + setSelectedPostId, +}: { + setSelectedPostId: (id: number) => void +}) { + // Simple query that fetches all posts + const { data: posts, isLoading } = useQuery({ + queryKey: ['posts'], + queryFn: fetchPosts, + }) + + // keep track of the hovered post id that could be prefetched + const [currentHoveredPostId, setCurrentHoveredPostId] = React.useState< + number | null + >(null) + + const [queuedHoveredPostId] = useQueuedValue(currentHoveredPostId, { + addItemsTo: 'front', // newest hovered link is top priority + wait: 100, // adjust this value to see the difference + expirationDuration: 500, // If a link was hovered over 500ms ago, and still hasn't been prefetched, remove it from the queue + onExpire: (item) => { + console.log('expired', item) + }, + }) + + useEffect(() => { + if (queuedHoveredPostId) { + // queue up the hovered post id to be processed in order + queryClient.ensureQueryData({ + queryKey: ['post', queuedHoveredPostId], + queryFn: () => fetchPost(queuedHoveredPostId), + }) + } + }, [queuedHoveredPostId]) + + const handleMouseEnter = (postId: number) => { + setCurrentHoveredPostId(postId) // update the hovered post id + } + + if (isLoading) return
Loading posts...
+ + return ( + + ) +} + +function PostDetail({ postId }: { postId: number }) { + const { data: post, isLoading } = useQuery({ + queryKey: ['post', postId], + queryFn: () => fetchPost(postId), + }) + + if (isLoading) return
Loading post...
+ + return ( +
+

{post?.title}

+

{post?.body}

+
+ ) +} + +function App() { + const [selectedPostId, setSelectedPostId] = React.useState( + null, + ) + + return ( +
+

TanStack Pacer/Query Queued Prefetch Example

+

Hover over a post title to queue up its prefetch

+

+ This example shows how to queue up prefetch requests when the user + hovers over a post, processing them in order with a delay between each. +

+

+ The queued query key is processed after a delay to avoid overwhelming + the server with too many requests at once. +

+
+ + {selectedPostId && } +
+
+ ) +} + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10_000, + }, + }, +}) + +const root = ReactDOM.createRoot(document.getElementById('root')!) +root.render( + + + + , +) diff --git a/examples/react/useQueuerState/tsconfig.json b/examples/react/react-query-queued-prefetch/tsconfig.json similarity index 100% rename from examples/react/useQueuerState/tsconfig.json rename to examples/react/react-query-queued-prefetch/tsconfig.json diff --git a/examples/react/useQueuerState/vite.config.ts b/examples/react/react-query-queued-prefetch/vite.config.ts similarity index 100% rename from examples/react/useQueuerState/vite.config.ts rename to examples/react/react-query-queued-prefetch/vite.config.ts diff --git a/examples/react/react-query-throttled-prefetch/.eslintrc.cjs b/examples/react/react-query-throttled-prefetch/.eslintrc.cjs new file mode 100644 index 000000000..9ff0b9fc9 --- /dev/null +++ b/examples/react/react-query-throttled-prefetch/.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/react-query-throttled-prefetch/.gitignore b/examples/react/react-query-throttled-prefetch/.gitignore new file mode 100644 index 000000000..4673b022e --- /dev/null +++ b/examples/react/react-query-throttled-prefetch/.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* diff --git a/examples/react/react-query-throttled-prefetch/README.md b/examples/react/react-query-throttled-prefetch/README.md new file mode 100644 index 000000000..1cf889265 --- /dev/null +++ b/examples/react/react-query-throttled-prefetch/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/react/react-query-throttled-prefetch/index.html b/examples/react/react-query-throttled-prefetch/index.html new file mode 100644 index 000000000..701aa26e3 --- /dev/null +++ b/examples/react/react-query-throttled-prefetch/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Pacer Example + + + +
+ + + diff --git a/examples/react/react-query-throttled-prefetch/package.json b/examples/react/react-query-throttled-prefetch/package.json new file mode 100644 index 000000000..50a000fa6 --- /dev/null +++ b/examples/react/react-query-throttled-prefetch/package.json @@ -0,0 +1,36 @@ +{ + "name": "@tanstack/pacer-example-react-query-throttled-prefetch", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3005", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/react-pacer": "^0.2.0", + "@tanstack/react-query": "^5.74.4", + "@tanstack/react-query-devtools": "^5.74.4", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "vite": "^6.3.3" + }, + "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/react-query-throttled-prefetch/public/emblem-light.svg b/examples/react/react-query-throttled-prefetch/public/emblem-light.svg new file mode 100644 index 000000000..a58e69ad5 --- /dev/null +++ b/examples/react/react-query-throttled-prefetch/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/react-query-throttled-prefetch/src/index.tsx b/examples/react/react-query-throttled-prefetch/src/index.tsx new file mode 100644 index 000000000..4d9b11118 --- /dev/null +++ b/examples/react/react-query-throttled-prefetch/src/index.tsx @@ -0,0 +1,144 @@ +import React, { useEffect } from 'react' +import ReactDOM from 'react-dom/client' +import { + QueryClient, + QueryClientProvider, + useQuery, +} from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { useThrottledValue } from '@tanstack/react-pacer' + +// Fetch all posts +const fetchPosts = async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts') + return response.json() +} + +// Fetch a single post +const fetchPost = async (id: number) => { + await new Promise((resolve) => setTimeout(resolve, 1000)) // Simulate a slow response + const response = await fetch( + `https://jsonplaceholder.typicode.com/posts/${id}`, + ) + return response.json() +} + +function PostList({ + setSelectedPostId, +}: { + setSelectedPostId: (id: number) => void +}) { + // Simple query that fetches all posts + const { data: posts, isLoading } = useQuery({ + queryKey: ['posts'], + queryFn: fetchPosts, + }) + + // keep track of the hovered post id that could be prefetched + const [currentHoveredPostId, setCurrentHoveredPostId] = React.useState< + number | null + >(null) + + // throttle the hovered post id to avoid excessive prefetches + const [throttledHoveredPostId] = useThrottledValue(currentHoveredPostId, { + wait: 100, // adjust this value to see the difference + }) + + // perform the prefetch when the throttled hovered post id changes + useEffect(() => { + if (throttledHoveredPostId) { + queryClient.ensureQueryData({ + queryKey: ['post', throttledHoveredPostId], + queryFn: () => fetchPost(throttledHoveredPostId), + }) + } + }, [throttledHoveredPostId]) + + const handleMouseEnter = (postId: number) => { + setCurrentHoveredPostId(postId) // update the hovered post id + } + + if (isLoading) return
Loading posts...
+ + return ( + + ) +} + +function PostDetail({ postId }: { postId: number }) { + const { data: post, isLoading } = useQuery({ + queryKey: ['post', postId], + queryFn: () => fetchPost(postId), + }) + + if (isLoading) return
Loading post...
+ + return ( +
+

{post?.title}

+

{post?.body}

+
+ ) +} + +function App() { + const [selectedPostId, setSelectedPostId] = React.useState( + null, + ) + + return ( +
+

TanStack Pacer/Query Throttled Prefetch Example

+

Hover over a post title to prefetch its content

+

+ This example shows how to prefetch a query when the user hovers over a + post. +

+

+ The throttled query key is created after a throttle to avoid excessive + prefetches. +

+
+ + {selectedPostId && } +
+
+ ) +} + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10_000, + }, + }, +}) + +const root = ReactDOM.createRoot(document.getElementById('root')!) +root.render( + + + + , +) diff --git a/examples/react/react-query-throttled-prefetch/tsconfig.json b/examples/react/react-query-throttled-prefetch/tsconfig.json new file mode 100644 index 000000000..6e9088d67 --- /dev/null +++ b/examples/react/react-query-throttled-prefetch/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/react-query-throttled-prefetch/vite.config.ts b/examples/react/react-query-throttled-prefetch/vite.config.ts new file mode 100644 index 000000000..4e1943662 --- /dev/null +++ b/examples/react/react-query-throttled-prefetch/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/throttle/package.json b/examples/react/throttle/package.json index 9d768fb50..8d5e0cfce 100644 --- a/examples/react/throttle/package.json +++ b/examples/react/throttle/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/throttle/src/index.tsx b/examples/react/throttle/src/index.tsx index 74efa7bbf..c743ec3bf 100644 --- a/examples/react/throttle/src/index.tsx +++ b/examples/react/throttle/src/index.tsx @@ -69,6 +69,7 @@ function App2() {

TanStack Pacer throttle Example 2

{ // optional error handler @@ -58,19 +59,12 @@ function App() { // get and name our debounced function const handleSearchDebounced = setSearchAsyncDebouncer.maybeExecute - useEffect(() => { - console.log('mount') - return () => { - console.log('unmount') - setSearchAsyncDebouncer.cancel() // cancel any pending async calls when the component unmounts - } - }, []) - // instant event handler that calls both the instant local state setter and the debounced function async function onSearchChange(e: React.ChangeEvent) { const newTerm = e.target.value setSearchTerm(newTerm) - await handleSearchDebounced(newTerm) // optionally await if you need to + const result = await handleSearchDebounced(newTerm) // optionally await result if you need to + console.log('result', result) } return ( @@ -78,6 +72,7 @@ function App() {

TanStack Pacer useAsyncDebouncer Example

{error &&
Error: {error.message}
}
-

API calls made: {setSearchAsyncDebouncer.getExecutionCount()}

+

API calls made: {setSearchAsyncDebouncer.getSuccessCount()}

{results.length > 0 && (
    {results.map((item) => ( diff --git a/examples/react/useAsyncQueuedState/.eslintrc.cjs b/examples/react/useAsyncQueuedState/.eslintrc.cjs new file mode 100644 index 000000000..9ff0b9fc9 --- /dev/null +++ b/examples/react/useAsyncQueuedState/.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/useAsyncQueuedState/.gitignore b/examples/react/useAsyncQueuedState/.gitignore new file mode 100644 index 000000000..4673b022e --- /dev/null +++ b/examples/react/useAsyncQueuedState/.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* diff --git a/examples/react/useAsyncQueuedState/README.md b/examples/react/useAsyncQueuedState/README.md new file mode 100644 index 000000000..1cf889265 --- /dev/null +++ b/examples/react/useAsyncQueuedState/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/react/useAsyncQueuedState/index.html b/examples/react/useAsyncQueuedState/index.html new file mode 100644 index 000000000..701aa26e3 --- /dev/null +++ b/examples/react/useAsyncQueuedState/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Pacer Example + + + +
    + + + diff --git a/examples/react/useAsyncQueuerState/package.json b/examples/react/useAsyncQueuedState/package.json similarity index 97% rename from examples/react/useAsyncQueuerState/package.json rename to examples/react/useAsyncQueuedState/package.json index 4fe3eecc6..673c34174 100644 --- a/examples/react/useAsyncQueuerState/package.json +++ b/examples/react/useAsyncQueuedState/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useAsyncQueuedState/public/emblem-light.svg b/examples/react/useAsyncQueuedState/public/emblem-light.svg new file mode 100644 index 000000000..a58e69ad5 --- /dev/null +++ b/examples/react/useAsyncQueuedState/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/useAsyncQueuerState/src/index.tsx b/examples/react/useAsyncQueuedState/src/index.tsx similarity index 94% rename from examples/react/useAsyncQueuerState/src/index.tsx rename to examples/react/useAsyncQueuedState/src/index.tsx index 6516bb3c4..f4985ee01 100644 --- a/examples/react/useAsyncQueuerState/src/index.tsx +++ b/examples/react/useAsyncQueuedState/src/index.tsx @@ -1,5 +1,5 @@ import ReactDOM from 'react-dom/client' -import { useAsyncQueuerState } from '@tanstack/react-pacer/async-queuer' +import { useAsyncQueuedState } from '@tanstack/react-pacer/async-queuer' import { useState } from 'react' type AsyncTask = () => Promise @@ -12,13 +12,14 @@ function App() { const [, rerender] = useState(0) // demo - rerender when start/stop changes // Queuer that uses React.useState under the hood - const [queueItems, queuer] = useAsyncQueuerState({ + const [queueItems, queuer] = useAsyncQueuedState({ maxSize: 25, initialItems: Array.from({ length: 10 }, (_, i) => async () => { await new Promise((resolve) => setTimeout(resolve, fakeWaitTime)) return `Initial Task ${i + 1}` }), concurrency: concurrency, // Process 2 items concurrently + started: false, wait: 100, // for demo purposes - usually you would not want extra wait time unless you are throttling onIsRunningChange: (_asyncQueuer) => { rerender((prev) => prev + 1) @@ -39,7 +40,7 @@ function App() { return (
    -

    TanStack Pacer useAsyncQueuerState Example

    +

    TanStack Pacer useAsyncQueuedState Example

    Queue Size: {queuer.getSize()}
    Queue Max Size: {25}
    diff --git a/examples/react/useAsyncQueuedState/tsconfig.json b/examples/react/useAsyncQueuedState/tsconfig.json new file mode 100644 index 000000000..6e9088d67 --- /dev/null +++ b/examples/react/useAsyncQueuedState/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/useAsyncQueuedState/vite.config.ts b/examples/react/useAsyncQueuedState/vite.config.ts new file mode 100644 index 000000000..4e1943662 --- /dev/null +++ b/examples/react/useAsyncQueuedState/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/useAsyncQueuer/package.json b/examples/react/useAsyncQueuer/package.json index 27e1a6209..b76aece0c 100644 --- a/examples/react/useAsyncQueuer/package.json +++ b/examples/react/useAsyncQueuer/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useAsyncQueuer/src/index.tsx b/examples/react/useAsyncQueuer/src/index.tsx index eefa8dc23..5c3436def 100644 --- a/examples/react/useAsyncQueuer/src/index.tsx +++ b/examples/react/useAsyncQueuer/src/index.tsx @@ -20,6 +20,7 @@ function App() { return `Initial Task ${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: (asyncQueuer) => { setQueueItems(asyncQueuer.getAllItems()) diff --git a/examples/react/useAsyncRateLimiter/package.json b/examples/react/useAsyncRateLimiter/package.json index ab19bc6e5..07e22f26b 100644 --- a/examples/react/useAsyncRateLimiter/package.json +++ b/examples/react/useAsyncRateLimiter/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useAsyncRateLimiter/src/index.tsx b/examples/react/useAsyncRateLimiter/src/index.tsx index 5e20484f1..f1e49a94a 100644 --- a/examples/react/useAsyncRateLimiter/src/index.tsx +++ b/examples/react/useAsyncRateLimiter/src/index.tsx @@ -41,7 +41,7 @@ function App() { setIsLoading(false) setError(null) - console.log(setSearchAsyncRateLimiter.getExecutionCount()) + console.log(setSearchAsyncRateLimiter.getSuccessCount()) } // hook that gives you an async rate limiter instance @@ -79,6 +79,7 @@ function App() {

    TanStack Pacer useAsyncRateLimiter Example

    {error &&
    Error: {error.message}
    }
    -

    API calls made: {setSearchAsyncRateLimiter.getExecutionCount()}

    +

    API calls made: {setSearchAsyncRateLimiter.getSuccessCount()}

    {results.length > 0 && (
      {results.map((item) => ( diff --git a/examples/react/useAsyncThrottler/package.json b/examples/react/useAsyncThrottler/package.json index 5888f9734..5109524ce 100644 --- a/examples/react/useAsyncThrottler/package.json +++ b/examples/react/useAsyncThrottler/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useAsyncThrottler/src/index.tsx b/examples/react/useAsyncThrottler/src/index.tsx index 17bd87335..c5d28640e 100644 --- a/examples/react/useAsyncThrottler/src/index.tsx +++ b/examples/react/useAsyncThrottler/src/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import ReactDOM from 'react-dom/client' import { useAsyncThrottler } from '@tanstack/react-pacer/async-throttler' @@ -20,7 +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 throttled @@ -32,20 +31,17 @@ 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(setSearchAsyncThrottler.getExecutionCount()) + return data // this could alternatively be a void function without a return } // hook that gives you an async throttler instance const setSearchAsyncThrottler = useAsyncThrottler(handleSearch, { + // leading: false, + // trailing: false, wait: 1000, // Wait 1 second between API calls onError: (error) => { // optional error handler @@ -58,19 +54,12 @@ function App() { // get and name our throttled function const handleSearchThrottled = setSearchAsyncThrottler.maybeExecute - useEffect(() => { - console.log('mount') - return () => { - console.log('unmount') - setSearchAsyncThrottler.cancel() // cancel any pending async calls when the component unmounts - } - }, []) - // instant event handler that calls both the instant local state setter and the throttled function async function onSearchChange(e: React.ChangeEvent) { const newTerm = e.target.value setSearchTerm(newTerm) - await handleSearchThrottled(newTerm) // optionally await if you need to + const result = await handleSearchThrottled(newTerm) // optionally await if you need to + console.log('result', result) } return ( @@ -78,6 +67,7 @@ function App() {

      TanStack Pacer useAsyncThrottler Example

      {error &&
      Error: {error.message}
      }
      -

      API calls made: {setSearchAsyncThrottler.getExecutionCount()}

      +

      API calls made: {setSearchAsyncThrottler.getSuccessCount()}

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

      Loading...

      } + {setSearchAsyncThrottler.getIsPending() ? ( +

      Pending...

      + ) : setSearchAsyncThrottler.getIsExecuting() ? ( +

      Executing...

      + ) : null}
      ) diff --git a/examples/react/useDebouncedCallback/package.json b/examples/react/useDebouncedCallback/package.json index 05aa4dade..1a48fae4b 100644 --- a/examples/react/useDebouncedCallback/package.json +++ b/examples/react/useDebouncedCallback/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useDebouncedCallback/src/index.tsx b/examples/react/useDebouncedCallback/src/index.tsx index 167965515..fb84ab318 100644 --- a/examples/react/useDebouncedCallback/src/index.tsx +++ b/examples/react/useDebouncedCallback/src/index.tsx @@ -10,7 +10,8 @@ function App1() { // Create debounced setter function - Stable reference provided by useDebouncedCallback const debouncedSetCount = useDebouncedCallback(setDebouncedCount, { wait: 500, - enabled: instantCount > 2, + // enabled: instantCount > 2, // optional, defaults to true + // leading: true, // optional, defaults to false }) function increment() { @@ -65,6 +66,7 @@ function App2() {

      TanStack Pacer useDebouncedCallback Example 2

      2, // optional, defaults to true + // leading: true, // optional, defaults to false }, ) @@ -30,13 +31,22 @@ function App1() { - - + + + + + + + + + @@ -62,7 +72,7 @@ function App2() { instantSearch, { wait: 500, - // enabled: instantSearch.length > 2, // optional, defaults to true + enabled: instantSearch.length > 2, // optional, defaults to true }, ) @@ -77,6 +87,7 @@ function App2() {

      TanStack Pacer useDebouncedState Example 2

      - - + + + + + + + + + diff --git a/examples/react/useDebouncedValue/package.json b/examples/react/useDebouncedValue/package.json index 8ef9bf167..2b6f6081d 100644 --- a/examples/react/useDebouncedValue/package.json +++ b/examples/react/useDebouncedValue/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useDebouncedValue/src/index.tsx b/examples/react/useDebouncedValue/src/index.tsx index f2707b87e..67806e735 100644 --- a/examples/react/useDebouncedValue/src/index.tsx +++ b/examples/react/useDebouncedValue/src/index.tsx @@ -10,10 +10,10 @@ function App1() { } // highest-level hook that watches an instant local state value and returns a debounced value - // optionally, grab the debouncer from the last index of the returned array - const [debouncedCount, debouncer] = useDebouncedValue(instantCount, { + const [debouncedCount] = useDebouncedValue(instantCount, { wait: 500, // enabled: instantCount > 2, // optional, defaults to true + // leading: true, // optional, defaults to false }) return ( @@ -21,14 +21,6 @@ function App1() {

      TanStack Pacer useDebouncedValue Example 1

      Execution Count:{debouncer.getExecutionCount()}Enabled:{debouncer.getOptions().enabled.toString()}
      Is Pending: {debouncer.getIsPending().toString()}
      Execution Count:{debouncer.getExecutionCount()}
      +
      +
      Instant Count: {instantCount}
      Execution Count:{debouncer.getExecutionCount()}Enabled:{debouncer.getOptions().enabled.toString()}
      Is Pending: {debouncer.getIsPending().toString()}
      Execution Count:{debouncer.getExecutionCount()}
      +
      +
      Instant Search: {instantSearch}
      - - - - - - - - @@ -50,9 +42,10 @@ function App2() { const [instantSearch, setInstantSearch] = useState('') // highest-level hook that watches an instant local state value and returns a debounced value - const [debouncedSearch, debouncer] = useDebouncedValue(instantSearch, { + // optionally, grab the debouncer from the last index of the returned array + const [debouncedSearch] = useDebouncedValue(instantSearch, { wait: 500, - // enabled: instantSearch.length > 2, // optional, defaults to true + enabled: instantSearch.length > 2, // optional, defaults to true }) function handleSearchChange(e: React.ChangeEvent) { @@ -64,6 +57,7 @@ function App2() {

      TanStack Pacer useDebouncedValue Example 2

      Execution Count:{debouncer.getExecutionCount()}
      Is Pending:{debouncer.getIsPending().toString()}
      Instant Count: {instantCount}
      - - - - - - - - diff --git a/examples/react/useDebouncer/package.json b/examples/react/useDebouncer/package.json index 780d18498..34898c4fa 100644 --- a/examples/react/useDebouncer/package.json +++ b/examples/react/useDebouncer/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useDebouncer/src/index.tsx b/examples/react/useDebouncer/src/index.tsx index 2fb05da1d..437dd25b8 100644 --- a/examples/react/useDebouncer/src/index.tsx +++ b/examples/react/useDebouncer/src/index.tsx @@ -10,7 +10,8 @@ function App1() { // Lower-level useDebouncer hook - requires you to manage your own state const setCountDebouncer = useDebouncer(setDebouncedCount, { wait: 500, - enabled: instantCount > 2, // optional, defaults to true + // enabled: instantCount > 2, // optional, defaults to true + // leading: true, // optional, defaults to false }) function increment() { @@ -28,13 +29,22 @@ function App1() {
      Execution Count:{debouncer.getExecutionCount()}
      Is Pending:{debouncer.getIsPending().toString()}
      Instant Search: {instantSearch}
      - - + + + + + + + + + @@ -55,15 +65,11 @@ function App1() { function App2() { const [searchText, setSearchText] = useState('') const [debouncedSearchText, setDebouncedSearchText] = useState('') - const [leading, setLeading] = useState(false) - const [trailing, setTrailing] = useState(true) // Lower-level useDebouncer hook - requires you to manage your own state const setSearchDebouncer = useDebouncer(setDebouncedSearchText, { wait: 500, enabled: searchText.length > 2, // optional, defaults to true - leading, - trailing, }) function handleSearchChange(e: React.ChangeEvent) { @@ -77,41 +83,33 @@ function App2() {

      TanStack Pacer useDebouncer Example 2

      setLeading((t) => !t)} - /> - - setTrailing((t) => !t)} - /> - -
      -
      -
      Execution Count:{setCountDebouncer.getExecutionCount()}Enabled:{setCountDebouncer.getOptions().enabled.toString()}
      Is Pending: {setCountDebouncer.getIsPending().toString()}
      Execution Count:{setCountDebouncer.getExecutionCount()}
      +
      +
      Instant Count: {instantCount}
      - - + + + + + + + + + diff --git a/examples/react/useQueuedState/.eslintrc.cjs b/examples/react/useQueuedState/.eslintrc.cjs new file mode 100644 index 000000000..9ff0b9fc9 --- /dev/null +++ b/examples/react/useQueuedState/.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/useQueuedState/.gitignore b/examples/react/useQueuedState/.gitignore new file mode 100644 index 000000000..4673b022e --- /dev/null +++ b/examples/react/useQueuedState/.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* diff --git a/examples/react/useQueuedState/README.md b/examples/react/useQueuedState/README.md new file mode 100644 index 000000000..1cf889265 --- /dev/null +++ b/examples/react/useQueuedState/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/react/useQueuedState/index.html b/examples/react/useQueuedState/index.html new file mode 100644 index 000000000..701aa26e3 --- /dev/null +++ b/examples/react/useQueuedState/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Pacer Example + + + +
      + + + diff --git a/examples/react/useQueuerState/package.json b/examples/react/useQueuedState/package.json similarity index 88% rename from examples/react/useQueuerState/package.json rename to examples/react/useQueuedState/package.json index 58c9c4fbe..c62e83fe1 100644 --- a/examples/react/useQueuerState/package.json +++ b/examples/react/useQueuedState/package.json @@ -1,5 +1,5 @@ { - "name": "@tanstack/pacer-example-react-use-queuer-state", + "name": "@tanstack/pacer-example-react-use-queued-state", "private": true, "type": "module", "scripts": { @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useQueuedState/public/emblem-light.svg b/examples/react/useQueuedState/public/emblem-light.svg new file mode 100644 index 000000000..a58e69ad5 --- /dev/null +++ b/examples/react/useQueuedState/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/useQueuerState/src/index.tsx b/examples/react/useQueuedState/src/index.tsx similarity index 90% rename from examples/react/useQueuerState/src/index.tsx rename to examples/react/useQueuedState/src/index.tsx index 1324a79c6..09583710e 100644 --- a/examples/react/useQueuerState/src/index.tsx +++ b/examples/react/useQueuedState/src/index.tsx @@ -1,17 +1,18 @@ import ReactDOM from 'react-dom/client' -import { useQueuerState } from '@tanstack/react-pacer/queuer' +import { useQueuedState } from '@tanstack/react-pacer/queuer' function App() { // Queuer that uses React.useState under the hood - const [queueItems, queuer] = useQueuerState({ + const [queueItems, addItem, queuer] = useQueuedState({ maxSize: 25, initialItems: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + started: false, wait: 1000, // wait 1 second between processing items - wait is optional! }) return (
      -

      TanStack Pacer useQueuerState Example

      +

      TanStack Pacer useQueuedState Example

      Queue Size: {queuer.getSize()}
      Queue Max Size: {25}
      Queue Full: {queuer.getIsFull() ? 'Yes' : 'No'}
      @@ -35,7 +36,7 @@ function App() { const nextNumber = queueItems.length ? queueItems[queueItems.length - 1] + 1 : 1 - queuer.addItem(nextNumber) + addItem(nextNumber) }} disabled={queuer.getIsFull()} > diff --git a/examples/react/useQueuedState/tsconfig.json b/examples/react/useQueuedState/tsconfig.json new file mode 100644 index 000000000..6e9088d67 --- /dev/null +++ b/examples/react/useQueuedState/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/useQueuedState/vite.config.ts b/examples/react/useQueuedState/vite.config.ts new file mode 100644 index 000000000..4e1943662 --- /dev/null +++ b/examples/react/useQueuedState/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/useQueuedValue/.eslintrc.cjs b/examples/react/useQueuedValue/.eslintrc.cjs new file mode 100644 index 000000000..9ff0b9fc9 --- /dev/null +++ b/examples/react/useQueuedValue/.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/useQueuedValue/.gitignore b/examples/react/useQueuedValue/.gitignore new file mode 100644 index 000000000..4673b022e --- /dev/null +++ b/examples/react/useQueuedValue/.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* diff --git a/examples/react/useQueuedValue/README.md b/examples/react/useQueuedValue/README.md new file mode 100644 index 000000000..1cf889265 --- /dev/null +++ b/examples/react/useQueuedValue/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/react/useQueuedValue/index.html b/examples/react/useQueuedValue/index.html new file mode 100644 index 000000000..701aa26e3 --- /dev/null +++ b/examples/react/useQueuedValue/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Pacer Example + + + +
      + + + diff --git a/examples/react/useQueuedValue/package.json b/examples/react/useQueuedValue/package.json new file mode 100644 index 000000000..6efeed78b --- /dev/null +++ b/examples/react/useQueuedValue/package.json @@ -0,0 +1,34 @@ +{ + "name": "@tanstack/pacer-example-react-use-queued-value", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3005", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/react-pacer": "^0.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "vite": "^6.3.3" + }, + "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/useQueuedValue/public/emblem-light.svg b/examples/react/useQueuedValue/public/emblem-light.svg new file mode 100644 index 000000000..a58e69ad5 --- /dev/null +++ b/examples/react/useQueuedValue/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/useQueuedValue/src/index.tsx b/examples/react/useQueuedValue/src/index.tsx new file mode 100644 index 000000000..55f89696f --- /dev/null +++ b/examples/react/useQueuedValue/src/index.tsx @@ -0,0 +1,74 @@ +import ReactDOM from 'react-dom/client' +import { useQueuedValue } from '@tanstack/react-pacer/queuer' +import { useState } from 'react' + +function App() { + const [instantSearchValue, setInstantSearchValue] = useState('') + + // 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 + }) + + return ( +
      +

      TanStack Pacer useQueuedValue Example

      +
      Current Value: {value}
      +
      +
      Queue Size: {queuer.getSize()}
      +
      Queue Full: {queuer.getIsFull() ? 'Yes' : 'No'}
      +
      Queue Peek: {queuer.getPeek()}
      +
      Queue Empty: {queuer.getIsEmpty() ? 'Yes' : 'No'}
      +
      Queue Idle: {queuer.getIsIdle() ? 'Yes' : 'No'}
      +
      Queuer Status: {queuer.getIsRunning() ? 'Running' : 'Stopped'}
      +
      Items Processed: {queuer.getExecutionCount()}
      +
      Queue Items: {queuer.getAllItems().join(', ')}
      +
      + { + setInstantSearchValue(e.target.value) // instantly update the local search value + }} + placeholder="Enter search term..." + disabled={queuer.getIsFull()} + /> + + + + + +
      +
      + ) +} + +const root = ReactDOM.createRoot(document.getElementById('root')!) +root.render() diff --git a/examples/react/useQueuedValue/tsconfig.json b/examples/react/useQueuedValue/tsconfig.json new file mode 100644 index 000000000..6e9088d67 --- /dev/null +++ b/examples/react/useQueuedValue/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/useQueuedValue/vite.config.ts b/examples/react/useQueuedValue/vite.config.ts new file mode 100644 index 000000000..4e1943662 --- /dev/null +++ b/examples/react/useQueuedValue/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/useQueuer/package.json b/examples/react/useQueuer/package.json index 54f3e7acc..32a8822c1 100644 --- a/examples/react/useQueuer/package.json +++ b/examples/react/useQueuer/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useQueuer/src/index.tsx b/examples/react/useQueuer/src/index.tsx index a25b34bb9..69dbf7c4b 100644 --- a/examples/react/useQueuer/src/index.tsx +++ b/examples/react/useQueuer/src/index.tsx @@ -9,6 +9,7 @@ function App() { const queuer = useQueuer({ maxSize: 25, initialItems: queueItems, + started: false, wait: 1000, // wait 1 second between processing items - wait is optional! onItemsChange: (queue) => { setQueueItems(queue.getAllItems()) diff --git a/examples/react/useRateLimitedCallback/package.json b/examples/react/useRateLimitedCallback/package.json index e8f6db129..36af4c7a8 100644 --- a/examples/react/useRateLimitedCallback/package.json +++ b/examples/react/useRateLimitedCallback/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useRateLimitedCallback/src/index.tsx b/examples/react/useRateLimitedCallback/src/index.tsx index 091b01580..eb9df10dd 100644 --- a/examples/react/useRateLimitedCallback/src/index.tsx +++ b/examples/react/useRateLimitedCallback/src/index.tsx @@ -80,6 +80,7 @@ function App2() {

      TanStack Pacer useRateLimitedCallback Example 2

      TanStack Pacer useRateLimitedState Example 2
      2, // optional, defaults to true limit: 5, window: 5000, @@ -26,14 +27,6 @@ function App1() {

      TanStack Pacer useRateLimitedValue Example 1

      Execution Count:{setSearchDebouncer.getExecutionCount()}Enabled:{setSearchDebouncer.getOptions().enabled.toString()}
      Is Pending: {setSearchDebouncer.getIsPending().toString()}
      Execution Count:{setSearchDebouncer.getExecutionCount()}
      +
      +
      Instant Search: {searchText}
      - - - - - - - - @@ -46,10 +39,6 @@ function App1() {
      Execution Count:{rateLimiter.getExecutionCount()}
      Rejection Count:{rateLimiter.getRejectionCount()}
      Instant Count: {instantCount}
      - -
      ) @@ -59,7 +48,7 @@ function App2() { const [instantSearch, setInstantSearch] = useState('') // Using useRateLimitedValue with a rate limit of 5 executions per 5 seconds - const [limitedSearch, rateLimiter] = useRateLimitedValue(instantSearch, { + const [limitedSearch] = useRateLimitedValue(instantSearch, { // enabled: instantSearch.length > 2, // optional, defaults to true limit: 5, window: 5000, @@ -79,6 +68,7 @@ function App2() {

      TanStack Pacer useRateLimitedValue Example 2

      - - - - - - - - @@ -106,12 +88,6 @@ function App2() {
      Execution Count:{rateLimiter.getExecutionCount()}
      Rejection Count:{rateLimiter.getRejectionCount()}
      Instant Search: {instantSearch}
      -
      - - -
      ) } diff --git a/examples/react/useRateLimiter/package.json b/examples/react/useRateLimiter/package.json index d3ea666d8..2ac2e4694 100644 --- a/examples/react/useRateLimiter/package.json +++ b/examples/react/useRateLimiter/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useRateLimiter/src/index.tsx b/examples/react/useRateLimiter/src/index.tsx index 94cdf0f3e..67661ef90 100644 --- a/examples/react/useRateLimiter/src/index.tsx +++ b/examples/react/useRateLimiter/src/index.tsx @@ -89,6 +89,7 @@ function App2() {

      TanStack Pacer useRateLimiter Example 2

      TanStack Pacer useThrottledCallback Example 2
      TanStack Pacer useThrottledState Example 2
      2, // optional, defaults to true }) @@ -21,10 +21,6 @@ function App1() {

      TanStack Pacer useThrottledValue Example 1

      - - - - @@ -46,7 +42,7 @@ function App2() { const [instantSearch, setInstantSearch] = useState('') // highest-level hook that watches an instant local state value and returns a throttled value - const [throttledSearch, throttler] = useThrottledValue(instantSearch, { + const [throttledSearch] = useThrottledValue(instantSearch, { wait: 1000, // enabled: instantSearch.length > 2, // optional, defaults to true }) @@ -60,6 +56,7 @@ function App2() {

      TanStack Pacer useThrottledValue Example 2

      Execution Count:{throttler.getExecutionCount()}
      Instant Count: {instantCount}
      - - - - diff --git a/examples/react/useThrottler/package.json b/examples/react/useThrottler/package.json index 0c3773a51..7bbde675e 100644 --- a/examples/react/useThrottler/package.json +++ b/examples/react/useThrottler/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", - "vite": "^6.3.2" + "vite": "^6.3.3" }, "browserslist": { "production": [ diff --git a/examples/react/useThrottler/src/index.tsx b/examples/react/useThrottler/src/index.tsx index 90e9b77a6..7491bee79 100644 --- a/examples/react/useThrottler/src/index.tsx +++ b/examples/react/useThrottler/src/index.tsx @@ -10,7 +10,9 @@ function App1() { // Lower-level useThrottler hook - requires you to manage your own state const setCountThrottler = useThrottler(setThrottledCount, { wait: 1000, - enabled: instantCount > 2, + leading: false, + // trailing: true, + // enabled: instantCount > 2, }) function increment() { @@ -69,6 +71,7 @@ function App2() {

      TanStack Pacer useThrottler Example 2

      TanStack Pacer asyncDebounce Example
      { @@ -68,11 +69,7 @@ function SearchApp() {
      Execution Count:{throttler.getExecutionCount()}
      Instant Search: {instantSearch}

      Search Results:

      -
        - {searchResults().map((result) => ( -
      • {result}
      • - ))} -
      + {(result) =>
    • {result}
    • }
      ) diff --git a/examples/solid/asyncRateLimit/package.json b/examples/solid/asyncRateLimit/package.json index fed5bec70..04014dcf5 100644 --- a/examples/solid/asyncRateLimit/package.json +++ b/examples/solid/asyncRateLimit/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/asyncRateLimit/src/index.tsx b/examples/solid/asyncRateLimit/src/index.tsx index 4799df63d..03e9da120 100644 --- a/examples/solid/asyncRateLimit/src/index.tsx +++ b/examples/solid/asyncRateLimit/src/index.tsx @@ -1,4 +1,4 @@ -import { createSignal } from 'solid-js' +import { For, createSignal } from 'solid-js' import { render } from 'solid-js/web' import { asyncRateLimit } from '@tanstack/solid-pacer/async-rate-limiter' @@ -46,6 +46,7 @@ function SearchApp() {

      TanStack Pacer asyncRateLimit Example

      { @@ -74,9 +75,7 @@ function SearchApp() {

      Search Results:

        - {searchResults().map((result) => ( -
      • {result}
      • - ))} + {(result) =>
      • {result}
      • }
      diff --git a/examples/solid/asyncThrottle/package.json b/examples/solid/asyncThrottle/package.json index 78483ef27..65d3b5a02 100644 --- a/examples/solid/asyncThrottle/package.json +++ b/examples/solid/asyncThrottle/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/asyncThrottle/src/index.tsx b/examples/solid/asyncThrottle/src/index.tsx index 942e8e30a..6d06a3f22 100644 --- a/examples/solid/asyncThrottle/src/index.tsx +++ b/examples/solid/asyncThrottle/src/index.tsx @@ -1,4 +1,4 @@ -import { createSignal } from 'solid-js' +import { For, createSignal } from 'solid-js' import { render } from 'solid-js/web' import { asyncThrottle } from '@tanstack/solid-pacer/async-throttler' @@ -41,6 +41,7 @@ function SearchApp() {

      TanStack Pacer asyncThrottle Example

      { @@ -69,9 +70,7 @@ function SearchApp() {

      Search Results:

        - {searchResults().map((result) => ( -
      • {result}
      • - ))} + {(result) =>
      • {result}
      • }
      diff --git a/examples/solid/createAsyncDebouncer/package.json b/examples/solid/createAsyncDebouncer/package.json index b8e3f046e..81a256ddc 100644 --- a/examples/solid/createAsyncDebouncer/package.json +++ b/examples/solid/createAsyncDebouncer/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/createAsyncDebouncer/src/index.tsx b/examples/solid/createAsyncDebouncer/src/index.tsx index 5d27d7137..7412c719d 100644 --- a/examples/solid/createAsyncDebouncer/src/index.tsx +++ b/examples/solid/createAsyncDebouncer/src/index.tsx @@ -1,4 +1,4 @@ -import { createSignal } from 'solid-js' +import { For, createSignal } from 'solid-js' import { render } from 'solid-js/web' import { createAsyncDebouncer } from '@tanstack/solid-pacer/async-debouncer' @@ -22,7 +22,6 @@ function App() { const [results, setResults] = createSignal>([]) const [isLoading, setIsLoading] = createSignal(false) const [error, setError] = createSignal(null) - const [executionCount, setExecutionCount] = createSignal(0) // The function that will become debounced const handleSearch = async (term: string) => { @@ -38,15 +37,16 @@ function App() { } const data = await fakeApi(term) - setResults(data) + setResults(data) // option 1: set results immediately setIsLoading(false) setError(null) - console.log(setSearchAsyncDebouncer.executionCount()) + 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 @@ -54,9 +54,6 @@ function App() { setError(error as Error) setResults([]) }, - onExecute: (asyncDebouncer) => { - setExecutionCount(asyncDebouncer.getExecutionCount()) - }, }) // get and name our debounced function @@ -66,7 +63,8 @@ function App() { async function onSearchChange(e: Event) { const newTerm = (e.target as HTMLInputElement).value setSearchTerm(newTerm) - await handleSearchDebounced(newTerm) // optionally await if you need to + const result = await handleSearchDebounced(newTerm) // ^option 2: await results + console.log('result', result) // demo test to see awaited result } return ( @@ -74,6 +72,7 @@ function App() {

      TanStack Pacer createAsyncDebouncer Example

      {error() &&
      Error: {error()?.message}
      }
      -

      API calls made: {executionCount()}

      +

      API calls made: {setSearchAsyncDebouncer.successCount()}

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

      Loading...

      } diff --git a/examples/solid/createAsyncQueuer/package.json b/examples/solid/createAsyncQueuer/package.json index 160a7335e..4a1f44103 100644 --- a/examples/solid/createAsyncQueuer/package.json +++ b/examples/solid/createAsyncQueuer/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/createAsyncQueuer/src/index.tsx b/examples/solid/createAsyncQueuer/src/index.tsx index b4848304d..0c8de420f 100644 --- a/examples/solid/createAsyncQueuer/src/index.tsx +++ b/examples/solid/createAsyncQueuer/src/index.tsx @@ -1,4 +1,4 @@ -import { createSignal } from 'solid-js' +import { For, createSignal } from 'solid-js' import { render } from 'solid-js/web' import { createAsyncQueuer } from '@tanstack/solid-pacer/async-queuer' @@ -18,6 +18,7 @@ function App() { }), concurrency: concurrency(), // Process 2 items concurrently wait: 500, // for demo purposes - usually you would not want extra wait time unless you are throttling with concurrency + started: false, }) // Simulated async task @@ -56,11 +57,13 @@ function App() {
      Queue Items: - {queuer.allItems().map((task, index) => ( -
      - {index}: {task.toString()} -
      - ))} + + {(task, index) => ( +
      + {index()}: {task.toString()} +
      + )} +
      TanStack Pacer createAsyncRateLimiter Example
      {error() &&
      Error: {error()?.message}
      }
      -

      API calls made: {setSearchAsyncRateLimiter.executionCount()}

      +

      API calls made: {setSearchAsyncRateLimiter.successCount()}

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

      Loading...

      } diff --git a/examples/solid/createAsyncThrottler/package.json b/examples/solid/createAsyncThrottler/package.json index 000e5ad71..15706fe44 100644 --- a/examples/solid/createAsyncThrottler/package.json +++ b/examples/solid/createAsyncThrottler/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/createAsyncThrottler/src/index.tsx b/examples/solid/createAsyncThrottler/src/index.tsx index afdee07fc..d313d58f9 100644 --- a/examples/solid/createAsyncThrottler/src/index.tsx +++ b/examples/solid/createAsyncThrottler/src/index.tsx @@ -1,4 +1,4 @@ -import { createSignal } from 'solid-js' +import { For, createSignal } from 'solid-js' import { render } from 'solid-js/web' import { createAsyncThrottler } from '@tanstack/solid-pacer/async-throttler' @@ -20,7 +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 throttled @@ -32,16 +31,11 @@ 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) + setResults(data) // option 1: set results immediately setError(null) - console.log(setSearchAsyncThrottler.executionCount()) + return data // option 2: return data if you need to } // hook that gives you an async throttler instance @@ -62,7 +56,8 @@ function App() { async function onSearchChange(e: Event) { const newTerm = (e.target as HTMLInputElement).value setSearchTerm(newTerm) - await handleSearchThrottled(newTerm) // optionally await if you need to + const results = await handleSearchThrottled(newTerm) // optionally await if you need to + console.log('results', results) } return ( @@ -70,6 +65,7 @@ function App() {

      TanStack Pacer createAsyncThrottler Example

      {error() &&
      Error: {error()?.message}
      }
      -

      API calls made: {setSearchAsyncThrottler.executionCount()}

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

      Loading...

      } +

      API calls made: {setSearchAsyncThrottler.successCount()}

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

      Pending...

      + ) : setSearchAsyncThrottler.isExecuting() ? ( +

      Executing...

      + ) : null}
      ) diff --git a/examples/solid/createDebouncedSignal/package.json b/examples/solid/createDebouncedSignal/package.json index a0868d18c..8dbc55e00 100644 --- a/examples/solid/createDebouncedSignal/package.json +++ b/examples/solid/createDebouncedSignal/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/createDebouncedSignal/src/index.tsx b/examples/solid/createDebouncedSignal/src/index.tsx index 12c23dda2..5e4cd8ee8 100644 --- a/examples/solid/createDebouncedSignal/src/index.tsx +++ b/examples/solid/createDebouncedSignal/src/index.tsx @@ -11,6 +11,7 @@ function App1() { instantCount(), { wait: 500, + // leading: true, // optional, defaults to false }, ) @@ -32,6 +33,11 @@ function App1() { Execution Count: {debouncer.executionCount()} + + +
      + + Instant Count: {instantCount()} @@ -70,6 +76,7 @@ function App2() {

      TanStack Pacer createDebouncedSignal Example 2

      Execution Count: {debouncer.executionCount()} + + +
      + + Instant Search: {instantSearch()} diff --git a/examples/solid/createDebouncedValue/package.json b/examples/solid/createDebouncedValue/package.json index d9d57204f..4592148b8 100644 --- a/examples/solid/createDebouncedValue/package.json +++ b/examples/solid/createDebouncedValue/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/createDebouncedValue/src/index.tsx b/examples/solid/createDebouncedValue/src/index.tsx index 6ebc6abb3..906a6bde8 100644 --- a/examples/solid/createDebouncedValue/src/index.tsx +++ b/examples/solid/createDebouncedValue/src/index.tsx @@ -10,9 +10,9 @@ function App1() { } // highest-level hook that watches an instant local state value and returns a debounced value - // optionally, grab the debouncer from the last index of the returned array - const [debouncedCount, debouncer] = createDebouncedValue(instantCount, { + const [debouncedCount] = createDebouncedValue(instantCount, { wait: 500, + // leading: true, // optional, defaults to false }) return ( @@ -20,10 +20,6 @@ function App1() {

      TanStack Pacer createDebouncedValue Example 1

      - - - - @@ -45,7 +41,7 @@ function App2() { const [instantSearch, setInstantSearch] = createSignal('') // highest-level hook that watches an instant local state value and returns a debounced value - const [debouncedSearch, debouncer] = createDebouncedValue(instantSearch, { + const [debouncedSearch] = createDebouncedValue(instantSearch, { wait: 500, }) @@ -58,6 +54,7 @@ function App2() {

      TanStack Pacer createDebouncedValue Example 2

      Execution Count:{debouncer.executionCount()}
      Instant Count: {instantCount()}
      - - - - diff --git a/examples/solid/createDebouncer/package.json b/examples/solid/createDebouncer/package.json index abb42e692..de66915b1 100644 --- a/examples/solid/createDebouncer/package.json +++ b/examples/solid/createDebouncer/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/createDebouncer/src/index.tsx b/examples/solid/createDebouncer/src/index.tsx index e25fc8543..26fd0a2d7 100644 --- a/examples/solid/createDebouncer/src/index.tsx +++ b/examples/solid/createDebouncer/src/index.tsx @@ -10,6 +10,7 @@ function App1() { // Lower-level createDebouncer hook - requires you to manage your own state const setCountDebouncer = createDebouncer(setDebouncedCount, { wait: 500, + // leading: true, // optional, defaults to false }) // enable the debouncer when the instant count is greater than 2 @@ -35,6 +36,11 @@ function App1() { + + + @@ -79,6 +85,7 @@ function App2() {

      TanStack Pacer createDebouncer Example 2

      Execution Count:
      + + + diff --git a/examples/solid/createQueuer/package.json b/examples/solid/createQueuer/package.json index a9c88147d..b963a7aa9 100644 --- a/examples/solid/createQueuer/package.json +++ b/examples/solid/createQueuer/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/createRateLimitedSignal/package.json b/examples/solid/createRateLimitedSignal/package.json index dab910200..6a5f21170 100644 --- a/examples/solid/createRateLimitedSignal/package.json +++ b/examples/solid/createRateLimitedSignal/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/createRateLimitedSignal/src/index.tsx b/examples/solid/createRateLimitedSignal/src/index.tsx index 27be3ebe6..7d66650d5 100644 --- a/examples/solid/createRateLimitedSignal/src/index.tsx +++ b/examples/solid/createRateLimitedSignal/src/index.tsx @@ -79,6 +79,7 @@ function App2() {

      TanStack Pacer createRateLimitedState Example 2

      TanStack Pacer createRateLimitedValue Example 1
      Execution Count:{debouncer.executionCount()}
      Instant Search: {instantSearch()}Execution Count: {setCountDebouncer.executionCount()}
      +
      +
      Instant Count: {instantCount()} {setSearchDebouncer.executionCount()}
      +
      +
      Instant Search: {searchText()}
      - - - - - - - - @@ -40,10 +32,6 @@ function App1() {
      Execution Count:{rateLimiter.executionCount()}
      Rejection Count:{rateLimiter.rejectionCount()}
      Instant Count: {instantCount()}
      - -
      ) @@ -53,7 +41,7 @@ function App2() { const [instantSearch, setInstantSearch] = createSignal('') // Using createRateLimitedValue with a rate limit of 5 executions per 5 seconds - const [limitedSearch, rateLimiter] = createRateLimitedValue(instantSearch, { + const [limitedSearch] = createRateLimitedValue(instantSearch, { limit: 5, window: 5000, }) @@ -67,6 +55,7 @@ function App2() {

      TanStack Pacer createRateLimitedValue Example 2

      - - - - - - - - @@ -94,12 +75,6 @@ function App2() {
      Execution Count:{rateLimiter.executionCount()}
      Rejection Count:{rateLimiter.rejectionCount()}
      Instant Search: {instantSearch()}
      -
      - - -
      ) } diff --git a/examples/solid/createRateLimiter/package.json b/examples/solid/createRateLimiter/package.json index 566bed0a8..9ebefd769 100644 --- a/examples/solid/createRateLimiter/package.json +++ b/examples/solid/createRateLimiter/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/createRateLimiter/src/index.tsx b/examples/solid/createRateLimiter/src/index.tsx index 37dd675bf..69089143e 100644 --- a/examples/solid/createRateLimiter/src/index.tsx +++ b/examples/solid/createRateLimiter/src/index.tsx @@ -78,6 +78,7 @@ function App2() {

      TanStack Pacer createRateLimiter Example 2

      TanStack Pacer createThrottledSignal Example 2
      TanStack Pacer createThrottledValue Example 1 - - - - @@ -45,7 +41,7 @@ function App2() { const [instantSearch, setInstantSearch] = createSignal('') // highest-level hook that watches an instant local state value and returns a throttled value - const [throttledSearch, throttler] = createThrottledValue(instantSearch, { + const [throttledSearch] = createThrottledValue(instantSearch, { wait: 1000, }) @@ -58,6 +54,7 @@ function App2() {

      TanStack Pacer createThrottledValue Example 2

      Execution Count:{throttler.executionCount()}
      Instant Count: {instantCount()}
      - - - - diff --git a/examples/solid/createThrottler/package.json b/examples/solid/createThrottler/package.json index 04ba22162..bfb3d42a1 100644 --- a/examples/solid/createThrottler/package.json +++ b/examples/solid/createThrottler/package.json @@ -13,7 +13,7 @@ "solid-js": "^1.9.5" }, "devDependencies": { - "vite": "^6.3.2", + "vite": "^6.3.3", "vite-plugin-solid": "^2.11.6" }, "browserslist": { diff --git a/examples/solid/createThrottler/src/index.tsx b/examples/solid/createThrottler/src/index.tsx index 17b641ae4..e6a78f88c 100644 --- a/examples/solid/createThrottler/src/index.tsx +++ b/examples/solid/createThrottler/src/index.tsx @@ -83,6 +83,7 @@ function App2() {

      TanStack Pacer createThrottler Example 2

      TanStack Pacer debounce Example 2
      TanStack Pacer queue Example 2
      TanStack Pacer rateLimit Example 2
      TanStack Pacer throttle Example 2
      , -> { +export interface AsyncDebouncerOptions { /** * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions. * Defaults to true. @@ -20,11 +17,15 @@ export interface AsyncDebouncerOptions< /** * Optional error handler for when the debounced function throws */ - onError?: (error: unknown) => void + onError?: (error: unknown, debouncer: AsyncDebouncer) => void /** - * Optional function to call when the debounced function is executed + * Optional callback to call when the debounced function is executed */ - onExecute?: (debouncer: AsyncDebouncer) => void + onSettled?: (debouncer: AsyncDebouncer) => void + /** + * Optional callback to call when the debounced function is executed + */ + onSuccess?: (result: ReturnType, debouncer: AsyncDebouncer) => void /** * Whether to execute on the trailing edge of the timeout. * Defaults to true. @@ -37,12 +38,13 @@ export interface AsyncDebouncerOptions< wait: number } -const defaultOptions: Required> = { +const defaultOptions: Required> = { enabled: true, leading: false, - trailing: true, onError: () => {}, - onExecute: () => {}, + onSettled: () => {}, + onSuccess: () => {}, + trailing: true, wait: 0, } @@ -68,21 +70,22 @@ const defaultOptions: Required> = { * }); * ``` */ -export class AsyncDebouncer< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, -> { +export class AsyncDebouncer { private _abortController: AbortController | null = null - private _executionCount = 0 - private _isExecuting = false - private _lastArgs: TArgs | undefined - private _options: Required> - private _timeoutId: ReturnType | null = null private _canLeadingExecute = true + private _errorCount = 0 + private _isExecuting = false + private _isPending = false + private _lastArgs: Parameters | undefined + private _lastResult: ReturnType | undefined + private _options: Required> + private _settleCount = 0 + private _successCount = 0 + private _timeoutId: NodeJS.Timeout | null = null constructor( private fn: TFn, - initialOptions: AsyncDebouncerOptions, + initialOptions: AsyncDebouncerOptions, ) { this._options = { ...defaultOptions, @@ -94,20 +97,19 @@ export class AsyncDebouncer< * Updates the debouncer options * Returns the new options state */ - setOptions( - newOptions: Partial>, - ): Required> { - 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 } - return this._options } /** * Returns the current debouncer options */ - getOptions(): Required> { + getOptions(): Required> { return this._options } @@ -115,61 +117,65 @@ export class AsyncDebouncer< * Attempts to execute the debounced function * If a call is already in progress, it will be queued */ - async maybeExecute(...args: TArgs): Promise { - this.cancel() + async maybeExecute( + ...args: Parameters + ): Promise | undefined> { + this._cancel() this._lastArgs = args // Handle leading execution if (this._options.leading && this._canLeadingExecute) { this._canLeadingExecute = false await this.executeFunction(...args) + return this._lastResult + } + + // Handle trailing execution + if (this._options.trailing) { + this._isPending = true } return new Promise((resolve) => { this._timeoutId = setTimeout(async () => { - if (this._isExecuting) { - resolve() - return + // Execute trailing if enabled + if (this._options.trailing && this._lastArgs) { + await this.executeFunction(...this._lastArgs) } + // Reset state and resolve this._canLeadingExecute = true - // Execute trailing only if enabled - if (this._options.trailing) { - this._abortController = new AbortController() - try { - this._isExecuting = true - if (this._lastArgs) { - await this.executeFunction(...this._lastArgs) - } - } catch (error) { - try { - this._options.onError(error) - } catch { - console.error('Error in error handler', error) - } - } finally { - this._isExecuting = false - this._abortController = null - resolve() - } - } else { - resolve() - } + resolve(this._lastResult) }, this._options.wait) }) } - private async executeFunction(...args: TArgs): Promise { - if (!this._options.enabled) return - this._executionCount++ - await this.fn(...args) - this._options.onExecute(this) + private async executeFunction( + ...args: Parameters + ): Promise | undefined> { + if (!this._options.enabled) 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) + } catch (error) { + this._errorCount++ + this._options.onError(error, this) + } finally { + this._isExecuting = false + this._isPending = false + this._settleCount++ + this._abortController = null + this._options.onSettled(this) + } + return this._lastResult } /** - * Cancels any pending execution + * Cancel without resetting _canLeadingExecute */ - cancel(): void { + private _cancel(): void { if (this._timeoutId) { clearTimeout(this._timeoutId) this._timeoutId = null @@ -179,23 +185,58 @@ export class AsyncDebouncer< this._abortController = null } 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 + } + + /** + * Returns the number of times the function has settled (completed or errored) + */ + getSettleCount(): number { + return this._settleCount } /** - * Returns the number of times the function has been executed + * Returns the number of times the function has errored */ - getExecutionCount(): number { - return this._executionCount + getErrorCount(): number { + return this._errorCount } /** - * Returns `true` if there is a pending execution + * Returns `true` if there is a pending execution queued up for trailing execution */ getIsPending(): boolean { - return ( - this._options.enabled && (this._timeoutId !== null || this._isExecuting) - ) + return this._options.enabled && this._isPending + } + + /** + * Returns `true` if there is currently an execution in progress + */ + getIsExecuting(): boolean { + return this._isExecuting } } @@ -216,10 +257,10 @@ export class AsyncDebouncer< * await debounced("third"); // Executes after 1s * ``` */ -export function asyncDebounce< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, ->(fn: TFn, initialOptions: Omit, 'enabled'>) { +export function asyncDebounce( + fn: TFn, + initialOptions: Omit, 'enabled'>, +) { const asyncDebouncer = new AsyncDebouncer(fn, initialOptions) return asyncDebouncer.maybeExecute.bind(asyncDebouncer) } diff --git a/packages/pacer/src/async-queuer.ts b/packages/pacer/src/async-queuer.ts index 9c720b9ad..643b12a1d 100644 --- a/packages/pacer/src/async-queuer.ts +++ b/packages/pacer/src/async-queuer.ts @@ -10,6 +10,16 @@ export interface AsyncQueuerOptions { * Maximum number of concurrent tasks to process */ concurrency?: number + /** + * Maximum time in milliseconds that an item can stay in the queue + * If not provided, items will never expire + */ + expirationDuration?: number + /** + * Function to determine if an item has expired + * If provided, this overrides the expirationDuration behavior + */ + getIsExpired?: (item: () => Promise, addedAt: number) => boolean /** * Default position to get items from during processing * @default 'front' @@ -49,7 +59,11 @@ export interface AsyncQueuerOptions { */ onReject?: (item: () => Promise, queuer: AsyncQueuer) => void /** - * Whether the queuer should start processing tasks immediately + * Callback fired whenever an item expires in the queuer + */ + onExpire?: (item: () => Promise, queuer: AsyncQueuer) => void + /** + * Whether the queuer should start processing tasks immediately or not. */ started?: boolean /** @@ -61,6 +75,8 @@ export interface AsyncQueuerOptions { const defaultOptions: Required> = { addItemsTo: 'back', concurrency: 1, + expirationDuration: Infinity, + getIsExpired: () => false, getItemsFrom: 'front', getPriority: (item) => (item as any)?.priority ?? 0, initialItems: [], @@ -69,7 +85,8 @@ const defaultOptions: Required> = { onIsRunningChange: () => {}, onItemsChange: () => {}, onReject: () => {}, - started: false, + onExpire: () => {}, + started: true, wait: 0, } @@ -83,6 +100,7 @@ const defaultOptions: Required> = { * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior * - Pause/resume task processing * - Task cancellation + * - Item expiration to clear stale items from the queue * * Tasks are processed concurrently up to the configured concurrency limit. When a task completes, * the next pending task is processed if below the concurrency limit. @@ -107,7 +125,9 @@ export class AsyncQueuer { private _activeItems: Set<() => Promise> = new Set() private _executionCount = 0 private _rejectionCount = 0 + private _expirationCount = 0 private _items: Array<() => Promise> = [] + private _itemTimestamps: Array = [] private _onErrorCallbacks: Array<(error: Error) => void> = [] private _onSettledCallbacks: Array<(result: TValue | Error) => void> = [] private _onSuccessCallbacks: Array<(result: TValue) => void> = [] @@ -129,11 +149,8 @@ export class AsyncQueuer { * Updates the queuer options * Returns the new options state */ - setOptions( - newOptions: Partial>, - ): AsyncQueuerOptions { + setOptions(newOptions: Partial>): void { this._options = { ...this._options, ...newOptions } - return this._options } /** @@ -152,6 +169,9 @@ export class AsyncQueuer { return } + // Check for expired items + this.checkExpiredItems() + while ( this._activeItems.size < this._options.concurrency && !this.getIsEmpty() @@ -196,6 +216,56 @@ export class AsyncQueuer { this._pendingTick = false } + /** + * Checks for and removes expired items from the queuer + */ + 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) + } + } + /** * Starts the queuer and processes items */ @@ -295,15 +365,19 @@ export class AsyncQueuer { if (insertIndex === -1) { this._items.push(task) + this._itemTimestamps.push(Date.now()) } else { this._items.splice(insertIndex, 0, task) + this._itemTimestamps.splice(insertIndex, 0, Date.now()) } } else { // Default FIFO/LIFO behavior if (position === 'front') { this._items.unshift(task) + this._itemTimestamps.unshift(Date.now()) } else { this._items.push(task) + this._itemTimestamps.push(Date.now()) } } @@ -328,8 +402,10 @@ export class AsyncQueuer { if (position === 'front') { item = this._items.shift() + this._itemTimestamps.shift() } else { item = this._items.pop() + this._itemTimestamps.pop() } if (item !== undefined) { @@ -455,6 +531,13 @@ export class AsyncQueuer { ) } } + + /** + * Returns the number of items that have expired from the queuer + */ + getExpirationCount(): number { + return this._expirationCount + } } /** @@ -474,7 +557,9 @@ export class AsyncQueuer { * @param options - Configuration options for the AsyncQueuer * @returns A bound addItem function that can be used to add tasks to the queuer */ -export function asyncQueue(options: AsyncQueuerOptions = {}) { - const queuer = new AsyncQueuer({ ...options, started: true }) +export function asyncQueue( + options: Omit, 'started'> = {}, +) { + const queuer = new AsyncQueuer(options) return queuer.addItem.bind(queuer) } diff --git a/packages/pacer/src/async-rate-limiter.ts b/packages/pacer/src/async-rate-limiter.ts index f9445680f..c87575dd2 100644 --- a/packages/pacer/src/async-rate-limiter.ts +++ b/packages/pacer/src/async-rate-limiter.ts @@ -3,10 +3,7 @@ import type { AnyAsyncFunction } from './types' /** * Options for configuring an async rate-limited function */ -export interface AsyncRateLimiterOptions< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, -> { +export interface AsyncRateLimiterOptions { /** * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions. * Defaults to true. @@ -19,15 +16,22 @@ export interface AsyncRateLimiterOptions< /** * Optional error handler for when the rate-limited function throws */ - onError?: (error: unknown) => void + onError?: (error: unknown, rateLimiter: AsyncRateLimiter) => void /** * Optional function to call when the rate-limited function is executed */ - onExecute?: (rateLimiter: AsyncRateLimiter) => void + onSettled?: (rateLimiter: AsyncRateLimiter) => void + /** + * Optional function to call when the rate-limited function is executed + */ + onSuccess?: ( + result: ReturnType, + rateLimiter: AsyncRateLimiter, + ) => void /** * Optional callback function that is called when an execution is rejected due to rate limiting */ - onReject?: (rateLimiter: AsyncRateLimiter) => void + onReject?: (rateLimiter: AsyncRateLimiter) => void /** * Time window in milliseconds within which the limit applies */ @@ -35,12 +39,13 @@ export interface AsyncRateLimiterOptions< } const defaultOptions: Required< - Omit, 'limit' | 'window'> + Omit, 'limit' | 'window'> > = { enabled: true, - onReject: () => {}, onError: () => {}, - onExecute: () => {}, + onReject: () => {}, + onSettled: () => {}, + onSuccess: () => {}, } /** @@ -68,18 +73,18 @@ const defaultOptions: Required< * await rateLimiter.maybeExecute('123'); * ``` */ -export class AsyncRateLimiter< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, -> { - private _executionCount = 0 +export class AsyncRateLimiter { + private _options: AsyncRateLimiterOptions + private _errorCount = 0 private _executionTimes: Array = [] - private _options: AsyncRateLimiterOptions + private _lastResult: ReturnType | undefined private _rejectionCount = 0 + private _settleCount = 0 + private _successCount = 0 constructor( private fn: TFn, - initialOptions: AsyncRateLimiterOptions, + initialOptions: AsyncRateLimiterOptions, ) { this._options = { ...defaultOptions, @@ -91,21 +96,15 @@ export class AsyncRateLimiter< * Updates the rate limiter options * Returns the new options state */ - setOptions( - newOptions: Partial>, - ): AsyncRateLimiterOptions { - this._options = { - ...this._options, - ...newOptions, - } - return this._options + setOptions(newOptions: Partial>): void { + this._options = { ...this._options, ...newOptions } } /** * Returns the current rate limiter options */ - getOptions(): Required> { - return this._options as Required> + getOptions(): Required> { + return this._options as Required> } /** @@ -124,38 +123,40 @@ export class AsyncRateLimiter< * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected * ``` */ - async maybeExecute(...args: TArgs): Promise { + async maybeExecute( + ...args: Parameters + ): Promise | undefined> { this.cleanupOldExecutions() if (this._executionTimes.length < this._options.limit) { await this.executeFunction(...args) - return true + return this._lastResult } this.rejectFunction() - return false + return undefined } - private async executeFunction(...args: TArgs): Promise { + private async executeFunction( + ...args: Parameters + ): Promise | undefined> { if (!this._options.enabled) return const now = Date.now() - this._executionCount++ this._executionTimes.push(now) try { - await this.fn(...args) + this._lastResult = await this.fn(...args) + this._successCount++ + this._options.onSuccess?.(this._lastResult!, this) } catch (error) { - if (this._options.onError) { - try { - this._options.onError(error) - } catch (error) { - console.error('Error in onError handler', error) - } - } - throw error + this._errorCount++ + this._options.onError?.(error, this) } finally { - this._options.onExecute?.(this) + this._settleCount++ + this._options.onSettled?.(this) } + + return this._lastResult } private rejectFunction(): void { @@ -173,33 +174,47 @@ export class AsyncRateLimiter< ) } + /** + * Returns the number of remaining executions allowed in the current window + */ + getRemainingInWindow(): number { + this.cleanupOldExecutions() + return Math.max(0, this._options.limit - this._executionTimes.length) + } + + /** + * Returns the number of milliseconds until the next execution will be possible + */ + getMsUntilNextWindow(): number { + return this.getRemainingInWindow() * this._options.window + } + /** * Returns the number of times the function has been executed */ - getExecutionCount(): number { - return this._executionCount + getSuccessCount(): number { + return this._successCount } /** - * Returns the number of times the function has been rejected + * Returns the number of times the function has been settled */ - getRejectionCount(): number { - return this._rejectionCount + getSettleCount(): number { + return this._settleCount } /** - * Returns the number of remaining executions allowed in the current window + * Returns the number of times the function has errored */ - getRemainingInWindow(): number { - this.cleanupOldExecutions() - return Math.max(0, this._options.limit - this._executionTimes.length) + getErrorCount(): number { + return this._errorCount } /** - * Returns the number of milliseconds until the next execution will be possible + * Returns the number of times the function has been rejected */ - getMsUntilNextWindow(): number { - return this.getRemainingInWindow() * this._options.window + getRejectionCount(): number { + return this._rejectionCount } /** @@ -207,8 +222,10 @@ export class AsyncRateLimiter< */ reset(): void { this._executionTimes = [] - this._executionCount = 0 + this._successCount = 0 + this._errorCount = 0 this._rejectionCount = 0 + this._settleCount = 0 } } @@ -242,12 +259,9 @@ export class AsyncRateLimiter< * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds * ``` */ -export function asyncRateLimit< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, ->( +export function asyncRateLimit( fn: TFn, - initialOptions: Omit, 'enabled'>, + initialOptions: Omit, 'enabled'>, ) { const rateLimiter = new AsyncRateLimiter(fn, initialOptions) return rateLimiter.maybeExecute.bind(rateLimiter) diff --git a/packages/pacer/src/async-throttler.ts b/packages/pacer/src/async-throttler.ts index f0eccb92f..880b3b13c 100644 --- a/packages/pacer/src/async-throttler.ts +++ b/packages/pacer/src/async-throttler.ts @@ -3,23 +3,37 @@ import type { AnyAsyncFunction } from './types' /** * Options for configuring an async throttled function */ -export interface AsyncThrottlerOptions< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, -> { +export interface AsyncThrottlerOptions { /** * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions. * Defaults to true. */ enabled?: boolean + /** + * Whether to execute the function immediately when called + * Defaults to true + */ + leading?: boolean /** * Optional error handler for when the throttled function throws */ - onError?: (error: unknown) => void + onError?: (error: unknown, asyncThrottler: AsyncThrottler) => void /** * Optional function to call when the throttled function is executed */ - onExecute?: (throttler: AsyncThrottler) => void + onSettled?: (asyncThrottler: AsyncThrottler) => void + /** + * Optional function to call when the throttled function is executed + */ + onSuccess?: ( + result: ReturnType, + asyncThrottler: AsyncThrottler, + ) => void + /** + * Whether to execute the function on the trailing edge of the wait period + * Defaults to true + */ + trailing?: boolean /** * Time window in milliseconds during which the function can only be executed once * Defaults to 0ms @@ -27,10 +41,13 @@ export interface AsyncThrottlerOptions< wait: number } -const defaultOptions: Required> = { +const defaultOptions: Required> = { enabled: true, + leading: true, onError: () => {}, - onExecute: () => {}, + onSettled: () => {}, + onSuccess: () => {}, + trailing: true, wait: 0, } @@ -56,22 +73,22 @@ const defaultOptions: Required> = { * }); * ``` */ -export class AsyncThrottler< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, -> { - private _options: Required> +export class AsyncThrottler { + private _options: Required> private _abortController: AbortController | null = null - private _executionCount = 0 + private _errorCount = 0 private _isExecuting = false - private _isPending = false - private _lastArgs: TArgs | undefined + 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 constructor( private fn: TFn, - initialOptions: AsyncThrottlerOptions, + initialOptions: AsyncThrottlerOptions, ) { this._options = { ...defaultOptions, @@ -83,20 +100,19 @@ export class AsyncThrottler< * Updates the throttler options * Returns the new options state */ - setOptions( - newOptions: Partial>, - ): Required> { - 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.cancel() } - return this._options } /** * Returns the current options */ - getOptions(): Required> { + getOptions(): Required> { return this._options } @@ -104,84 +120,82 @@ export class AsyncThrottler< * Attempts to execute the throttled function * If a call is already in progress, it may be blocked or queued depending on the `wait` option */ - async maybeExecute(...args: TArgs): Promise { - this._lastArgs = args - if (this._isPending) return - this._isPending = true + async maybeExecute( + ...args: Parameters + ): Promise | undefined> { + const now = Date.now() + const timeSinceLastExecution = now - this._lastExecutionTime - this._abortController = new AbortController() - const signal = this._abortController.signal + // Handle leading execution + if (this._options.leading && timeSinceLastExecution >= this._options.wait) { + await this.executeFunction(...args) + return this._lastResult + } else { + // Store the most recent arguments for potential trailing execution + this._lastArgs = args - try { - while (this._isExecuting) { - await this.delay(this._options.wait, signal) - } + return new Promise((resolve) => { + // Clear any existing timeout to ensure we use the latest arguments + if (this._timeoutId) { + clearTimeout(this._timeoutId) + } - while (Date.now() < this._nextExecutionTime) { - await this.delay(this._nextExecutionTime - Date.now(), signal) - } + // Set up trailing execution if enabled + if (this._options.trailing) { + const _timeSinceLastExecution = this._lastExecutionTime + ? now - this._lastExecutionTime + : 0 + const timeoutDuration = this._options.wait - _timeSinceLastExecution + this._timeoutId = setTimeout(async () => { + if (this._lastArgs !== undefined) { + await this.executeFunction(...this._lastArgs) + } + resolve(this._lastResult) + }, timeoutDuration) + } + }) + } + } - this._isPending = false + private async executeFunction( + ...args: Parameters + ): Promise | undefined> { + if (!this._options.enabled || this._isExecuting) return undefined + this._abortController = new AbortController() + try { this._isExecuting = true - - await this.executeFunction(...this._lastArgs) + this._lastResult = await this.fn(...args) // EXECUTE! + this._successCount++ + this._options.onSuccess(this._lastResult!, this) } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - return // Silent return on cancellation - } - try { - this._options.onError(error) - } catch { - // Ignore errors from error handler - } + this._errorCount++ + this._options.onError(error, this) } finally { - this._lastExecutionTime = Date.now() - this._nextExecutionTime = this._lastExecutionTime + this._options.wait this._isExecuting = false + this._settleCount++ this._abortController = null + this._lastExecutionTime = Date.now() + this._nextExecutionTime = this._lastExecutionTime + this._options.wait + this._options.onSettled(this) } - } - - private delay(ms: number, signal: AbortSignal): Promise { - return new Promise((resolve, reject) => { - const timeout = setTimeout(resolve, ms) - signal.addEventListener( - 'abort', - () => { - clearTimeout(timeout) - reject(new Error('AbortError')) - }, - { once: true }, - ) - }) - } - - private async executeFunction(...args: TArgs): Promise { - if (!this._options.enabled) return - this._executionCount++ - await this.fn(...args) - this._options.onExecute(this) + return this._lastResult } /** - * Cancels any pending execution + * Cancels any pending execution or aborts any execution in progress */ cancel(): void { + if (this._timeoutId) { + clearTimeout(this._timeoutId) + this._timeoutId = null + } if (this._abortController) { this._abortController.abort() this._abortController = null } - this._isPending = false this._lastArgs = undefined } - /** - * Returns the number of times the function has been executed - */ - getExecutionCount(): number { - return this._executionCount - } - /** * Returns the last execution time */ @@ -196,11 +210,46 @@ export class AsyncThrottler< return this._nextExecutionTime } + /** + * 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 + } + + /** + * Returns the number of times the function has settled (completed or errored) + */ + getSettleCount(): number { + return this._settleCount + } + + /** + * Returns the number of times the function has errored + */ + getErrorCount(): number { + return this._errorCount + } + /** * Returns the current pending state */ getIsPending(): boolean { - return this._isPending + return this._options.enabled && !!this._timeoutId + } + + /** + * Returns the current executing state + */ + getIsExecuting(): boolean { + return this._isExecuting } } @@ -220,10 +269,10 @@ export class AsyncThrottler< * await throttled(); // Waits 1 second before executing * ``` */ -export function asyncThrottle< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, ->(fn: TFn, initialOptions: Omit, 'enabled'>) { +export function asyncThrottle( + fn: TFn, + initialOptions: Omit, 'enabled'>, +) { const asyncThrottler = new AsyncThrottler(fn, initialOptions) return asyncThrottler.maybeExecute.bind(asyncThrottler) } diff --git a/packages/pacer/src/debouncer.ts b/packages/pacer/src/debouncer.ts index f3a57166b..158dd74d5 100644 --- a/packages/pacer/src/debouncer.ts +++ b/packages/pacer/src/debouncer.ts @@ -3,10 +3,7 @@ import type { AnyFunction } from './types' /** * Options for configuring a debounced function */ -export interface DebouncerOptions< - TFn extends AnyFunction, - TArgs extends Parameters, -> { +export interface DebouncerOptions { /** * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions. * Defaults to true. @@ -14,13 +11,14 @@ export interface DebouncerOptions< enabled?: boolean /** * Whether to execute on the leading edge of the timeout. + * The first call will execute immediately and the rest will wait the delay. * Defaults to false. */ leading?: boolean /** * Callback function that is called after the function is executed */ - onExecute?: (debouncer: Debouncer) => void + onExecute?: (debouncer: Debouncer) => void /** * Whether to execute on the trailing edge of the timeout. * Defaults to true. @@ -33,12 +31,12 @@ export interface DebouncerOptions< wait: number } -const defaultOptions: Required> = { +const defaultOptions: Required> = { enabled: true, leading: false, + onExecute: () => {}, trailing: true, wait: 0, - onExecute: () => {}, } /** @@ -64,16 +62,16 @@ const defaultOptions: Required> = { * }); * ``` */ -export class Debouncer> { +export class Debouncer { private _canLeadingExecute = true - private _isPending = false private _executionCount = 0 - private _options: Required> + private _isPending = false + private _options: Required> private _timeoutId: NodeJS.Timeout | undefined constructor( private fn: TFn, - initialOptions: DebouncerOptions, + initialOptions: DebouncerOptions, ) { this._options = { ...defaultOptions, @@ -85,26 +83,19 @@ export class Debouncer> { * Updates the debouncer options * Returns the new options state */ - setOptions( - newOptions: Partial>, - ): Required> { - 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 } - - return this._options } /** * Returns the current debouncer options */ - getOptions(): Required> { + getOptions(): Required> { return this._options } @@ -112,35 +103,37 @@ export class Debouncer> { * Attempts to execute the debounced function * If a call is already in progress, it will be queued */ - maybeExecute(...args: TArgs): void { + maybeExecute(...args: Parameters): void { + let _didLeadingExecute = false + // Handle leading execution if (this._options.leading && this._canLeadingExecute) { - this.executeFunction(...args) this._canLeadingExecute = false + _didLeadingExecute = true + this.executeFunction(...args) } - // Start pending state - if (this._options.leading || this._options.trailing) { + // Start pending state to indicate that the debouncer is waiting for the trailing edge + if (this._options.trailing) { this._isPending = true } // Clear any existing timeout if (this._timeoutId) clearTimeout(this._timeoutId) - // Set new timeout that will reset canLeadingExecute + // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading this._timeoutId = setTimeout(() => { this._canLeadingExecute = true - this._isPending = false - // Execute trailing only if enabled - if (this._options.trailing) { + if (this._options.trailing && !_didLeadingExecute) { this.executeFunction(...args) } }, this._options.wait) } - private executeFunction(...args: TArgs): void { - if (!this._options.enabled) return + private executeFunction(...args: Parameters): void { + if (!this._options.enabled) return undefined this.fn(...args) // EXECUTE! + this._isPending = false this._executionCount++ this._options.onExecute(this) } @@ -193,8 +186,8 @@ export class Debouncer> { */ export function debounce( fn: TFn, - initialOptions: Omit>, 'enabled'>, -) { + initialOptions: Omit, 'enabled'>, +): (...args: Parameters) => void { const debouncer = new Debouncer(fn, initialOptions) return debouncer.maybeExecute.bind(debouncer) } diff --git a/packages/pacer/src/queuer.ts b/packages/pacer/src/queuer.ts index d88b2bbf3..aa3424764 100644 --- a/packages/pacer/src/queuer.ts +++ b/packages/pacer/src/queuer.ts @@ -7,6 +7,16 @@ export interface QueuerOptions { * @default 'back' */ addItemsTo?: QueuePosition + /** + * Maximum time in milliseconds that an item can stay in the queue + * If not provided, items will never expire + */ + expirationDuration?: number + /** + * Function to determine if an item has expired + * If provided, this overrides the expirationDuration behavior + */ + getIsExpired?: (item: TValue, addedAt: number) => boolean /** * Default position to get items from during processing * @default 'front' @@ -25,6 +35,10 @@ export interface QueuerOptions { * Maximum number of items allowed in the queuer */ maxSize?: number + /** + * Callback fired whenever an item expires in the queuer + */ + onExpire?: (item: TValue, queuer: Queuer) => void /** * Callback fired whenever an item is removed from the queuer */ @@ -55,12 +69,15 @@ const defaultOptions: Required> = { addItemsTo: 'back', getItemsFrom: 'front', getPriority: (item) => item?.priority ?? 0, + getIsExpired: () => false, + expirationDuration: Infinity, initialItems: [], maxSize: Infinity, onGetNextItem: () => {}, onIsRunningChange: () => {}, onItemsChange: () => {}, onReject: () => {}, + onExpire: () => {}, started: false, wait: 0, } @@ -99,6 +116,11 @@ export type QueuePosition = 'front' | 'back' * - wait: configurable delay between processing items * - onItemsChange/onGetNextItem: callbacks for monitoring queuer state * + * Supports item expiration to clear stale items from the queuer + * - expirationDuration: maximum time in milliseconds that an item can stay in the queue + * - getIsExpired: function to override default expiration behavior + * - onExpire: callback for when an item expires + * * @example * ```ts * // FIFO queuer @@ -122,8 +144,10 @@ 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 @@ -143,11 +167,8 @@ export class Queuer { * Updates the queuer options * Returns the new options state */ - setOptions( - newOptions: Partial>, - ): QueuerOptions { + setOptions(newOptions: Partial>): void { this._options = { ...this._options, ...newOptions } - return this._options } /** @@ -165,6 +186,10 @@ export class Queuer { this._pendingTick = false return } + + // Check for expired items + this.checkExpiredItems() + while (!this.getIsEmpty()) { const nextItem = this.getNextItem(this._options.getItemsFrom) if (nextItem === undefined) { @@ -183,6 +208,56 @@ export class Queuer { this._pendingTick = false } + /** + * Checks for and removes expired items from the queuer + */ + 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 the queuer from processing items */ @@ -248,15 +323,19 @@ export class Queuer { if (insertIndex === -1) { this._items.push(item) + this._itemTimestamps.push(Date.now()) } else { this._items.splice(insertIndex, 0, item) + this._itemTimestamps.splice(insertIndex, 0, Date.now()) } } else { // Default FIFO/LIFO behavior if (position === 'front') { this._items.unshift(item) + this._itemTimestamps.unshift(Date.now()) } else { this._items.push(item) + this._itemTimestamps.push(Date.now()) } } @@ -288,8 +367,10 @@ export class Queuer { if (position === 'front') { item = this._items.shift() + this._itemTimestamps.shift() } else { item = this._items.pop() + this._itemTimestamps.pop() } if (item !== undefined) { @@ -362,6 +443,13 @@ export class Queuer { return this._rejectionCount } + /** + * Returns the number of items that have expired from the queuer + */ + getExpirationCount(): number { + return this._expirationCount + } + /** * Returns true if the queuer is running */ diff --git a/packages/pacer/src/rate-limiter.ts b/packages/pacer/src/rate-limiter.ts index 37f824a9e..99e44be3a 100644 --- a/packages/pacer/src/rate-limiter.ts +++ b/packages/pacer/src/rate-limiter.ts @@ -3,10 +3,7 @@ import type { AnyFunction } from './types' /** * Options for configuring a rate-limited function */ -export interface RateLimiterOptions< - TFn extends AnyFunction, - TArgs extends Parameters, -> { +export interface RateLimiterOptions { /** * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions. * Defaults to true. @@ -19,18 +16,18 @@ export interface RateLimiterOptions< /** * Callback function that is called after the function is executed */ - onExecute?: (rateLimiter: RateLimiter) => void + onExecute?: (rateLimiter: RateLimiter) => void /** * Optional callback function that is called when an execution is rejected due to rate limiting */ - onReject?: (rateLimiter: RateLimiter) => void + onReject?: (rateLimiter: RateLimiter) => void /** * Time window in milliseconds within which the limit applies */ window: number } -const defaultOptions: Required> = { +const defaultOptions: Required> = { enabled: true, limit: 1, onExecute: () => {}, @@ -63,18 +60,15 @@ const defaultOptions: Required> = { * rateLimiter.maybeExecute('123'); * ``` */ -export class RateLimiter< - TFn extends AnyFunction, - TArgs extends Parameters, -> { +export class RateLimiter { private _executionCount = 0 private _rejectionCount = 0 private _executionTimes: Array = [] - private _options: RateLimiterOptions + private _options: RateLimiterOptions constructor( private fn: TFn, - initialOptions: RateLimiterOptions, + initialOptions: RateLimiterOptions, ) { this._options = { ...defaultOptions, @@ -86,21 +80,15 @@ export class RateLimiter< * Updates the rate limiter options * Returns the new options state */ - setOptions( - newOptions: Partial>, - ): RateLimiterOptions { - this._options = { - ...this._options, - ...newOptions, - } - return this._options + setOptions(newOptions: Partial>): void { + this._options = { ...this._options, ...newOptions } } /** * Returns the current rate limiter options */ - getOptions(): Required> { - return this._options as Required> + getOptions(): Required> { + return this._options as Required> } /** @@ -118,7 +106,7 @@ export class RateLimiter< * rateLimiter.maybeExecute('arg1', 'arg2'); // false * ``` */ - maybeExecute(...args: TArgs): boolean { + maybeExecute(...args: Parameters): boolean { this.cleanupOldExecutions() if (this._executionTimes.length < this._options.limit) { @@ -131,7 +119,7 @@ export class RateLimiter< return false } - private executeFunction(...args: TArgs): void { + private executeFunction(...args: Parameters): void { if (!this._options.enabled) return const now = Date.now() this._executionCount++ @@ -227,7 +215,7 @@ export class RateLimiter< */ export function rateLimit( fn: TFn, - initialOptions: Omit>, 'enabled'>, + initialOptions: Omit, 'enabled'>, ) { const rateLimiter = new RateLimiter(fn, initialOptions) return rateLimiter.maybeExecute.bind(rateLimiter) diff --git a/packages/pacer/src/throttler.ts b/packages/pacer/src/throttler.ts index e2629334b..33209fde1 100644 --- a/packages/pacer/src/throttler.ts +++ b/packages/pacer/src/throttler.ts @@ -3,10 +3,7 @@ import type { AnyFunction } from './types' /** * Options for configuring a throttled function */ -export interface ThrottlerOptions< - TFn extends AnyFunction, - TArgs extends Parameters, -> { +export interface ThrottlerOptions { /** * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions. * Defaults to true. @@ -20,7 +17,7 @@ export interface ThrottlerOptions< /** * Callback function that is called after the function is executed */ - onExecute?: (throttler: Throttler) => void + onExecute?: (throttler: Throttler) => void /** * Whether to execute on the trailing edge of the timeout. * Defaults to true. @@ -32,12 +29,12 @@ export interface ThrottlerOptions< wait: number } -const defaultOptions: Required> = { +const defaultOptions: Required> = { enabled: true, leading: true, + onExecute: () => {}, trailing: true, wait: 0, - onExecute: () => {}, } /** @@ -67,17 +64,16 @@ const defaultOptions: Required> = { * throttler.maybeExecute('123'); // Throttled * ``` */ -export class Throttler> { +export class Throttler { private _executionCount = 0 - private _lastArgs: TArgs | undefined + private _lastArgs: Parameters | undefined private _lastExecutionTime = 0 - private _options: Required> + private _options: Required> private _timeoutId: NodeJS.Timeout | undefined - private _isPending = false constructor( private fn: TFn, - initialOptions: ThrottlerOptions, + initialOptions: ThrottlerOptions, ) { this._options = { ...defaultOptions, @@ -89,20 +85,19 @@ export class Throttler> { * Updates the throttler options * Returns the new options state */ - setOptions( - newOptions: Partial>, - ): Required> { - 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.cancel() } - return this._options } /** * Returns the current throttler options */ - getOptions(): Required> { + getOptions(): Required> { return this._options } @@ -128,42 +123,40 @@ export class Throttler> { * throttled.maybeExecute('c', 'd'); * ``` */ - maybeExecute(...args: TArgs): void { + maybeExecute(...args: Parameters): void { const now = Date.now() const timeSinceLastExecution = now - this._lastExecutionTime // Handle leading execution - if (timeSinceLastExecution >= this._options.wait) { - if (this._options.leading) { - this.executeFunction(...args) - } - this._lastExecutionTime = now - this._isPending = false + if (this._options.leading && timeSinceLastExecution >= this._options.wait) { + this.executeFunction(...args) } else { // Store the most recent arguments for potential trailing execution this._lastArgs = args // Set up trailing execution if not already scheduled if (!this._timeoutId && this._options.trailing) { - this._isPending = true + const _timeSinceLastExecution = this._lastExecutionTime + ? now - this._lastExecutionTime + : 0 + const timeoutDuration = this._options.wait - _timeSinceLastExecution this._timeoutId = setTimeout(() => { - if (this._lastArgs) { + if (this._lastArgs !== undefined) { this.executeFunction(...this._lastArgs) - this._lastArgs = undefined } - this._lastExecutionTime = Date.now() - this._timeoutId = undefined - this._isPending = false - this._options.onExecute(this) - }, this._options.wait - timeSinceLastExecution) + }, timeoutDuration) } } } - private executeFunction(...args: TArgs): void { + private executeFunction(...args: Parameters): void { if (!this._options.enabled) return this.fn(...args) // EXECUTE! this._executionCount++ + this._lastExecutionTime = Date.now() + this._timeoutId = undefined + this._lastArgs = undefined + this._options.onExecute(this) } /** @@ -180,36 +173,35 @@ export class Throttler> { clearTimeout(this._timeoutId) this._timeoutId = undefined this._lastArgs = undefined - this._isPending = false } } /** - * Returns the number of times the function has been executed + * Returns the last execution time */ - getExecutionCount(): number { - return this._executionCount + getLastExecutionTime(): number { + return this._lastExecutionTime } /** - * Returns `true` if there is a pending execution + * Returns the next execution time */ - getIsPending(): boolean { - return this._options.enabled && this._isPending + getNextExecutionTime(): number { + return this._lastExecutionTime + this._options.wait } /** - * Returns the last execution time + * Returns the number of times the function has been executed */ - getLastExecutionTime(): number { - return this._lastExecutionTime + getExecutionCount(): number { + return this._executionCount } /** - * Returns the next execution time + * Returns `true` if there is a pending execution */ - getNextExecutionTime(): number { - return this._lastExecutionTime + this._options.wait + getIsPending(): boolean { + return this._options.enabled && !!this._timeoutId } } @@ -239,10 +231,10 @@ export class Throttler> { * }); * ``` */ -export function throttle< - TFn extends AnyFunction, - TArgs extends Parameters, ->(fn: TFn, initialOptions: Omit, 'enabled'>) { +export function throttle( + fn: TFn, + initialOptions: Omit, 'enabled'>, +) { const throttler = new Throttler(fn, initialOptions) return throttler.maybeExecute.bind(throttler) } diff --git a/packages/pacer/src/types.ts b/packages/pacer/src/types.ts index 86b6f6d62..4aeba4933 100644 --- a/packages/pacer/src/types.ts +++ b/packages/pacer/src/types.ts @@ -1,17 +1,9 @@ /** * Represents a function that can be called with any arguments and returns any value. - * @template TArgs - The type of the arguments the function can be called with. - * @returns The return value of the function. */ -export type AnyFunction = Array> = ( - ...args: TArgs -) => any +export type AnyFunction = (...args: Array) => any /** * Represents an asynchronous function that can be called with any arguments and returns a promise. - * @template TArgs - The type of the arguments the function can be called with. - * @returns A promise that resolves to the return value of the function. */ -export type AnyAsyncFunction = Array> = ( - ...args: TArgs -) => Promise +export type AnyAsyncFunction = (...args: Array) => Promise diff --git a/packages/pacer/src/utils.ts b/packages/pacer/src/utils.ts index 6673ed3ac..891991c2f 100644 --- a/packages/pacer/src/utils.ts +++ b/packages/pacer/src/utils.ts @@ -1,6 +1,6 @@ export function bindInstanceMethods>( instance: T, -) { +): T { return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)) .filter((key) => typeof instance[key as keyof T] === 'function') .reduce((acc: any, key) => { diff --git a/packages/pacer/tests/async-debouncer.test.ts b/packages/pacer/tests/async-debouncer.test.ts index 4b16ca937..bb9d20356 100644 --- a/packages/pacer/tests/async-debouncer.test.ts +++ b/packages/pacer/tests/async-debouncer.test.ts @@ -1,155 +1,1131 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { AsyncDebouncer } from '../src/async-debouncer' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { AsyncDebouncer, asyncDebounce } from '../src/async-debouncer' describe('AsyncDebouncer', () => { beforeEach(() => { vi.useFakeTimers() }) - it('should delay execution until after wait period', async () => { - const mockFn = vi.fn().mockResolvedValue(undefined) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('Basic Async Debouncing', () => { + it('should not execute the async function before the specified wait', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + const promise = debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() + + vi.advanceTimersByTime(999) + expect(mockFn).not.toBeCalled() + + vi.advanceTimersByTime(1) + await promise + expect(mockFn).toBeCalledTimes(1) + }) + + it('should execute the async function after the specified wait', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + const promise = debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() + + vi.advanceTimersByTime(1000) + const result = await promise + expect(mockFn).toBeCalledTimes(1) + expect(result).toBe('result') + }) + + it('should debounce multiple async calls', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // Make multiple calls + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(500) + const promise2 = debouncer.maybeExecute() + vi.advanceTimersByTime(500) + const promise3 = debouncer.maybeExecute() + + // Function should not be called yet + expect(mockFn).not.toBeCalled() + + // Wait for the full delay + vi.advanceTimersByTime(1000) + await Promise.any([promise1, promise2, promise3]) + + // Should only execute once + expect(mockFn).toBeCalledTimes(1) + }) + + it('should pass arguments to the debounced async function', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + const promise = debouncer.maybeExecute('arg1', 42, { foo: 'bar' }) + vi.advanceTimersByTime(1000) + await promise + + expect(mockFn).toBeCalledWith('arg1', 42, { foo: 'bar' }) + }) + + it('should return a promise that resolves with the function result', async () => { + const mockFn = vi.fn().mockResolvedValue('test result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + const promise = debouncer.maybeExecute() + expect(promise).toBeInstanceOf(Promise) + + vi.advanceTimersByTime(1000) + const result = await promise + expect(result).toBe('test result') + }) + }) + + describe('Async Execution Edge Cases', () => { + it('should execute immediately with leading option', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + leading: true, + trailing: false, + }) + + const promise = debouncer.maybeExecute() + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith() + + vi.advanceTimersByTime(1000) + await promise + expect(mockFn).toBeCalledTimes(1) // Should not execute again + }) + + it('should respect leading edge timing', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + leading: true, + trailing: false, + }) + + // First call - executes immediately + const promise1 = debouncer.maybeExecute('first') + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') + + // Call again before wait expires - should not execute + vi.advanceTimersByTime(500) + const promise2 = debouncer.maybeExecute('second') + expect(mockFn).toBeCalledTimes(1) + + // Advance to end of second call's wait period - should not execute + vi.advanceTimersByTime(1000) + await Promise.all([promise1, promise2]) + expect(mockFn).toBeCalledTimes(1) + + // Now that the full wait has passed since last call, this should execute + const promise3 = debouncer.maybeExecute('third') + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toHaveBeenLastCalledWith('third') + await promise3 + }) + + it('should support both leading and trailing execution', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + leading: true, + trailing: true, + }) + + // First call - executes immediately (leading) + const promise1 = debouncer.maybeExecute('first') + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') + + // Second call - should not execute immediately + const promise2 = debouncer.maybeExecute('second') + expect(mockFn).toBeCalledTimes(1) + + // After wait, should execute again (trailing) + vi.advanceTimersByTime(1000) + await Promise.all([promise1, promise2]) + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toHaveBeenLastCalledWith('second') + }) + + it('should default to trailing-only execution', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // First call - should not execute immediately + const promise1 = debouncer.maybeExecute('first') + expect(mockFn).not.toBeCalled() + + // Second call - should not execute immediately + const promise2 = debouncer.maybeExecute('second') + expect(mockFn).not.toBeCalled() + + // After wait, should execute once with last arguments + vi.advanceTimersByTime(1000) + await Promise.any([promise1, promise2]) + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('second') + }) + + it('should handle case where both leading and trailing are false', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + leading: false, + trailing: false, + }) + + // First call - should not execute + debouncer.maybeExecute('test') + expect(mockFn).not.toBeCalled() + + // Second call - should cancel first promise and not execute + const promise2 = debouncer.maybeExecute('test2') + expect(mockFn).not.toBeCalled() + + // Advance time and wait for the last promise + vi.advanceTimersByTime(1000) + await promise2 + + // Verify no executions occurred + expect(mockFn).not.toBeCalled() + }) + + it('should handle rapid calls with leading edge execution', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + leading: true, + trailing: false, + }) + + // Make rapid calls + debouncer.maybeExecute('first') + debouncer.maybeExecute('second') + debouncer.maybeExecute('third') + const promise4 = debouncer.maybeExecute('fourth') + + // Only first call should execute immediately + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') + + // Wait for timeout and last promise + vi.advanceTimersByTime(1000) + await promise4 - const promise = debouncer.maybeExecute('first') - expect(mockFn).not.toHaveBeenCalled() + // Next call should execute immediately + const promise5 = debouncer.maybeExecute('fifth') + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toHaveBeenLastCalledWith('fifth') + await promise5 + }) + }) + + describe('Promise Handling', () => { + it('should properly handle promise resolution', async () => { + const mockFn = vi.fn().mockResolvedValue('resolved value') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + const result = await promise + + expect(result).toBe('resolved value') + expect(mockFn).toBeCalledTimes(1) + }) + + it('should handle promise errors without rejecting', async () => { + const error = new Error('test error') + const mockFn = vi.fn().mockRejectedValue(error) + const onError = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onError, + }) + + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + + // The promise should resolve with undefined, not reject + const result = await promise + expect(result).toBeUndefined() + expect(onError).toBeCalledWith(error, debouncer) + }) + + it('should maintain execution order of promises', async () => { + const results: Array = [] + const mockFn = vi.fn().mockImplementation((value: string) => { + results.push(value) + return value + }) + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // Make multiple calls + debouncer.maybeExecute('first') + vi.advanceTimersByTime(500) + debouncer.maybeExecute('second') + vi.advanceTimersByTime(500) + const promise3 = debouncer.maybeExecute('third') + + // Wait for the last promise + vi.advanceTimersByTime(1000) + await promise3 + + // Should only execute once with the last value + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('third') + expect(results).toEqual(['third']) + }) + + it('should handle multiple promise resolutions', async () => { + const mockFn = vi.fn().mockImplementation((value: string) => { + return value + }) + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // Start first execution + const promise1 = debouncer.maybeExecute('first') + vi.advanceTimersByTime(1000) + await promise1 + expect(mockFn).toHaveBeenCalledWith('first') + + // Start second execution after first completes + const promise2 = debouncer.maybeExecute('second') + vi.advanceTimersByTime(1000) + await promise2 + expect(mockFn).toHaveBeenCalledWith('second') + + // Start third execution after second completes + const promise3 = debouncer.maybeExecute('third') + vi.advanceTimersByTime(1000) + await promise3 + expect(mockFn).toHaveBeenCalledWith('third') + + expect(mockFn).toBeCalledTimes(3) + }) - vi.advanceTimersByTime(99) - expect(mockFn).not.toHaveBeenCalled() + it('should handle promise cancellation', () => { + const mockFn = vi.fn().mockImplementation(async (value: string) => { + await new Promise((resolve) => setTimeout(resolve, 100)) + return value + }) + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) - vi.advanceTimersByTime(1) - await promise - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenLastCalledWith('first') + debouncer.maybeExecute('test') + debouncer.cancel() + vi.advanceTimersByTime(1100) + expect(mockFn).toBeCalledTimes(0) + }) }) - it('should reset timer on subsequent calls', async () => { - const mockFn = vi.fn().mockResolvedValue(undefined) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + describe('Error Handling', () => { + it('should call onError when the async function throws', async () => { + const error = new Error('test error') + const mockFn = vi.fn().mockRejectedValue(error) + const onError = vi.fn() + const onSettled = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onError, + onSettled, + }) + + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + 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) + }) - debouncer.maybeExecute('first') - vi.advanceTimersByTime(50) + it('should not break debouncing chain on error', async () => { + const error = new Error('test error') + const mockFn = vi + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValueOnce('success') + const onError = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onError, + }) + + // First call - should error + const promise1 = debouncer.maybeExecute() + 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) + + // Second call - should succeed + const promise2 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + 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) + }) - debouncer.maybeExecute('second') - vi.advanceTimersByTime(50) - await Promise.resolve() - expect(mockFn).not.toHaveBeenCalled() + it('should handle multiple errors in sequence', async () => { + const error1 = new Error('error 1') + const error2 = new Error('error 2') + const mockFn = vi + .fn() + .mockRejectedValueOnce(error1) + .mockRejectedValueOnce(error2) + const onError = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onError, + }) - vi.advanceTimersByTime(50) - await Promise.resolve() - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenLastCalledWith('second') + // First call - should error + const promise1 = debouncer.maybeExecute() + 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) + + // 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) + }) + + it('should handle errors during leading execution', async () => { + const error = new Error('test error') + const mockFn = vi.fn().mockRejectedValue(error) + const onError = vi.fn() + const onSettled = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + leading: true, + onError, + onSettled, + }) + + const promise = debouncer.maybeExecute() + await vi.advanceTimersByTimeAsync(1000) + 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) + }) + + it('should handle errors during trailing execution', async () => { + const error = new Error('test error') + const mockFn = vi.fn().mockRejectedValue(error) + const onError = vi.fn() + const onSettled = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + trailing: true, + onError, + onSettled, + }) + + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + 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) + }) + + it('should maintain state after error', async () => { + const error = new Error('test error') + const mockFn = vi.fn().mockRejectedValue(error) + const onError = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onError, + }) + + // First call - should error + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + 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) + }) }) - it('should track execution count correctly', async () => { - const mockFn = vi.fn().mockResolvedValue(undefined) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + describe('Callback Execution', () => { + it('should call onSuccess after successful execution', async () => { + const mockFn = vi.fn().mockResolvedValue('success') + const onSuccess = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onSuccess, + }) + + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise + + expect(onSuccess).toBeCalledTimes(1) + expect(onSuccess).toBeCalledWith('success', debouncer) + }) + + it('should call onSettled after execution completes', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const onSettled = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onSettled, + }) + + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise + + expect(onSettled).toBeCalledTimes(1) + expect(onSettled).toBeCalledWith(debouncer) + }) + + it('should call onError when execution fails', async () => { + const error = new Error('test error') + const mockFn = vi.fn().mockRejectedValue(error) + const onError = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onError, + }) + + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise + + expect(onError).toBeCalledTimes(1) + expect(onError).toBeCalledWith(error, debouncer) + }) + + it('should maintain correct callback order', async () => { + const mockFn = vi.fn().mockResolvedValue('success') + const callOrder: Array = [] + const onSuccess = vi + .fn() + .mockImplementation(() => callOrder.push('success')) + const onSettled = vi + .fn() + .mockImplementation(() => callOrder.push('settled')) + const onError = vi.fn().mockImplementation(() => callOrder.push('error')) + + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onSuccess, + onSettled, + onError, + }) + + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise + + expect(callOrder).toEqual(['success', 'settled']) + }) + + it('should handle callback errors gracefully', async () => { + const mockFn = vi.fn().mockResolvedValue('success') + const callbackError = new Error('callback error') + const onSuccess = vi.fn().mockImplementation(() => { + throw callbackError + }) + const onSettled = vi.fn() + const onError = vi.fn() - const promise1 = debouncer.maybeExecute() - expect(debouncer.getExecutionCount()).toBe(0) + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onSuccess, + onSettled, + onError, + }) - vi.advanceTimersByTime(100) - await promise1 - expect(debouncer.getExecutionCount()).toBe(1) + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise - const promise2 = debouncer.maybeExecute() - vi.advanceTimersByTime(100) - await promise2 - expect(debouncer.getExecutionCount()).toBe(2) + // onSuccess throws, which triggers onError, and onSettled is always called + expect(onSuccess).toBeCalledTimes(1) + expect(onError).toBeCalledTimes(1) + expect(onError).toBeCalledWith(callbackError, debouncer) + expect(onSettled).toBeCalledTimes(1) + expect(onSettled).toBeCalledWith(debouncer) + }) }) - it('should handle errors with onError callback', async () => { - const error = new Error('Test error') - const mockFn = vi.fn().mockRejectedValue(error) - const onError = vi.fn() - const debouncer = new AsyncDebouncer(mockFn, { wait: 100, onError }) + describe('Execution Control', () => { + it('should cancel pending execution', () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // Start execution + debouncer.maybeExecute() + expect(debouncer.getIsPending()).toBe(true) + + // Cancel before wait period ends + debouncer.cancel() + expect(debouncer.getIsPending()).toBe(false) + + // Advance time and verify no execution + vi.advanceTimersByTime(1000) + expect(mockFn).not.toBeCalled() + }) + + it('should properly handle canLeadingExecute flag after cancellation', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + leading: true, + }) + + // First call - should execute immediately + const promise1 = debouncer.maybeExecute('first') + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') + + // Cancel and verify canLeadingExecute is reset + debouncer.cancel() + expect(debouncer.getIsPending()).toBe(false) + + // Next call should execute immediately again + const promise2 = debouncer.maybeExecute('second') + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toBeCalledWith('second') + + await Promise.all([promise1, promise2]) + }) + + it('should handle cancellation during leading execution', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + leading: true, + }) - const promise = debouncer.maybeExecute() - vi.advanceTimersByTime(100) - await promise - expect(onError).toHaveBeenCalledWith(error) + // First call - executes immediately + const promise1 = debouncer.maybeExecute('first') + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') + + // Cancel during leading execution + debouncer.cancel() + expect(debouncer.getIsPending()).toBe(false) + + // Next call should execute immediately again + const promise2 = debouncer.maybeExecute('second') + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toBeCalledWith('second') + + await Promise.all([promise1, promise2]) + }) }) - it('should ignore errors in onError callback', async () => { - const error = new Error('Test error') - const mockFn = vi.fn().mockRejectedValue(error) - const onError = vi.fn().mockImplementation(() => { - throw new Error('Error handler error') + describe('Result Management', () => { + it('should track last result', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise + + expect(debouncer.getLastResult()).toBe('result') + }) + + it('should return last result when execution is pending', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // First execution + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise1 + expect(debouncer.getLastResult()).toBe('result') + + // Second execution - should still return last result while pending + const promise2 = debouncer.maybeExecute() + expect(debouncer.getLastResult()).toBe('result') + vi.advanceTimersByTime(1000) + await promise2 }) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100, onError }) - const promise = debouncer.maybeExecute() - vi.advanceTimersByTime(100) - await expect(promise).resolves.not.toThrow() + it('should clear last result on cancellation', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // First execution + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise1 + expect(debouncer.getLastResult()).toBe('result') + + // Second execution with cancellation + debouncer.maybeExecute() + debouncer.cancel() + expect(debouncer.getLastResult()).toBe('result') // Should still have last result + + // Third execution + const promise3 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise3 + expect(debouncer.getLastResult()).toBe('result') + }) + + it('should maintain last result across multiple executions', async () => { + const mockFn = vi + .fn() + .mockResolvedValueOnce('first') + .mockResolvedValueOnce('second') + .mockResolvedValueOnce('third') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // First execution + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise1 + expect(debouncer.getLastResult()).toBe('first') + + // Second execution + const promise2 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise2 + expect(debouncer.getLastResult()).toBe('second') + + // Third execution + const promise3 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise3 + expect(debouncer.getLastResult()).toBe('third') + }) + + it('should handle undefined/null results', async () => { + const mockFn = vi + .fn() + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(null) + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // Test undefined result + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise1 + expect(debouncer.getLastResult()).toBeUndefined() + + // Test null result + const promise2 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise2 + expect(debouncer.getLastResult()).toBeNull() + }) + + it('should maintain last result after error', async () => { + const mockFn = vi + .fn() + .mockResolvedValueOnce('success') + .mockRejectedValueOnce(new Error('test error')) + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // First execution - success + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise1 + expect(debouncer.getLastResult()).toBe('success') + + // Second execution - error + const promise2 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise2 + expect(debouncer.getLastResult()).toBe('success') // Should maintain last successful result + }) }) - it('should cancel pending execution', async () => { - const mockFn = vi.fn().mockResolvedValue(undefined) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + describe('Enabled/Disabled State', () => { + it('should not execute when enabled is false', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + enabled: false, + }) + + // Try to execute while disabled + const promise = debouncer.maybeExecute() + expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.getIsExecuting()).toBe(false) + + // Advance time and verify no execution + vi.advanceTimersByTime(1000) + await promise + expect(mockFn).not.toBeCalled() + }) + + it('should not execute leading edge when disabled', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + leading: true, + enabled: false, + }) + + // Try to execute while disabled + const promise = debouncer.maybeExecute() + expect(debouncer.getIsPending()).toBe(false) + expect(debouncer.getIsExecuting()).toBe(false) + + // Advance time and verify no execution + vi.advanceTimersByTime(1000) + await promise + expect(mockFn).not.toBeCalled() + }) + + it('should default to enabled', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // Should execute by default + const promise = debouncer.maybeExecute() + expect(debouncer.getIsPending()).toBe(true) + + // Advance time and verify execution + vi.advanceTimersByTime(1000) + await promise + expect(mockFn).toBeCalledTimes(1) + }) + + it('should allow disabling mid-wait', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // Start execution + const promise = debouncer.maybeExecute() + expect(debouncer.getIsPending()).toBe(true) + + // Disable during wait + debouncer.setOptions({ enabled: false }) + expect(debouncer.getIsPending()).toBe(false) - debouncer.maybeExecute('test') - vi.advanceTimersByTime(50) - debouncer.cancel() + // Advance time and verify no execution + vi.advanceTimersByTime(1000) + await promise + expect(mockFn).not.toBeCalled() + }) + + it('should handle rapid enable/disable cycles', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // Enable/disable rapidly + debouncer.setOptions({ enabled: true }) + debouncer.maybeExecute() + debouncer.setOptions({ enabled: false }) + debouncer.setOptions({ enabled: true }) + debouncer.maybeExecute() + debouncer.setOptions({ enabled: false }) + debouncer.setOptions({ enabled: true }) + const promise = debouncer.maybeExecute() + + // Should only have one pending execution + expect(debouncer.getIsPending()).toBe(true) + + // Advance time and verify single execution + vi.advanceTimersByTime(1000) + await promise + expect(mockFn).toBeCalledTimes(1) + }) + + it('should maintain state when disabled', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) - vi.advanceTimersByTime(50) - await Promise.resolve() - expect(mockFn).not.toHaveBeenCalled() + // First execution + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise1 + expect(debouncer.getLastResult()).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) + + // Re-enable and verify state is still maintained + debouncer.setOptions({ enabled: true }) + expect(debouncer.getLastResult()).toBe('result') + }) }) - it('should allow new executions after cancel', async () => { - const mockFn = vi.fn().mockResolvedValue(undefined) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + describe('Options Management', () => { + it('should allow updating multiple options at once', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // Update multiple options + debouncer.setOptions({ + wait: 500, + leading: true, + trailing: false, + onSuccess: vi.fn(), + onError: vi.fn(), + onSettled: vi.fn(), + }) + + // Verify new options are applied + const promise = debouncer.maybeExecute() + expect(mockFn).toBeCalledTimes(1) // Leading execution + expect(mockFn).toBeCalledWith() + + // Advance time and verify no trailing execution + vi.advanceTimersByTime(500) + await promise + expect(mockFn).toBeCalledTimes(1) + }) + + it('should handle option changes during execution', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const onSuccess = vi.fn() + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onSuccess: vi.fn(), + }) + + // Start execution + const promise = debouncer.maybeExecute() + expect(debouncer.getIsPending()).toBe(true) + + // Change options during wait + debouncer.setOptions({ onSuccess }) + + // Advance time and verify new callback is used + vi.advanceTimersByTime(1000) + await promise + expect(onSuccess).toBeCalledTimes(1) + expect(onSuccess).toBeCalledWith('result', debouncer) + }) + + it('should maintain state across option changes', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) + + // First execution + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise1 + expect(debouncer.getLastResult()).toBe('result') + expect(debouncer.getSuccessCount()).toBe(1) - debouncer.maybeExecute('first') - vi.advanceTimersByTime(50) - debouncer.cancel() + // Change options + debouncer.setOptions({ wait: 500, leading: true }) - debouncer.maybeExecute('second') - vi.advanceTimersByTime(100) - await Promise.resolve() + // Verify state is maintained + expect(debouncer.getLastResult()).toBe('result') + expect(debouncer.getSuccessCount()).toBe(1) - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenCalledWith('second') + // 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) + }) + + it('should handle callback option changes', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const onSuccess1 = vi.fn() + const onSuccess2 = vi.fn() + const onError1 = vi.fn() + const onError2 = vi.fn() + const onSettled1 = vi.fn() + const onSettled2 = vi.fn() + + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onSuccess: onSuccess1, + onError: onError1, + onSettled: onSettled1, + }) + + // First execution + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise1 + expect(onSuccess1).toBeCalledTimes(1) + expect(onSettled1).toBeCalledTimes(1) + expect(onError1).not.toBeCalled() + + // Change callbacks + debouncer.setOptions({ + onSuccess: onSuccess2, + onError: onError2, + onSettled: onSettled2, + }) + + // Second execution + const promise2 = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise2 + expect(onSuccess2).toBeCalledTimes(1) + expect(onSettled2).toBeCalledTimes(1) + expect(onError2).not.toBeCalled() + }) + + it('should handle option changes during error handling', async () => { + const error = new Error('test error') + const mockFn = vi.fn().mockRejectedValue(error) + const onError1 = vi.fn() + const onError2 = vi.fn() + const onSettled1 = vi.fn() + const onSettled2 = vi.fn() + + const debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onError: onError1, + onSettled: onSettled1, + }) + + // Start execution + const promise = debouncer.maybeExecute() + expect(debouncer.getIsPending()).toBe(true) + + // Change callbacks during wait + debouncer.setOptions({ + onError: onError2, + onSettled: onSettled2, + }) + + // Advance time and verify new callbacks are used + vi.advanceTimersByTime(1000) + await promise + expect(onError2).toBeCalledTimes(1) + expect(onError2).toBeCalledWith(error, debouncer) + expect(onSettled2).toBeCalledTimes(1) + expect(onSettled2).toBeCalledWith(debouncer) + expect(onError1).not.toBeCalled() + expect(onSettled1).not.toBeCalled() + }) + }) +}) + +describe('asyncDebounce helper function', () => { + beforeEach(() => { + vi.useFakeTimers() }) - it('should handle multiple rapid calls', async () => { - const mockFn = vi.fn().mockResolvedValue(undefined) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('Basic Functionality', () => { + it('should create a debounced async function with default options', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debounced = asyncDebounce(mockFn, { wait: 1000 }) - debouncer.maybeExecute('first') - vi.advanceTimersByTime(20) - debouncer.maybeExecute('second') - vi.advanceTimersByTime(20) - debouncer.maybeExecute('third') - vi.advanceTimersByTime(20) - debouncer.maybeExecute('fourth') + const promise = debounced() + expect(mockFn).not.toBeCalled() - vi.advanceTimersByTime(100) - await Promise.resolve() + vi.advanceTimersByTime(1000) + const result = await promise + expect(mockFn).toBeCalledTimes(1) + expect(result).toBe('result') + }) - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenCalledWith('fourth') + it('should pass arguments correctly', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debounced = asyncDebounce(mockFn, { wait: 1000 }) + + const promise = debounced('arg1', 42, { foo: 'bar' }) + vi.advanceTimersByTime(1000) + await promise + + expect(mockFn).toBeCalledWith('arg1', 42, { foo: 'bar' }) + }) + + it('should return a promise', () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debounced = asyncDebounce(mockFn, { wait: 1000 }) + + const promise = debounced() + expect(promise).toBeInstanceOf(Promise) + }) }) - it('should handle long-running functions', async () => { - let resolveFirst: (value: unknown) => void - const firstCall = new Promise((resolve) => { - resolveFirst = resolve + describe('Execution Options', () => { + it('should respect leading option', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debounced = asyncDebounce(mockFn, { wait: 1000, leading: true }) + + const promise = debounced() + expect(mockFn).toBeCalledTimes(1) + + vi.advanceTimersByTime(1000) + await promise + expect(mockFn).toBeCalledTimes(1) + }) + + it('should handle multiple calls with trailing edge', () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debounced = asyncDebounce(mockFn, { wait: 1000 }) + + debounced() + debounced() + debounced() + expect(mockFn).not.toBeCalled() + + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) }) - const mockFn = vi.fn().mockImplementation(() => firstCall) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + it('should support both leading and trailing execution', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debounced = asyncDebounce(mockFn, { + wait: 1000, + leading: true, + trailing: true, + }) - const promise = debouncer.maybeExecute('test') - vi.advanceTimersByTime(100) + // First call - should execute immediately + const promise1 = debounced('first') + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') - expect(mockFn).toHaveBeenCalledTimes(1) - resolveFirst!({}) - await promise + // Second call - should queue for trailing + const promise2 = debounced('second') + expect(mockFn).toBeCalledTimes(1) - // Subsequent call should work - debouncer.maybeExecute('next') - vi.advanceTimersByTime(100) - await Promise.resolve() - expect(mockFn).toHaveBeenCalledTimes(2) + vi.advanceTimersByTime(1000) + await Promise.all([promise1, promise2]) + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toBeCalledWith('second') + }) }) }) diff --git a/packages/pacer/tests/async-queuer.test.ts b/packages/pacer/tests/async-queuer.test.ts index 17b6973f3..88cab5cbb 100644 --- a/packages/pacer/tests/async-queuer.test.ts +++ b/packages/pacer/tests/async-queuer.test.ts @@ -9,13 +9,17 @@ describe('AsyncQueuer', () => { }) describe('basic functionality', () => { - it('should create an empty queue', () => { + it('should create an empty queue that is running and idle', () => { + expect(asyncQueuer.getAllItems()).toHaveLength(0) + expect(asyncQueuer.getIsIdle()).toBe(true) + expect(asyncQueuer.getIsRunning()).toBe(true) + }) + + it('should create an empty queue that is not running and idle', () => { + asyncQueuer = new AsyncQueuer({ started: false }) expect(asyncQueuer.getAllItems()).toHaveLength(0) expect(asyncQueuer.getIsIdle()).toBe(false) expect(asyncQueuer.getIsRunning()).toBe(false) - asyncQueuer.start() - expect(asyncQueuer.getIsRunning()).toBe(true) - expect(asyncQueuer.getIsRunning()).toBe(true) }) it('should process tasks in FIFO order when started', async () => { @@ -89,6 +93,7 @@ describe('AsyncQueuer', () => { describe('queue control', () => { it('should clear the queue', () => { + asyncQueuer.stop() asyncQueuer.addItem(() => Promise.resolve(1)) asyncQueuer.addItem(() => Promise.resolve(2)) @@ -130,6 +135,7 @@ describe('AsyncQueuer', () => { describe('queue positions', () => { it('should support peeking at tasks', () => { + asyncQueuer.stop() const task1 = () => Promise.resolve(1) const task2 = () => Promise.resolve(2) diff --git a/packages/pacer/tests/async-throttler.test.ts b/packages/pacer/tests/async-throttler.test.ts index e714afa5d..c1ea5526e 100644 --- a/packages/pacer/tests/async-throttler.test.ts +++ b/packages/pacer/tests/async-throttler.test.ts @@ -41,38 +41,6 @@ describe('AsyncThrottler', () => { expect(mockFn).toHaveBeenLastCalledWith('first') }) - it('should handle long-running functions that exceed wait period', async () => { - const wait = 1000 - let resolveFirst: (value: unknown) => void - const firstCall = new Promise((resolve) => { - resolveFirst = resolve - }) - - const mockFn = vi.fn().mockImplementation(() => firstCall) - const throttler = new AsyncThrottler(mockFn, { wait }) - - // Start first long-running call - const promise1 = throttler.maybeExecute('1') - throttler.maybeExecute('2') - const promise3 = throttler.maybeExecute('3') - - // First call should be executing - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenLastCalledWith('1') - - // Complete first long-running call - resolveFirst!({}) - await promise1 - - // Wait for throttle period after first call completes - vi.advanceTimersByTime(wait) - await promise3 - - // Should have executed with latest args - expect(mockFn).toHaveBeenCalledTimes(2) - expect(mockFn).toHaveBeenLastCalledWith('3') - }) - it('should handle special timing cases with delayed calls', async () => { const mockFn = vi.fn().mockResolvedValue(undefined) const throttler = new AsyncThrottler(mockFn, { wait: 100 }) @@ -124,14 +92,14 @@ describe('AsyncThrottler', () => { const throttler = new AsyncThrottler(mockFn, { wait: 100 }) await throttler.maybeExecute() - expect(throttler.getExecutionCount()).toBe(1) + expect(throttler.getSuccessCount()).toBe(1) const promise = throttler.maybeExecute() - expect(throttler.getExecutionCount()).toBe(1) + expect(throttler.getSuccessCount()).toBe(1) vi.advanceTimersByTime(100) await promise - expect(throttler.getExecutionCount()).toBe(2) + expect(throttler.getSuccessCount()).toBe(2) }) it('should handle errors with onError callback', async () => { @@ -141,19 +109,7 @@ describe('AsyncThrottler', () => { const throttler = new AsyncThrottler(mockFn, { wait: 100, onError }) await throttler.maybeExecute() - expect(onError).toHaveBeenCalledWith(error) - }) - - it('should ignore errors in onError callback', async () => { - const error = new Error('Test error') - const mockFn = vi.fn().mockRejectedValue(error) - const onError = vi.fn().mockImplementation(() => { - throw new Error('Error handler error') - }) - const throttler = new AsyncThrottler(mockFn, { wait: 100, onError }) - - // Should not throw - await expect(throttler.maybeExecute()).resolves.not.toThrow() + expect(onError).toHaveBeenCalledWith(error, throttler) }) it('should continue processing after function throws error', async () => { @@ -175,31 +131,6 @@ describe('AsyncThrottler', () => { expect(mockFn).toHaveBeenLastCalledWith(2) }) - it('should wait for execution to complete before starting next one', async () => { - let resolveFirst: (value: unknown) => void - const firstCall = new Promise((resolve) => { - resolveFirst = resolve - }) - - const mockFn = vi.fn().mockImplementation(() => firstCall) - const throttler = new AsyncThrottler(mockFn, { wait: 100 }) - - const promise1 = throttler.maybeExecute('first') - const promise2 = throttler.maybeExecute('second') - - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenCalledWith('first') - - resolveFirst!({}) - await promise1 - - vi.advanceTimersByTime(100) - await promise2 - - expect(mockFn).toHaveBeenCalledTimes(2) - expect(mockFn).toHaveBeenLastCalledWith('second') - }) - it('should maintain proper timing between executions', async () => { const mockFn = vi.fn().mockResolvedValue(undefined) const throttler = new AsyncThrottler(mockFn, { wait: 100 }) @@ -244,7 +175,7 @@ describe('AsyncThrottler', () => { const throttler = new AsyncThrottler(mockFn, { wait: 100 }) // First call executes immediately - await throttler.maybeExecute('first') + throttler.maybeExecute('first') expect(mockFn).toHaveBeenCalledTimes(1) expect(mockFn).toHaveBeenLastCalledWith('first') @@ -263,7 +194,7 @@ describe('AsyncThrottler', () => { const mockFn = vi.fn().mockResolvedValue(undefined) const throttler = new AsyncThrottler(mockFn, { wait: 100 }) - await throttler.maybeExecute('first') + throttler.maybeExecute('first') const promise = throttler.maybeExecute('second') throttler.cancel() await promise @@ -281,7 +212,7 @@ describe('AsyncThrottler', () => { const throttler = new AsyncThrottler(mockFn, { wait: 100 }) // First call should execute - await throttler.maybeExecute('first') + throttler.maybeExecute('first') expect(mockFn).toHaveBeenCalledTimes(1) expect(mockFn).toHaveBeenLastCalledWith('first') @@ -324,20 +255,22 @@ describe('AsyncThrottler', () => { const throttler = new AsyncThrottler(mockFn, { wait: 100 }) // First call should go through - await throttler.maybeExecute('first') + throttler.maybeExecute('first') + vi.advanceTimersByTime(100) expect(mockFn).toHaveBeenCalledTimes(1) expect(mockFn).toHaveBeenLastCalledWith('first') // Second call should be cancelled - const promise1 = throttler.maybeExecute('second') + throttler.maybeExecute('second') throttler.cancel() - await promise1 + vi.advanceTimersByTime(100) expect(mockFn).toHaveBeenCalledTimes(1) expect(mockFn).toHaveBeenLastCalledWith('first') // Third call should also be cancelled const promise2 = throttler.maybeExecute('third') throttler.cancel() + vi.advanceTimersByTime(100) await promise2 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 4708eb206..e1a8aeec3 100644 --- a/packages/pacer/tests/debouncer.test.ts +++ b/packages/pacer/tests/debouncer.test.ts @@ -10,424 +10,646 @@ describe('Debouncer', () => { vi.restoreAllMocks() }) - it('should not execute the function before the specified wait', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + describe('Basic Debouncing', () => { + it('should not execute the function before the specified wait', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - debouncer.maybeExecute() - expect(mockFn).not.toBeCalled() - }) + debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() + }) - it('should execute the function after the specified wait', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + it('should execute the function after the specified wait', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - debouncer.maybeExecute() - expect(mockFn).not.toBeCalled() + debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) - }) + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + }) - it('should debounce multiple calls', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + it('should debounce multiple calls', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - debouncer.maybeExecute() - debouncer.maybeExecute() - debouncer.maybeExecute() - expect(mockFn).not.toBeCalled() + debouncer.maybeExecute() + debouncer.maybeExecute() + debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) - }) + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + }) - it('should pass arguments to the debounced function', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + it('should pass arguments to the debounced function', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - debouncer.maybeExecute('test', 123) - vi.advanceTimersByTime(1000) + debouncer.maybeExecute('test', 123) + vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledWith('test', 123) + expect(mockFn).toBeCalledWith('test', 123) + }) }) - it('should cancel pending execution', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + describe('Execution Edge Cases', () => { + it('should execute immediately with leading option', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: true, + trailing: false, + }) - debouncer.maybeExecute() - debouncer.cancel() + debouncer.maybeExecute('test') + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('test') - vi.advanceTimersByTime(1000) - expect(mockFn).not.toBeCalled() - }) + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + }) - it('should execute immediately with leading option', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - leading: true, - trailing: false, + it('should respect leading edge timing', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: true, + trailing: false, + }) + + // First call - executes immediately + debouncer.maybeExecute('first') + expect(mockFn).toBeCalledTimes(1) + + // Call again before wait expires - should not execute + vi.advanceTimersByTime(500) + debouncer.maybeExecute('second') + expect(mockFn).toBeCalledTimes(1) + + // Advance to end of second call's wait period - should not execute + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + + // Now that the full wait has passed since last call, this should execute + debouncer.maybeExecute('third') + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toHaveBeenLastCalledWith('third') }) - debouncer.maybeExecute('test') - expect(mockFn).toBeCalledTimes(1) - expect(mockFn).toBeCalledWith('test') + it('should support both leading and trailing execution', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: true, + trailing: true, + }) - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) - }) + debouncer.maybeExecute('test1') + debouncer.maybeExecute('test2') + expect(mockFn).toBeCalledTimes(1) // Leading call - it('should respect leading edge timing', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - leading: true, - trailing: false, + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(2) // Trailing call }) - // First call - executes immediately - debouncer.maybeExecute('first') - expect(mockFn).toBeCalledTimes(1) + it('should default to trailing-only execution', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - // Call again before wait expires - should not execute - vi.advanceTimersByTime(500) - debouncer.maybeExecute('second') - expect(mockFn).toBeCalledTimes(1) + debouncer.maybeExecute('test1') + debouncer.maybeExecute('test2') + expect(mockFn).not.toBeCalled() - // Advance to end of second call's wait period - should not execute - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('test2') + }) - // Now that the full wait has passed since last call, this should execute - debouncer.maybeExecute('third') - expect(mockFn).toBeCalledTimes(2) - expect(mockFn).toHaveBeenLastCalledWith('third') - }) + it('should handle case where both leading and trailing are false', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: false, + trailing: false, + }) - it('should support both leading and trailing execution', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - leading: true, - }) + debouncer.maybeExecute('test') + expect(mockFn).not.toBeCalled() - debouncer.maybeExecute('test') - expect(mockFn).toBeCalledTimes(1) // Leading call + vi.advanceTimersByTime(1000) + expect(mockFn).not.toBeCalled() - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(2) // Trailing call + // Should still reset canLeadingExecute flag + debouncer.maybeExecute('test2') + expect(mockFn).not.toBeCalled() + }) }) - it('should default to trailing-only execution', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + describe('Execution Control', () => { + it('should cancel pending execution', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) + + debouncer.maybeExecute() + debouncer.cancel() - debouncer.maybeExecute('test') - expect(mockFn).not.toBeCalled() + vi.advanceTimersByTime(1000) + expect(mockFn).not.toBeCalled() + }) + + it('should properly handle canLeadingExecute flag after cancellation', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: true, + trailing: false, + }) + + // First call - executes immediately + debouncer.maybeExecute('first') + expect(mockFn).toBeCalledTimes(1) + + // Cancel before wait expires + vi.advanceTimersByTime(500) + debouncer.cancel() + + // Should be able to execute immediately again after cancellation + debouncer.maybeExecute('second') + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toHaveBeenLastCalledWith('second') + }) - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) - expect(mockFn).toBeCalledWith('test') + it('should handle rapid calls with leading edge execution', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: true, + trailing: false, + }) + + // Make rapid calls + debouncer.maybeExecute('first') + debouncer.maybeExecute('second') + debouncer.maybeExecute('third') + debouncer.maybeExecute('fourth') + + // Only first call should execute immediately + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') + + // Wait for timeout + vi.advanceTimersByTime(1000) + + // Next call should execute immediately + debouncer.maybeExecute('fifth') + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toHaveBeenLastCalledWith('fifth') + }) }) - it('should track execution count', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + describe('Enabled/Disabled State', () => { + it('should not execute when enabled is false', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + enabled: false, + }) - expect(debouncer.getExecutionCount()).toBe(0) + debouncer.maybeExecute('test') + vi.advanceTimersByTime(1000) + expect(mockFn).not.toBeCalled() + }) - debouncer.maybeExecute('test') - vi.advanceTimersByTime(1000) - expect(debouncer.getExecutionCount()).toBe(1) + it('should not execute leading edge when disabled', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: true, + enabled: false, + }) + + debouncer.maybeExecute('test') + expect(mockFn).not.toBeCalled() + vi.advanceTimersByTime(1000) + expect(mockFn).not.toBeCalled() + }) - debouncer.maybeExecute('test') - vi.advanceTimersByTime(1000) - expect(debouncer.getExecutionCount()).toBe(2) - }) + it('should default to enabled', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + }) - it('should track execution count with leading and trailing', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - leading: true, + debouncer.maybeExecute('test') + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('test') }) - expect(debouncer.getExecutionCount()).toBe(0) + it('should allow enabling/disabling after construction', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) + + // Start enabled by default + debouncer.maybeExecute('first') + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') + + // Disable and verify no execution + debouncer.setOptions({ enabled: false }) + debouncer.maybeExecute('second') + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) // Still only called once + + // Re-enable and verify execution resumes + debouncer.setOptions({ enabled: true }) + debouncer.maybeExecute('third') + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toHaveBeenLastCalledWith('third') + }) - debouncer.maybeExecute('test') - expect(debouncer.getExecutionCount()).toBe(1) // Leading execution + it('should allow disabling mid-wait', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - vi.advanceTimersByTime(1000) - expect(debouncer.getExecutionCount()).toBe(2) // Trailing execution + debouncer.maybeExecute('test') + vi.advanceTimersByTime(500) // Half-way through wait + debouncer.setOptions({ enabled: false }) + vi.advanceTimersByTime(500) // Complete wait + expect(mockFn).not.toBeCalled() + }) }) - it('should not increment count when execution is cancelled', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + describe('Options Management', () => { + it('should allow updating multiple options at once', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) + + // Update both wait time and leading option + debouncer.setOptions({ wait: 500, leading: true }) - debouncer.maybeExecute('test') - debouncer.cancel() - vi.advanceTimersByTime(1000) + // Verify new leading behavior + debouncer.maybeExecute('test1') + debouncer.maybeExecute('test2') + expect(mockFn).toBeCalledTimes(1) // Immediate execution due to leading: true - expect(debouncer.getExecutionCount()).toBe(0) + // Verify new wait time + vi.advanceTimersByTime(500) // Only need to wait 500ms now + expect(mockFn).toBeCalledTimes(2) // Trailing execution after shorter wait + }) }) - it('should handle case where both leading and trailing are false', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - leading: false, - trailing: false, + describe('State Tracking', () => { + it('should track execution count', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) + + expect(debouncer.getExecutionCount()).toBe(0) + + debouncer.maybeExecute('test') + vi.advanceTimersByTime(1000) + expect(debouncer.getExecutionCount()).toBe(1) + + debouncer.maybeExecute('test') + vi.advanceTimersByTime(1000) + expect(debouncer.getExecutionCount()).toBe(2) }) - debouncer.maybeExecute('test') - expect(mockFn).not.toBeCalled() + it('should track execution count with leading and trailing', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: true, + }) - vi.advanceTimersByTime(1000) - expect(mockFn).not.toBeCalled() + expect(debouncer.getExecutionCount()).toBe(0) - // Should still reset canLeadingExecute flag - debouncer.maybeExecute('test2') - expect(mockFn).not.toBeCalled() - }) + debouncer.maybeExecute('test') + debouncer.maybeExecute('test2') + expect(debouncer.getExecutionCount()).toBe(1) // Leading execution - it('should properly handle canLeadingExecute flag after cancellation', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - leading: true, - trailing: false, + vi.advanceTimersByTime(1000) + expect(debouncer.getExecutionCount()).toBe(2) // Trailing execution }) - // First call - executes immediately - debouncer.maybeExecute('first') - expect(mockFn).toBeCalledTimes(1) + it('should not increment count when execution is cancelled', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - // Cancel before wait expires - vi.advanceTimersByTime(500) - debouncer.cancel() + debouncer.maybeExecute('test') + debouncer.cancel() + vi.advanceTimersByTime(1000) - // Should be able to execute immediately again after cancellation - debouncer.maybeExecute('second') - expect(mockFn).toBeCalledTimes(2) - expect(mockFn).toHaveBeenLastCalledWith('second') + expect(debouncer.getExecutionCount()).toBe(0) + }) }) - it('should handle rapid calls with leading edge execution', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - leading: true, - trailing: false, + describe('Pending State', () => { + it('should update pending when trailing-only', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + trailing: true, + leading: false, + }) + + debouncer.maybeExecute('test') + expect(debouncer.getIsPending()).toBe(true) + + // Call again before wait expires + vi.advanceTimersByTime(500) + debouncer.maybeExecute('test') // Should reset pending + + // Time is almost up + vi.advanceTimersByTime(900) + expect(debouncer.getIsPending()).toBe(true) // Still pending + + vi.advanceTimersByTime(100) + expect(debouncer.getIsPending()).toBe(false) // Now it's done }) - // Make rapid calls - debouncer.maybeExecute('first') - debouncer.maybeExecute('second') - debouncer.maybeExecute('third') - debouncer.maybeExecute('fourth') + it('should never be pending when trailing is false', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: true, + trailing: false, + }) - // Only first call should execute immediately - expect(mockFn).toBeCalledTimes(1) - expect(mockFn).toBeCalledWith('first') + debouncer.maybeExecute('test1') + expect(debouncer.getIsPending()).toBe(false) - // Wait for timeout - vi.advanceTimersByTime(1000) + // Call again before wait expires + vi.advanceTimersByTime(500) + debouncer.maybeExecute('test2') - // Next call should execute immediately - debouncer.maybeExecute('fifth') - expect(mockFn).toBeCalledTimes(2) - expect(mockFn).toHaveBeenLastCalledWith('fifth') - }) + // Time is almost up + vi.advanceTimersByTime(900) + expect(debouncer.getIsPending()).toBe(false) - it('should not execute when enabled is false', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - enabled: false, + vi.advanceTimersByTime(100) + expect(debouncer.getIsPending()).toBe(false) }) - debouncer.maybeExecute('test') - vi.advanceTimersByTime(1000) - expect(mockFn).not.toBeCalled() - }) + it('should not be pending when leading and trailing are both false', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: false, + trailing: false, + }) + + debouncer.maybeExecute('test') + expect(debouncer.getIsPending()).toBe(false) - it('should not execute leading edge when disabled', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - leading: true, - enabled: false, + vi.advanceTimersByTime(1000) + expect(debouncer.getIsPending()).toBe(false) }) - debouncer.maybeExecute('test') - expect(mockFn).not.toBeCalled() - vi.advanceTimersByTime(1000) - expect(mockFn).not.toBeCalled() - }) + it('should not be pending when disabled', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000, enabled: false }) + + debouncer.maybeExecute('test') + expect(debouncer.getIsPending()).toBe(false) - it('should default to enabled', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, + vi.advanceTimersByTime(1000) + expect(debouncer.getIsPending()).toBe(false) }) - debouncer.maybeExecute('test') - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) - expect(mockFn).toBeCalledWith('test') - }) + it('should update pending when enabling/disabling', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - it('should allow enabling/disabling after construction', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) - - // Start enabled by default - debouncer.maybeExecute('first') - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) - expect(mockFn).toBeCalledWith('first') - - // Disable and verify no execution - debouncer.setOptions({ enabled: false }) - debouncer.maybeExecute('second') - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) // Still only called once - - // Re-enable and verify execution resumes - debouncer.setOptions({ enabled: true }) - debouncer.maybeExecute('third') - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(2) - expect(mockFn).toHaveBeenLastCalledWith('third') - }) + debouncer.maybeExecute('test') + expect(debouncer.getIsPending()).toBe(true) + + // Disable while there is a pending execution + debouncer.setOptions({ enabled: false }) + expect(debouncer.getIsPending()).toBe(false) // Should be false now + + // Re-enable + debouncer.setOptions({ enabled: true }) + expect(debouncer.getIsPending()).toBe(false) // Should still be false + }) - it('should allow disabling mid-wait', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + it('should set pending to false when canceled', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - debouncer.maybeExecute('test') - vi.advanceTimersByTime(500) // Half-way through wait - debouncer.setOptions({ enabled: false }) - vi.advanceTimersByTime(500) // Complete wait - expect(mockFn).not.toBeCalled() + debouncer.maybeExecute('test') + expect(debouncer.getIsPending()).toBe(true) + + debouncer.cancel() + expect(debouncer.getIsPending()).toBe(false) + }) }) - it('should allow updating multiple options at once', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + describe('onExecute Callback', () => { + it('should call onExecute callback after execution', () => { + const mockFn = vi.fn() + const onExecute = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + onExecute, + }) + + debouncer.maybeExecute() + expect(onExecute).not.toBeCalled() + + vi.advanceTimersByTime(1000) + expect(onExecute).toBeCalledTimes(1) + expect(onExecute).toBeCalledWith(debouncer) + }) + + it('should call onExecute callback with leading execution', () => { + const mockFn = vi.fn() + const onExecute = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + leading: true, + onExecute, + }) + + debouncer.maybeExecute() + expect(onExecute).toBeCalledTimes(1) + expect(onExecute).toBeCalledWith(debouncer) + + vi.advanceTimersByTime(1000) + expect(onExecute).toBeCalledTimes(1) // Should not be called again + }) - // Update both wait time and leading option - debouncer.setOptions({ wait: 500, leading: true }) + it('should not call onExecute callback when disabled', () => { + const mockFn = vi.fn() + const onExecute = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + enabled: false, + onExecute, + }) + + debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + expect(onExecute).not.toBeCalled() + }) + + it('should not call onExecute callback when cancelled', () => { + const mockFn = vi.fn() + const onExecute = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + onExecute, + }) + + debouncer.maybeExecute() + debouncer.cancel() + vi.advanceTimersByTime(1000) + expect(onExecute).not.toBeCalled() + }) - // Verify new leading behavior - debouncer.maybeExecute('test') - expect(mockFn).toBeCalledTimes(1) // Immediate execution due to leading: true + it('should call onExecute callback with correct debouncer instance', () => { + const mockFn = vi.fn() + const onExecute = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + onExecute, + }) + + debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + expect(onExecute).toBeCalledWith(debouncer) + expect(onExecute.mock.calls[0]?.[0]).toBe(debouncer) + }) - // Verify new wait time - vi.advanceTimersByTime(500) // Only need to wait 500ms now - expect(mockFn).toBeCalledTimes(2) // Trailing execution after shorter wait + it('should call onExecute callback after each execution', () => { + const mockFn = vi.fn() + const onExecute = vi.fn() + const debouncer = new Debouncer(mockFn, { + wait: 1000, + onExecute, + }) + + // First execution + debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + expect(onExecute).toBeCalledTimes(1) + + // Second execution + debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + expect(onExecute).toBeCalledTimes(2) + }) }) - it('should update pending when trailing-only', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - trailing: true, - leading: false, + describe('Edge Cases and Error Handling', () => { + it('should handle wait time of 0', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 0 }) + + debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() + + vi.advanceTimersByTime(0) + expect(mockFn).toBeCalledTimes(1) }) - debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(true) + it('should handle negative wait time by using 0', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: -1000 }) + + debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() - // Call again before wait expires - vi.advanceTimersByTime(500) - debouncer.maybeExecute('test') // Should reset pending + vi.advanceTimersByTime(0) + expect(mockFn).toBeCalledTimes(1) + }) - // Time is almost up - vi.advanceTimersByTime(900) - expect(debouncer.getIsPending()).toBe(true) // Still pending + it('should handle very large wait times', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: Number.MAX_SAFE_INTEGER }) - vi.advanceTimersByTime(100) - expect(debouncer.getIsPending()).toBe(false) // Now it's done - }) + debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() - it('should update pending when leading-only', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - leading: true, - trailing: false, + vi.advanceTimersByTime(Number.MAX_SAFE_INTEGER) + expect(mockFn).toBeCalledTimes(1) }) - debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(true) + it('should handle NaN wait time by using 0', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: NaN }) - // Call again before wait expires - vi.advanceTimersByTime(500) - debouncer.maybeExecute('test') // Should reset pending + debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() - // Time is almost up - vi.advanceTimersByTime(900) - expect(debouncer.getIsPending()).toBe(true) // Still pending + vi.advanceTimersByTime(0) + expect(mockFn).toBeCalledTimes(1) + }) - vi.advanceTimersByTime(100) - expect(debouncer.getIsPending()).toBe(false) // Now it's done - }) + it('should handle undefined/null arguments', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - it('should not be pending when leading and trailing are both false', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { - wait: 1000, - leading: false, - trailing: false, + debouncer.maybeExecute(undefined, null) + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledWith(undefined, null) }) - debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(false) + it('should prevent memory leaks by clearing timeouts', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - vi.advanceTimersByTime(1000) - expect(debouncer.getIsPending()).toBe(false) - }) + // Create multiple pending executions + debouncer.maybeExecute() + debouncer.maybeExecute() + debouncer.maybeExecute() - it('should not be pending when disabled', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000, enabled: false }) + // Cancel all pending executions + debouncer.cancel() - debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(false) + // Advance time to ensure no executions occur + vi.advanceTimersByTime(1000) + expect(mockFn).not.toBeCalled() + }) - vi.advanceTimersByTime(1000) - expect(debouncer.getIsPending()).toBe(false) - }) + it('should handle rapid option changes', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - it('should update pending when enabling/disabling', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + // Start execution + debouncer.maybeExecute() - debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(true) + // Rapidly change options + debouncer.setOptions({ wait: 500 }) + debouncer.setOptions({ wait: 2000 }) + debouncer.setOptions({ wait: 1000 }) - // Disable while there is a pending execution - debouncer.setOptions({ enabled: false }) - expect(debouncer.getIsPending()).toBe(false) // Should be false now + // Should still execute after the last wait time + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + }) - // Re-enable - debouncer.setOptions({ enabled: true }) - expect(debouncer.getIsPending()).toBe(false) // Should still be false - }) + it('should handle rapid enable/disable cycles', () => { + const mockFn = vi.fn() + const debouncer = new Debouncer(mockFn, { wait: 1000 }) - it('should set pending to false when canceled', () => { - const mockFn = vi.fn() - const debouncer = new Debouncer(mockFn, { wait: 1000 }) + // Start execution + debouncer.maybeExecute() - debouncer.maybeExecute('test') - expect(debouncer.getIsPending()).toBe(true) + // Rapidly enable/disable + debouncer.setOptions({ enabled: false }) + debouncer.setOptions({ enabled: true }) + debouncer.setOptions({ enabled: false }) + debouncer.setOptions({ enabled: true }) - debouncer.cancel() - expect(debouncer.getIsPending()).toBe(false) + // Should execute if last state was enabled + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + }) }) }) @@ -440,85 +662,89 @@ describe('debounce helper function', () => { vi.restoreAllMocks() }) - it('should create a debounced function with default options', () => { - const mockFn = vi.fn() - const debouncedFn = debounce(mockFn, { wait: 1000 }) + describe('Basic Functionality', () => { + it('should create a debounced function with default options', () => { + const mockFn = vi.fn() + const debouncedFn = debounce(mockFn, { wait: 1000 }) - debouncedFn('test') - expect(mockFn).not.toBeCalled() + debouncedFn('test') + expect(mockFn).not.toBeCalled() - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) - expect(mockFn).toBeCalledWith('test') - }) + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('test') + }) - it('should pass arguments correctly', () => { - const mockFn = vi.fn() - const debouncedFn = debounce(mockFn, { wait: 1000 }) + it('should pass arguments correctly', () => { + const mockFn = vi.fn() + const debouncedFn = debounce(mockFn, { wait: 1000 }) - debouncedFn(42, 'test', { foo: 'bar' }) - vi.advanceTimersByTime(1000) + debouncedFn(42, 'test', { foo: 'bar' }) + vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledWith(42, 'test', { foo: 'bar' }) - }) - - it('should respect leading option', () => { - const mockFn = vi.fn() - const debouncedFn = debounce(mockFn, { - wait: 1000, - leading: true, - trailing: false, + expect(mockFn).toBeCalledWith(42, 'test', { foo: 'bar' }) }) + }) - debouncedFn('first') - expect(mockFn).toBeCalledTimes(1) - expect(mockFn).toBeCalledWith('first') + describe('Execution Options', () => { + it('should respect leading option', () => { + const mockFn = vi.fn() + const debouncedFn = debounce(mockFn, { + wait: 1000, + leading: true, + trailing: false, + }) - debouncedFn('second') - expect(mockFn).toBeCalledTimes(1) + debouncedFn('first') + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) + debouncedFn('second') + expect(mockFn).toBeCalledTimes(1) - debouncedFn('third') - expect(mockFn).toBeCalledTimes(2) - expect(mockFn).toHaveBeenLastCalledWith('third') - }) + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) - it('should handle multiple calls with trailing edge', () => { - const mockFn = vi.fn() - const debouncedFn = debounce(mockFn, { wait: 1000 }) + debouncedFn('third') + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toHaveBeenLastCalledWith('third') + }) - debouncedFn('a') - debouncedFn('b') - debouncedFn('c') - expect(mockFn).not.toBeCalled() + it('should handle multiple calls with trailing edge', () => { + const mockFn = vi.fn() + const debouncedFn = debounce(mockFn, { wait: 1000 }) - vi.advanceTimersByTime(500) - debouncedFn('d') - expect(mockFn).not.toBeCalled() + debouncedFn('a') + debouncedFn('b') + debouncedFn('c') + expect(mockFn).not.toBeCalled() - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(1) - expect(mockFn).toBeCalledWith('d') - }) + vi.advanceTimersByTime(500) + debouncedFn('d') + expect(mockFn).not.toBeCalled() - it('should support both leading and trailing execution', () => { - const mockFn = vi.fn() - const debouncedFn = debounce(mockFn, { - wait: 1000, - leading: true, + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('d') }) - debouncedFn('first') - expect(mockFn).toBeCalledTimes(1) - expect(mockFn).toBeCalledWith('first') + it('should support both leading and trailing execution', () => { + const mockFn = vi.fn() + const debouncedFn = debounce(mockFn, { + wait: 1000, + leading: true, + }) - debouncedFn('second') - expect(mockFn).toBeCalledTimes(1) + debouncedFn('first') + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') - vi.advanceTimersByTime(1000) - expect(mockFn).toBeCalledTimes(2) - expect(mockFn).toHaveBeenLastCalledWith('second') + debouncedFn('second') + expect(mockFn).toBeCalledTimes(1) + + vi.advanceTimersByTime(1000) + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toHaveBeenLastCalledWith('second') + }) }) }) diff --git a/packages/react-pacer/package.json b/packages/react-pacer/package.json index ad38dcd6b..ee35e83f0 100644 --- a/packages/react-pacer/package.json +++ b/packages/react-pacer/package.json @@ -154,7 +154,7 @@ "@tanstack/pacer": "workspace:*" }, "devDependencies": { - "@eslint-react/eslint-plugin": "^1.48.4", + "@eslint-react/eslint-plugin": "^1.48.5", "@types/react": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", "eslint-plugin-react-compiler": "19.1.0-rc.1", diff --git a/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts b/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts index f7bc8d8c4..3341a1c08 100644 --- a/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts +++ b/packages/react-pacer/src/async-debouncer/useAsyncDebouncer.ts @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { AsyncDebouncer } from '@tanstack/pacer/async-debouncer' import { bindInstanceMethods } from '@tanstack/pacer/utils' import type { AnyAsyncFunction } from '@tanstack/pacer/types' @@ -39,18 +39,21 @@ import type { AsyncDebouncerOptions } from '@tanstack/pacer/async-debouncer' * ``` */ -export function useAsyncDebouncer< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, ->( +export function useAsyncDebouncer( fn: TFn, - options: AsyncDebouncerOptions, -): AsyncDebouncer { + options: AsyncDebouncerOptions, +): AsyncDebouncer { const [asyncDebouncer] = useState(() => - bindInstanceMethods(new AsyncDebouncer(fn, options)), + bindInstanceMethods(new AsyncDebouncer(fn, options)), ) asyncDebouncer.setOptions(options) + useEffect(() => { + return () => { + asyncDebouncer.cancel() + } + }, [asyncDebouncer]) + return asyncDebouncer } diff --git a/packages/react-pacer/src/async-queuer/index.ts b/packages/react-pacer/src/async-queuer/index.ts index 800d1f84a..aed55d498 100644 --- a/packages/react-pacer/src/async-queuer/index.ts +++ b/packages/react-pacer/src/async-queuer/index.ts @@ -1,4 +1,4 @@ export * from '@tanstack/pacer/async-queuer' export * from './useAsyncQueuer' -export * from './useAsyncQueuerState' +export * from './useAsyncQueuedState' diff --git a/packages/react-pacer/src/async-queuer/useAsyncQueuerState.ts b/packages/react-pacer/src/async-queuer/useAsyncQueuedState.ts similarity index 94% rename from packages/react-pacer/src/async-queuer/useAsyncQueuerState.ts rename to packages/react-pacer/src/async-queuer/useAsyncQueuedState.ts index 77d11673c..6ab57ad7f 100644 --- a/packages/react-pacer/src/async-queuer/useAsyncQueuerState.ts +++ b/packages/react-pacer/src/async-queuer/useAsyncQueuedState.ts @@ -28,7 +28,7 @@ import type { * @example * ```tsx * // Create a queue with state management - * const [queueItems, asyncQueuer] = useAsyncQueuerState({ + * const [queueItems, asyncQueuer] = useAsyncQueuedState({ * concurrency: 2, * maxSize: 100, * started: true @@ -50,7 +50,7 @@ import type { * const pendingCount = asyncQueuer.getPendingItems().length; * ``` */ -export function useAsyncQueuerState( +export function useAsyncQueuedState( options: AsyncQueuerOptions = {}, ): [Array<() => Promise>, AsyncQueuer] { const [items, setItems] = useState Promise>>( diff --git a/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts b/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts index c20436c2f..b26629874 100644 --- a/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts +++ b/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts @@ -40,15 +40,12 @@ import type { AsyncRateLimiterOptions } from '@tanstack/pacer/async-rate-limiter * ); * ``` */ -export function useAsyncRateLimiter< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, ->( +export function useAsyncRateLimiter( fn: TFn, - options: AsyncRateLimiterOptions, -): AsyncRateLimiter { + options: AsyncRateLimiterOptions, +): AsyncRateLimiter { const [asyncRateLimiter] = useState(() => - bindInstanceMethods(new AsyncRateLimiter(fn, options)), + bindInstanceMethods(new AsyncRateLimiter(fn, options)), ) asyncRateLimiter.setOptions(options) diff --git a/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts b/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts index 515961935..ec4cf0fb9 100644 --- a/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts +++ b/packages/react-pacer/src/async-throttler/useAsyncThrottler.ts @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { AsyncThrottler } from '@tanstack/pacer/async-throttler' import { bindInstanceMethods } from '@tanstack/pacer/utils' import type { AnyAsyncFunction } from '@tanstack/pacer/types' @@ -41,18 +41,19 @@ import type { AsyncThrottlerOptions } from '@tanstack/pacer/async-throttler' * ``` */ -export function useAsyncThrottler< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, ->( +export function useAsyncThrottler( fn: TFn, - options: AsyncThrottlerOptions, -): AsyncThrottler { + options: AsyncThrottlerOptions, +): AsyncThrottler { const [asyncThrottler] = useState(() => - bindInstanceMethods(new AsyncThrottler(fn, options)), + bindInstanceMethods(new AsyncThrottler(fn, options)), ) asyncThrottler.setOptions(options) + useEffect(() => { + return () => asyncThrottler.cancel() + }, [asyncThrottler]) + return asyncThrottler } diff --git a/packages/react-pacer/src/debouncer/useDebouncedCallback.ts b/packages/react-pacer/src/debouncer/useDebouncedCallback.ts index 587e5ccd8..01047a989 100644 --- a/packages/react-pacer/src/debouncer/useDebouncedCallback.ts +++ b/packages/react-pacer/src/debouncer/useDebouncedCallback.ts @@ -39,10 +39,13 @@ import type { AnyFunction } from '@tanstack/pacer/types' * /> * ``` */ -export function useDebouncedCallback< - TFn extends AnyFunction, - TArgs extends Parameters, ->(fn: TFn, options: DebouncerOptions) { - const debouncedFn = useDebouncer(fn, options).maybeExecute - return useCallback((...args: TArgs) => debouncedFn(...args), [debouncedFn]) +export function useDebouncedCallback( + fn: TFn, + options: DebouncerOptions, +) { + const debouncedFn = useDebouncer(fn, options).maybeExecute + return useCallback( + (...args: Parameters) => debouncedFn(...args), + [debouncedFn], + ) } diff --git a/packages/react-pacer/src/debouncer/useDebouncedState.ts b/packages/react-pacer/src/debouncer/useDebouncedState.ts index 0b5009ea0..24ac87a0d 100644 --- a/packages/react-pacer/src/debouncer/useDebouncedState.ts +++ b/packages/react-pacer/src/debouncer/useDebouncedState.ts @@ -37,14 +37,11 @@ import type { Debouncer, DebouncerOptions } from '@tanstack/pacer/debouncer' */ export function useDebouncedState( value: TValue, - options: DebouncerOptions< - React.Dispatch>, - [value: React.SetStateAction] - >, + options: DebouncerOptions>>, ): [ TValue, React.Dispatch>, - Debouncer>, [TValue]>, + Debouncer>>, ] { const [debouncedValue, setDebouncedValue] = useState(value) const debouncer = useDebouncer(setDebouncedValue, options) diff --git a/packages/react-pacer/src/debouncer/useDebouncedValue.ts b/packages/react-pacer/src/debouncer/useDebouncedValue.ts index 7bee5828f..8c37cface 100644 --- a/packages/react-pacer/src/debouncer/useDebouncedValue.ts +++ b/packages/react-pacer/src/debouncer/useDebouncedValue.ts @@ -15,9 +15,9 @@ import type { Debouncer, DebouncerOptions } from '@tanstack/pacer/debouncer' * like search queries or form inputs, where you want to limit how often downstream effects * or calculations occur. * - * The hook returns a tuple containing: - * - The current debounced value - * - The debouncer instance with control methods + * The hook returns the current debounced value and the underlying debouncer instance. + * The debouncer instance can be used to access additional functionality like cancellation + * and execution counts. * * @example * ```tsx @@ -40,11 +40,8 @@ import type { Debouncer, DebouncerOptions } from '@tanstack/pacer/debouncer' */ export function useDebouncedValue( value: TValue, - options: DebouncerOptions< - React.Dispatch>, - [value: React.SetStateAction] - >, -): [TValue, Debouncer>, [TValue]>] { + options: DebouncerOptions>>, +): [TValue, Debouncer>>] { const [debouncedValue, setDebouncedValue, debouncer] = useDebouncedState( value, options, @@ -52,10 +49,7 @@ export function useDebouncedValue( useEffect(() => { setDebouncedValue(value) - return () => { - debouncer.cancel() - } - }, [value, setDebouncedValue, debouncer]) + }, [value, setDebouncedValue]) return [debouncedValue, debouncer] } diff --git a/packages/react-pacer/src/debouncer/useDebouncer.ts b/packages/react-pacer/src/debouncer/useDebouncer.ts index ddf132fdb..81bc3c56c 100644 --- a/packages/react-pacer/src/debouncer/useDebouncer.ts +++ b/packages/react-pacer/src/debouncer/useDebouncer.ts @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Debouncer } from '@tanstack/pacer/debouncer' import { bindInstanceMethods } from '@tanstack/pacer/utils' import type { DebouncerOptions } from '@tanstack/pacer/debouncer' @@ -39,15 +39,21 @@ import type { AnyFunction } from '@tanstack/pacer/types' * const isPending = searchDebouncer.getIsPending(); * ``` */ -export function useDebouncer< - TFn extends AnyFunction, - TArgs extends Parameters, ->(fn: TFn, options: DebouncerOptions): Debouncer { +export function useDebouncer( + fn: TFn, + options: DebouncerOptions, +): Debouncer { const [debouncer] = useState(() => - bindInstanceMethods(new Debouncer(fn, options)), + bindInstanceMethods(new Debouncer(fn, options)), ) debouncer.setOptions(options) + useEffect(() => { + return () => { + debouncer.cancel() + } + }, [debouncer]) + return debouncer } diff --git a/packages/react-pacer/src/index.ts b/packages/react-pacer/src/index.ts index c69e62b4b..a0c6443f3 100644 --- a/packages/react-pacer/src/index.ts +++ b/packages/react-pacer/src/index.ts @@ -10,7 +10,7 @@ export * from './async-debouncer/useAsyncDebouncer' // async-queuer export * from './async-queuer/useAsyncQueuer' -export * from './async-queuer/useAsyncQueuerState' +export * from './async-queuer/useAsyncQueuedState' // async-rate-limiter export * from './async-rate-limiter/useAsyncRateLimiter' @@ -26,7 +26,8 @@ export * from './debouncer/useDebouncer' // queuer export * from './queuer/useQueuer' -export * from './queuer/useQueuerState' +export * from './queuer/useQueuedState' +export * from './queuer/useQueuedValue' // rate-limiter export * from './rate-limiter/useRateLimitedCallback' diff --git a/packages/react-pacer/src/queuer/index.ts b/packages/react-pacer/src/queuer/index.ts index b70fe512c..6809bb174 100644 --- a/packages/react-pacer/src/queuer/index.ts +++ b/packages/react-pacer/src/queuer/index.ts @@ -1,4 +1,5 @@ export * from '@tanstack/pacer/queuer' export * from './useQueuer' -export * from './useQueuerState' +export * from './useQueuedState' +export * from './useQueuedValue' diff --git a/packages/react-pacer/src/queuer/useQueuerState.ts b/packages/react-pacer/src/queuer/useQueuedState.ts similarity index 91% rename from packages/react-pacer/src/queuer/useQueuerState.ts rename to packages/react-pacer/src/queuer/useQueuedState.ts index 63c025ae7..5957810a6 100644 --- a/packages/react-pacer/src/queuer/useQueuerState.ts +++ b/packages/react-pacer/src/queuer/useQueuedState.ts @@ -20,7 +20,7 @@ import type { Queuer, QueuerOptions } from '@tanstack/pacer/queuer' * @example * ```tsx * // Basic queue with initial items and priority - * const [items, queue] = useQueuerState({ + * const [items, queue] = useQueuedState({ * initialItems: ['item1', 'item2'], * started: true, * wait: 1000, @@ -51,9 +51,9 @@ import type { Queuer, QueuerOptions } from '@tanstack/pacer/queuer' * }; * ``` */ -export function useQueuerState( +export function useQueuedState( options: QueuerOptions = {}, -): [Array, Queuer] { +): [Array, Queuer['addItem'], Queuer] { const [allItems, setAllItems] = useState>( options.initialItems || [], ) @@ -70,5 +70,5 @@ export function useQueuerState( }, }) - return [allItems, queue] + return [allItems, queue.addItem, queue] } diff --git a/packages/react-pacer/src/queuer/useQueuedValue.ts b/packages/react-pacer/src/queuer/useQueuedValue.ts new file mode 100644 index 000000000..400b6dd20 --- /dev/null +++ b/packages/react-pacer/src/queuer/useQueuedValue.ts @@ -0,0 +1,60 @@ +import { useEffect, useState } from 'react' +import { useQueuedState } from './useQueuedState' +import type { Queuer, QueuerOptions } from '@tanstack/pacer/queuer' + +/** + * 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. + * + * The queued value will process changes in the order they are received, with optional delays between + * processing each change. This is useful for handling state updates that need to be processed + * in a specific order, like animations or sequential UI updates. + * + * The hook returns a tuple containing: + * - The current queued value + * - The queuer instance with control methods + * + * @example + * ```tsx + * // Queue state changes with a delay between each + * const [value, queuer] = useQueuedValue(initialValue, { + * wait: 500, // Wait 500ms between processing each change + * started: true // Start processing immediately + * }); + * + * // Add changes to the queue + * const handleChange = (newValue) => { + * queuer.addItem(newValue); + * }; + * + * // Control the queue + * const pauseProcessing = () => { + * queuer.stop(); + * }; + * + * const resumeProcessing = () => { + * queuer.start(); + * }; + * ``` + */ +export function useQueuedValue( + initialValue: TValue, + options: QueuerOptions = {}, +): [TValue, Queuer] { + const [value, setValue] = useState(initialValue) + + const [, addItem, queuer] = useQueuedState({ + started: true, + ...options, + onGetNextItem: (item, queuer) => { + setValue(item) + options.onGetNextItem?.(item, queuer) + }, + }) + + useEffect(() => { + addItem(initialValue) + }, [initialValue, addItem]) + + return [value, queuer] +} diff --git a/packages/react-pacer/src/queuer/useQueuer.ts b/packages/react-pacer/src/queuer/useQueuer.ts index 8281bdc80..efbaa595f 100644 --- a/packages/react-pacer/src/queuer/useQueuer.ts +++ b/packages/react-pacer/src/queuer/useQueuer.ts @@ -10,7 +10,7 @@ import type { QueuerOptions } from '@tanstack/pacer/queuer' * any built-in state management. This allows you to integrate it with any state management solution * you prefer (useState, Redux, Zustand, etc.) by utilizing the onItemsChange callback. * - * For a hook with built-in state management, see useQueuerState. + * For a hook with built-in state management, see useQueuedState. * * The Queuer extends the base Queue to add processing capabilities. Items are processed * synchronously in order, with optional delays between processing each item. The queuer includes diff --git a/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts b/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts index bf3fb11a6..3697906a4 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts @@ -52,8 +52,8 @@ 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 +>(fn: TFn, options: RateLimiterOptions) { + const rateLimitedFn = useRateLimiter(fn, options).maybeExecute return useCallback( (...args: TArgs) => rateLimitedFn(...args), [rateLimitedFn], diff --git a/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts b/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts index c681304cc..d0b39e118 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts @@ -58,14 +58,11 @@ import type { export function useRateLimitedState( value: TValue, - options: RateLimiterOptions< - React.Dispatch>, - [value: React.SetStateAction] - >, + options: RateLimiterOptions>>, ): [ TValue, React.Dispatch>, - RateLimiter>, [TValue]>, + RateLimiter>>, ] { const [rateLimitedValue, setRateLimitedValue] = useState(value) const rateLimiter = useRateLimiter(setRateLimitedValue, options) diff --git a/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts b/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts index 06aa5aaa9..ffdc3ef3f 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts @@ -19,7 +19,9 @@ import type { * * Rate limiting should primarily be used when you need to enforce strict limits, like API rate limits. * - * The hook returns both the rate-limited value and the underlying rateLimiter instance for additional control. + * The hook returns a tuple containing: + * - The rate-limited value that updates according to the configured rate limit + * - The rate limiter instance with control methods * * For more direct control over rate limiting behavior without React state management, * consider using the lower-level useRateLimiter hook instead. @@ -27,7 +29,7 @@ import type { * @example * ```tsx * // Basic rate limiting - update at most 5 times per minute - * const [rateLimitedValue] = useRateLimitedValue(rawValue, { + * const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { * limit: 5, * window: 60000 * }); @@ -40,28 +42,12 @@ import type { * console.log(`Update rejected. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); * } * }); - * - * // Optionally access rateLimiter methods - * const handleSubmit = () => { - * const remaining = rateLimiter.getRemainingInWindow(); - * if (remaining > 0) { - * console.log(`${remaining} updates remaining in this window`); - * } else { - * console.log('Rate limit reached for this window'); - * } - * }; * ``` */ export function useRateLimitedValue( value: TValue, - options: RateLimiterOptions< - React.Dispatch>, - [value: React.SetStateAction] - >, -): [ - TValue, - RateLimiter>, [TValue]>, -] { + options: RateLimiterOptions>>, +): [TValue, RateLimiter>>] { const [rateLimitedValue, setRateLimitedValue, rateLimiter] = useRateLimitedState(value, options) diff --git a/packages/react-pacer/src/rate-limiter/useRateLimiter.ts b/packages/react-pacer/src/rate-limiter/useRateLimiter.ts index 41f9afc3f..7ee2a58de 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimiter.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimiter.ts @@ -45,12 +45,12 @@ import type { AnyFunction } from '@tanstack/pacer/types' * }; * ``` */ -export function useRateLimiter< - TFn extends AnyFunction, - TArgs extends Parameters, ->(fn: TFn, options: RateLimiterOptions): RateLimiter { +export function useRateLimiter( + fn: TFn, + options: RateLimiterOptions, +): RateLimiter { const [rateLimiter] = useState(() => - bindInstanceMethods(new RateLimiter(fn, options)), + bindInstanceMethods(new RateLimiter(fn, options)), ) rateLimiter.setOptions(options) diff --git a/packages/react-pacer/src/throttler/useThrottledCallback.ts b/packages/react-pacer/src/throttler/useThrottledCallback.ts index 9d23141c3..d0395ea76 100644 --- a/packages/react-pacer/src/throttler/useThrottledCallback.ts +++ b/packages/react-pacer/src/throttler/useThrottledCallback.ts @@ -43,7 +43,7 @@ import type { AnyFunction } from '@tanstack/pacer/types' export function useThrottledCallback< TFn extends AnyFunction, TArgs extends Parameters, ->(fn: TFn, options: ThrottlerOptions) { - const throttledFn = useThrottler(fn, options).maybeExecute +>(fn: TFn, options: ThrottlerOptions) { + const throttledFn = useThrottler(fn, options).maybeExecute return useCallback((...args: TArgs) => throttledFn(...args), [throttledFn]) } diff --git a/packages/react-pacer/src/throttler/useThrottledState.ts b/packages/react-pacer/src/throttler/useThrottledState.ts index 5b58e8f0a..78d0e6b39 100644 --- a/packages/react-pacer/src/throttler/useThrottledState.ts +++ b/packages/react-pacer/src/throttler/useThrottledState.ts @@ -39,14 +39,11 @@ import type { Throttler, ThrottlerOptions } from '@tanstack/pacer/throttler' export function useThrottledState( value: TValue, - options: ThrottlerOptions< - React.Dispatch>, - [value: React.SetStateAction] - >, + options: ThrottlerOptions>>, ): [ TValue, React.Dispatch>, - Throttler>, [TValue]>, + Throttler>>, ] { const [throttledValue, setThrottledValue] = useState(value) const throttler = useThrottler(setThrottledValue, options) diff --git a/packages/react-pacer/src/throttler/useThrottledValue.ts b/packages/react-pacer/src/throttler/useThrottledValue.ts index 4c6253b99..30f6be8ab 100644 --- a/packages/react-pacer/src/throttler/useThrottledValue.ts +++ b/packages/react-pacer/src/throttler/useThrottledValue.ts @@ -9,8 +9,9 @@ import type { Throttler, ThrottlerOptions } from '@tanstack/pacer/throttler' * Throttling ensures the value updates occur at a controlled rate regardless of how frequently the input value changes. * This is useful for rate-limiting expensive re-renders or API calls that depend on rapidly changing values. * - * The hook returns both the throttled value and the underlying throttler instance for additional control. - * The throttled value will update according to the leading/trailing edge behavior specified in the options. + * The hook returns a tuple containing: + * - The throttled value that updates according to the leading/trailing edge behavior specified in the options + * - The throttler instance with control methods * * For more direct control over throttling behavior without React state management, * consider using the lower-level useThrottler hook instead. @@ -18,7 +19,7 @@ import type { Throttler, ThrottlerOptions } from '@tanstack/pacer/throttler' * @example * ```tsx * // Basic throttling - update at most once per second - * const [throttledValue] = useThrottledValue(rawValue, { wait: 1000 }); + * const [throttledValue, throttler] = useThrottledValue(rawValue, { wait: 1000 }); * * // With custom leading/trailing behavior * const [throttledValue, throttler] = useThrottledValue(rawValue, { @@ -26,20 +27,12 @@ import type { Throttler, ThrottlerOptions } from '@tanstack/pacer/throttler' * leading: true, // Update immediately on first change * trailing: false // Skip trailing edge updates * }); - * - * // Optionally access throttler methods - * const handleExecutionCount = () => { - * console.log('Executions:', throttler.getExecutionCount()); - * }; * ``` */ export function useThrottledValue( value: TValue, - options: ThrottlerOptions< - React.Dispatch>, - [value: React.SetStateAction] - >, -): [TValue, Throttler>, [TValue]>] { + options: ThrottlerOptions>>, +): [TValue, Throttler>>] { const [throttledValue, setThrottledValue, throttler] = useThrottledState( value, options, @@ -47,10 +40,7 @@ export function useThrottledValue( useEffect(() => { setThrottledValue(value) - return () => { - throttler.cancel() - } - }, [value, setThrottledValue, throttler]) + }, [value, setThrottledValue]) return [throttledValue, throttler] } diff --git a/packages/react-pacer/src/throttler/useThrottler.ts b/packages/react-pacer/src/throttler/useThrottler.ts index 8fdbffff5..cfefb081b 100644 --- a/packages/react-pacer/src/throttler/useThrottler.ts +++ b/packages/react-pacer/src/throttler/useThrottler.ts @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Throttler } from '@tanstack/pacer/throttler' import { bindInstanceMethods } from '@tanstack/pacer/utils' import type { AnyFunction } from '@tanstack/pacer/types' @@ -39,15 +39,21 @@ import type { ThrottlerOptions } from '@tanstack/pacer/throttler' * ); * ``` */ -export function useThrottler< - TFn extends AnyFunction, - TArgs extends Parameters, ->(fn: TFn, options: ThrottlerOptions): Throttler { +export function useThrottler( + fn: TFn, + options: ThrottlerOptions, +): Throttler { const [throttler] = useState(() => - bindInstanceMethods(new Throttler(fn, options)), + bindInstanceMethods(new Throttler(fn, options)), ) throttler.setOptions(options) + useEffect(() => { + return () => { + throttler.cancel() + } + }, [throttler]) + return throttler } diff --git a/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts index 6fdec0678..5ff229d5a 100644 --- a/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts +++ b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts @@ -5,15 +5,20 @@ import type { AsyncDebouncerOptions } from '@tanstack/pacer/async-debouncer' import type { AnyAsyncFunction } from '@tanstack/pacer/types' import type { Accessor } from 'solid-js' -export interface SolidAsyncDebouncer< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, -> extends Omit< - AsyncDebouncer, - 'getExecutionCount' | 'getIsPending' +export interface SolidAsyncDebouncer + extends Omit< + AsyncDebouncer, + | 'getErrorCount' + | 'getIsPending' + | 'getLastResult' + | 'getSettleCount' + | 'getSuccessCount' > { - executionCount: Accessor + errorCount: Accessor isPending: Accessor + lastResult: Accessor | undefined> + settleCount: Accessor + successCount: Accessor } /** @@ -51,29 +56,37 @@ export interface SolidAsyncDebouncer< * ``` */ -export function createAsyncDebouncer< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, ->( +export function createAsyncDebouncer( fn: TFn, - initialOptions: AsyncDebouncerOptions, -): SolidAsyncDebouncer { - const asyncDebouncer = new AsyncDebouncer(fn, initialOptions) + initialOptions: AsyncDebouncerOptions, +): SolidAsyncDebouncer { + const asyncDebouncer = new AsyncDebouncer(fn, initialOptions) - const [executionCount, setExecutionCount] = createSignal( - asyncDebouncer.getExecutionCount(), + 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(), + ) - function setOptions(newOptions: Partial>) { + function setOptions(newOptions: Partial>) { asyncDebouncer.setOptions({ ...newOptions, - onExecute: (asyncDebouncer) => { - setExecutionCount(asyncDebouncer.getExecutionCount()) + onSettled: (asyncDebouncer) => { + setSuccessCount(asyncDebouncer.getSuccessCount()) + setErrorCount(asyncDebouncer.getErrorCount()) + setSettleCount(asyncDebouncer.getSettleCount()) setIsPending(asyncDebouncer.getIsPending()) - - const onExecute = newOptions.onExecute ?? initialOptions.onExecute - onExecute?.(asyncDebouncer) + setLastResult(asyncDebouncer.getLastResult()) + const onSettled = newOptions.onSettled ?? initialOptions.onSettled + onSettled?.(asyncDebouncer) }, }) } @@ -82,8 +95,11 @@ export function createAsyncDebouncer< return { ...bindInstanceMethods(asyncDebouncer), - executionCount, + errorCount, isPending, + lastResult, + settleCount, + successCount, setOptions, - } + } as SolidAsyncDebouncer } diff --git a/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts b/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts index f93d52cc6..cf12b8773 100644 --- a/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts +++ b/packages/solid-pacer/src/async-queuer/createAsyncQueuer.ts @@ -49,11 +49,15 @@ export interface SolidAsyncQueuer /** * Signal version of `getPeek` */ - peek: Accessor + peek: Accessor<(() => Promise) | undefined> /** * Signal version of `getPendingItems` */ pendingItems: Accessor Promise>> + /** + * Signal version of `getRejectionCount` + */ + rejectionCount: Accessor /** * Signal version of `getSize` */ @@ -171,5 +175,5 @@ export function createAsyncQueuer( pendingItems, rejectionCount, size, - } + } as SolidAsyncQueuer } diff --git a/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts b/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts index 81e41b74f..be2d442fe 100644 --- a/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts +++ b/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts @@ -5,17 +5,19 @@ import type { Accessor } from 'solid-js' import type { AnyAsyncFunction } from '@tanstack/pacer/types' import type { AsyncRateLimiterOptions } from '@tanstack/pacer/async-rate-limiter' -export interface SolidAsyncRateLimiter< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, -> extends Omit< - AsyncRateLimiter, - | 'getExecutionCount' +export interface SolidAsyncRateLimiter + extends Omit< + AsyncRateLimiter, + | 'getSuccessCount' + | 'getSettleCount' + | 'getErrorCount' | 'getRejectionCount' | 'getRemainingInWindow' | 'getMsUntilNextWindow' > { - executionCount: Accessor + successCount: Accessor + settleCount: Accessor + errorCount: Accessor rejectionCount: Accessor remainingInWindow: Accessor msUntilNextWindow: Accessor @@ -57,21 +59,24 @@ export interface SolidAsyncRateLimiter< * ); * ``` */ -export function createAsyncRateLimiter< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, ->( +export function createAsyncRateLimiter( fn: TFn, - initialOptions: AsyncRateLimiterOptions, -): SolidAsyncRateLimiter { - const asyncRateLimiter = new AsyncRateLimiter(fn, initialOptions) + initialOptions: AsyncRateLimiterOptions, +): SolidAsyncRateLimiter { + const asyncRateLimiter = new AsyncRateLimiter(fn, initialOptions) - const [executionCount, setExecutionCount] = createSignal( - asyncRateLimiter.getExecutionCount(), + 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(), ) @@ -79,18 +84,19 @@ export function createAsyncRateLimiter< asyncRateLimiter.getMsUntilNextWindow(), ) - function setOptions( - newOptions: Partial>, - ) { + function setOptions(newOptions: Partial>) { asyncRateLimiter.setOptions({ ...newOptions, - onExecute: (rateLimiter) => { - setExecutionCount(rateLimiter.getExecutionCount()) + onSettled: (rateLimiter) => { + setSuccessCount(rateLimiter.getSuccessCount()) + setSettleCount(rateLimiter.getSettleCount()) + setErrorCount(rateLimiter.getErrorCount()) + setRejectionCount(rateLimiter.getRejectionCount()) setRemainingInWindow(rateLimiter.getRemainingInWindow()) setMsUntilNextWindow(rateLimiter.getMsUntilNextWindow()) - const onExecute = newOptions.onExecute ?? initialOptions.onExecute - onExecute?.(rateLimiter) + const onSettled = newOptions.onSettled ?? initialOptions.onSettled + onSettled?.(rateLimiter) }, onReject: (rateLimiter) => { setRejectionCount(rateLimiter.getRejectionCount()) @@ -107,10 +113,12 @@ export function createAsyncRateLimiter< return { ...bindInstanceMethods(asyncRateLimiter), - executionCount, - rejectionCount, + errorCount, remainingInWindow, msUntilNextWindow, + rejectionCount, setOptions, - } + settleCount, + successCount, + } as SolidAsyncRateLimiter } diff --git a/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts b/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts index 92d89f11c..7121d896a 100644 --- a/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts +++ b/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts @@ -5,18 +5,24 @@ import type { Accessor } from 'solid-js' import type { AnyAsyncFunction } from '@tanstack/pacer/types' import type { AsyncThrottlerOptions } from '@tanstack/pacer/async-throttler' -export interface SolidAsyncThrottler< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, -> extends Omit< - AsyncThrottler, - | 'getExecutionCount' +export interface SolidAsyncThrottler + extends Omit< + AsyncThrottler, + | 'getSuccessCount' + | 'getSettleCount' + | 'getErrorCount' | 'getIsPending' + | 'getIsExecuting' + | 'getLastResult' | 'getLastExecutionTime' | 'getNextExecutionTime' > { - executionCount: Accessor + successCount: Accessor + settleCount: Accessor + errorCount: Accessor isPending: Accessor + isExecuting: Accessor + lastResult: Accessor | undefined> lastExecutionTime: Accessor nextExecutionTime: Accessor } @@ -58,19 +64,30 @@ export interface SolidAsyncThrottler< * ``` */ -export function createAsyncThrottler< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, ->( +export function createAsyncThrottler( fn: TFn, - initialOptions: AsyncThrottlerOptions, -): SolidAsyncThrottler { - const asyncThrottler = new AsyncThrottler(fn, initialOptions) + initialOptions: AsyncThrottlerOptions, +): SolidAsyncThrottler { + const asyncThrottler = bindInstanceMethods( + new AsyncThrottler(fn, initialOptions), + ) - const [executionCount, setExecutionCount] = createSignal( - asyncThrottler.getExecutionCount(), + 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(), ) @@ -78,17 +95,21 @@ export function createAsyncThrottler< asyncThrottler.getNextExecutionTime(), ) - function setOptions(newOptions: Partial>) { + function setOptions(newOptions: Partial>) { asyncThrottler.setOptions({ ...newOptions, - onExecute: (throttler) => { - setExecutionCount(throttler.getExecutionCount()) + 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 onExecute = newOptions.onExecute ?? initialOptions.onExecute - onExecute?.(throttler) + const onSettled = newOptions.onSettled ?? initialOptions.onSettled + onSettled?.(throttler) }, }) } @@ -96,11 +117,15 @@ export function createAsyncThrottler< setOptions(initialOptions) return { - ...bindInstanceMethods(asyncThrottler), - executionCount, + ...asyncThrottler, + errorCount, + isExecuting, isPending, lastExecutionTime, + lastResult, nextExecutionTime, setOptions, - } + settleCount, + successCount, + } as SolidAsyncThrottler } diff --git a/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts b/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts index 3948c0199..7783ac897 100644 --- a/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts +++ b/packages/solid-pacer/src/debouncer/createDebouncedSignal.ts @@ -45,12 +45,8 @@ import type { DebouncerOptions } from '@tanstack/pacer/debouncer' */ export function createDebouncedSignal( value: TValue, - initialOptions: DebouncerOptions, [Accessor]>, -): [ - Accessor, - Setter, - SolidDebouncer, [Accessor]>, -] { + initialOptions: DebouncerOptions>, +): [Accessor, Setter, SolidDebouncer>] { const [debouncedValue, setDebouncedValue] = createSignal(value) const debouncer = createDebouncer(setDebouncedValue, initialOptions) diff --git a/packages/solid-pacer/src/debouncer/createDebouncedValue.ts b/packages/solid-pacer/src/debouncer/createDebouncedValue.ts index 52f582b86..e610d6a26 100644 --- a/packages/solid-pacer/src/debouncer/createDebouncedValue.ts +++ b/packages/solid-pacer/src/debouncer/createDebouncedValue.ts @@ -1,4 +1,4 @@ -import { createEffect, onCleanup } from 'solid-js' +import { createEffect } from 'solid-js' import { createDebouncedSignal } from './createDebouncedSignal' import type { SolidDebouncer } from './createDebouncer' import type { Accessor, Setter } from 'solid-js' @@ -18,8 +18,8 @@ import type { DebouncerOptions } from '@tanstack/pacer/debouncer' * or calculations occur. * * The hook returns a tuple containing: - * - The current debounced value (as an Accessor) - * - The debouncer instance with control methods and state signals + * - An Accessor that provides the current debounced value + * - The debouncer instance with control methods * * @example * ```tsx @@ -34,20 +34,14 @@ import type { DebouncerOptions } from '@tanstack/pacer/debouncer' * fetchSearchResults(debouncedQuery()); * }); * - * // Access debouncer state via signals - * console.log('Executions:', debouncer.executionCount()); - * console.log('Is pending:', debouncer.isPending()); - * - * // Handle input changes - * const handleChange = (e) => { - * setSearchQuery(e.target.value); - * }; + * // Control the debouncer + * debouncer.cancel(); // Cancel any pending updates * ``` */ export function createDebouncedValue( value: Accessor, - initialOptions: DebouncerOptions, [Accessor]>, -): [Accessor, SolidDebouncer, [Accessor]>] { + initialOptions: DebouncerOptions>, +): [Accessor, SolidDebouncer>] { const [debouncedValue, setDebouncedValue, debouncer] = createDebouncedSignal( value(), initialOptions, @@ -55,9 +49,6 @@ export function createDebouncedValue( createEffect(() => { setDebouncedValue(value() as any) - onCleanup(() => { - debouncer.cancel() - }) }) return [debouncedValue, debouncer] diff --git a/packages/solid-pacer/src/debouncer/createDebouncer.ts b/packages/solid-pacer/src/debouncer/createDebouncer.ts index b9b9709a7..6ad07ffe2 100644 --- a/packages/solid-pacer/src/debouncer/createDebouncer.ts +++ b/packages/solid-pacer/src/debouncer/createDebouncer.ts @@ -1,5 +1,5 @@ import { Debouncer } from '@tanstack/pacer/debouncer' -import { createSignal } from 'solid-js' +import { createEffect, createSignal, onCleanup } from 'solid-js' import { bindInstanceMethods } from '@tanstack/pacer/utils' import type { Accessor } from 'solid-js' import type { AnyFunction } from '@tanstack/pacer/types' @@ -8,10 +8,8 @@ import type { DebouncerOptions } 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< - TFn extends AnyFunction, - TArgs extends Parameters, -> extends Omit, 'getExecutionCount' | 'getIsPending'> { +export interface SolidDebouncer + extends Omit, 'getExecutionCount' | 'getIsPending'> { executionCount: Accessor isPending: Accessor } @@ -52,21 +50,18 @@ export interface SolidDebouncer< * debouncer.setOptions({ wait: 1000 }); * ``` */ -export function createDebouncer< - TFn extends AnyFunction, - TArgs extends Parameters, ->( +export function createDebouncer( fn: TFn, - initialOptions: DebouncerOptions, -): SolidDebouncer { - const debouncer = new Debouncer(fn, initialOptions) + 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>) { + function setOptions(newOptions: Partial>) { debouncer.setOptions({ ...newOptions, onExecute: (debouncer) => { @@ -81,10 +76,16 @@ export function createDebouncer< setOptions(initialOptions) + createEffect(() => { + onCleanup(() => { + debouncer.cancel() + }) + }) + return { - ...bindInstanceMethods(debouncer), + ...debouncer, executionCount, isPending, setOptions, - } + } as SolidDebouncer } diff --git a/packages/solid-pacer/src/queuer/createQueuer.ts b/packages/solid-pacer/src/queuer/createQueuer.ts index 9acfe4546..e8efaffb8 100644 --- a/packages/solid-pacer/src/queuer/createQueuer.ts +++ b/packages/solid-pacer/src/queuer/createQueuer.ts @@ -44,6 +44,10 @@ export interface SolidQueuer * Signal version of `getPeek` */ peek: Accessor + /** + * Signal version of `getRejectionCount` + */ + rejectionCount: Accessor /** * Signal version of `getSize` */ @@ -98,7 +102,7 @@ export interface SolidQueuer export function createQueuer( initialOptions: QueuerOptions = {}, ): SolidQueuer { - const queuer = new Queuer(initialOptions) + const queuer = bindInstanceMethods(new Queuer(initialOptions)) const [allItems, setAllItems] = createSignal>( queuer.getAllItems(), @@ -152,7 +156,7 @@ export function createQueuer( setOptions(initialOptions) return { - ...bindInstanceMethods(queuer), + ...queuer, allItems, executionCount, isEmpty, @@ -163,5 +167,5 @@ export function createQueuer( rejectionCount, size, setOptions, - } + } as SolidQueuer } diff --git a/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts b/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts index d8d41d4fe..bb8d1e3e6 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts @@ -56,12 +56,8 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' */ export function createRateLimitedSignal( value: TValue, - initialOptions: RateLimiterOptions, [Accessor]>, -): [ - Accessor, - Setter, - SolidRateLimiter, [Accessor]>, -] { + initialOptions: RateLimiterOptions>, +): [Accessor, Setter, SolidRateLimiter>] { const [rateLimitedValue, setRateLimitedValue] = createSignal(value) const rateLimiter = createRateLimiter(setRateLimitedValue, initialOptions) diff --git a/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts b/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts index d779ceda8..2d895f870 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts @@ -18,7 +18,9 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * * Rate limiting should primarily be used when you need to enforce strict limits, like API rate limits. * - * The hook returns both the rate-limited value and the underlying rateLimiter instance for additional control. + * The hook returns a tuple containing: + * - An accessor function that provides the rate-limited value + * - The rate limiter instance with control methods * * For more direct control over rate limiting behavior without Solid state management, * consider using the lower-level createRateLimiter hook instead. @@ -26,35 +28,22 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * @example * ```tsx * // Basic rate limiting - update at most 5 times per minute - * const [rateLimitedValue] = createRateLimitedValue(rawValue, { + * const [rateLimitedValue, rateLimiter] = createRateLimitedValue(rawValue, { * limit: 5, * window: 60000 * }); * - * // With rejection callback - * const [rateLimitedValue, rateLimiter] = createRateLimitedValue(rawValue, { - * limit: 3, - * window: 5000, - * onReject: (rateLimiter) => { - * console.log(`Update rejected. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); - * } - * }); + * // Use the rate-limited value + * console.log(rateLimitedValue()); // Access the current rate-limited value * - * // Optionally access rateLimiter state via signals - * const handleSubmit = () => { - * const remaining = rateLimiter.remainingInWindow(); - * if (remaining > 0) { - * console.log(`${remaining} updates remaining in this window`); - * } else { - * console.log('Rate limit reached for this window'); - * } - * }; + * // Control the rate limiter + * rateLimiter.reset(); // Reset the rate limit window * ``` */ export function createRateLimitedValue( value: Accessor, - initialOptions: RateLimiterOptions, [Accessor]>, -): [Accessor, SolidRateLimiter, [Accessor]>] { + initialOptions: RateLimiterOptions>, +): [Accessor, SolidRateLimiter>] { const [rateLimitedValue, setRateLimitedValue, rateLimiter] = createRateLimitedSignal(value(), initialOptions) diff --git a/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts b/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts index c5a3a6d4d..be73b8828 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts @@ -5,11 +5,9 @@ import type { Accessor } from 'solid-js' import type { AnyFunction } from '@tanstack/pacer/types' import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' -export interface SolidRateLimiter< - TFn extends AnyFunction, - TArgs extends Parameters, -> extends Omit< - RateLimiter, +export interface SolidRateLimiter + extends Omit< + RateLimiter, | 'getExecutionCount' | 'getMsUntilNextWindow' | 'getRejectionCount' @@ -60,14 +58,13 @@ export interface SolidRateLimiter< * console.log('Next window in:', rateLimiter.msUntilNextWindow()); * ``` */ -export function createRateLimiter< - TFn extends AnyFunction, - TArgs extends Parameters, ->( +export function createRateLimiter( fn: TFn, - initialOptions: RateLimiterOptions, -): SolidRateLimiter { - const rateLimiter = new RateLimiter(fn, initialOptions) + initialOptions: RateLimiterOptions, +): SolidRateLimiter { + const rateLimiter = bindInstanceMethods( + new RateLimiter(fn, initialOptions), + ) const [executionCount, setExecutionCount] = createSignal( rateLimiter.getExecutionCount(), @@ -82,13 +79,14 @@ export function createRateLimiter< rateLimiter.getMsUntilNextWindow(), ) - function setOptions(newOptions: Partial>) { + 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) }, @@ -96,6 +94,7 @@ export function createRateLimiter< setRejectionCount(rateLimiter.getRejectionCount()) setRemainingInWindow(rateLimiter.getRemainingInWindow()) setMsUntilNextWindow(rateLimiter.getMsUntilNextWindow()) + const onReject = newOptions.onReject ?? initialOptions.onReject onReject?.(rateLimiter) }, @@ -105,11 +104,11 @@ export function createRateLimiter< setOptions(initialOptions) return { - ...bindInstanceMethods(rateLimiter), + ...rateLimiter, executionCount, rejectionCount, remainingInWindow, msUntilNextWindow, setOptions, - } + } as SolidRateLimiter } diff --git a/packages/solid-pacer/src/throttler/createThrottledSignal.ts b/packages/solid-pacer/src/throttler/createThrottledSignal.ts index f7718eeca..f77916ee8 100644 --- a/packages/solid-pacer/src/throttler/createThrottledSignal.ts +++ b/packages/solid-pacer/src/throttler/createThrottledSignal.ts @@ -40,12 +40,8 @@ import type { ThrottlerOptions } from '@tanstack/pacer/throttler' */ export function createThrottledSignal( value: TValue, - initialOptions: ThrottlerOptions, [Accessor]>, -): [ - Accessor, - Setter, - SolidThrottler, [Accessor]>, -] { + initialOptions: ThrottlerOptions>, +): [Accessor, Setter, SolidThrottler>] { const [throttledValue, setThrottledValue] = createSignal(value) const throttler = createThrottler(setThrottledValue, initialOptions) 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 210be8ffd..eb17e01cd 100644 --- a/packages/solid-pacer/src/throttler/createThrottledValue.ts +++ b/packages/solid-pacer/src/throttler/createThrottledValue.ts @@ -1,4 +1,4 @@ -import { createEffect, onCleanup } from 'solid-js' +import { createEffect } from 'solid-js' import { createThrottledSignal } from './createThrottledSignal' import type { SolidThrottler } from './createThrottler' import type { Accessor, Setter } from 'solid-js' @@ -11,7 +11,10 @@ import type { ThrottlerOptions } from '@tanstack/pacer/throttler' * Throttling ensures the value updates occur at a controlled rate regardless of how frequently the input value changes. * This is useful for rate-limiting expensive re-renders or API calls that depend on rapidly changing values. * - * The hook returns both the throttled value and the underlying throttler instance for additional control. + * The hook returns a tuple containing: + * - An accessor function that provides the throttled value + * - The throttler instance with control methods + * * The throttled value will update according to the leading/trailing edge behavior specified in the options. * * For more direct control over throttling behavior without Solid state management, @@ -20,26 +23,19 @@ import type { ThrottlerOptions } from '@tanstack/pacer/throttler' * @example * ```tsx * // Basic throttling - update at most once per second - * const [throttledValue] = createThrottledValue(rawValue, { wait: 1000 }); + * const [throttledValue, throttler] = createThrottledValue(rawValue, { wait: 1000 }); * - * // With custom leading/trailing behavior - * const [throttledValue, throttler] = createThrottledValue(rawValue, { - * wait: 1000, - * leading: true, // Update immediately on first change - * trailing: false // Skip trailing edge updates - * }); + * // Use the throttled value + * console.log(throttledValue()); // Access the current throttled value * - * // Access throttler state via signals - * console.log('Executions:', throttler.executionCount()); - * console.log('Is pending:', throttler.isPending()); - * console.log('Last execution:', throttler.lastExecutionTime()); - * console.log('Next execution:', throttler.nextExecutionTime()); + * // Control the throttler + * throttler.cancel(); // Cancel any pending updates * ``` */ export function createThrottledValue( value: Accessor, - initialOptions: ThrottlerOptions, [Accessor]>, -): [Accessor, SolidThrottler, [Accessor]>] { + initialOptions: ThrottlerOptions>, +): [Accessor, SolidThrottler>] { const [throttledValue, setThrottledValue, throttler] = createThrottledSignal( value(), initialOptions, @@ -47,9 +43,6 @@ export function createThrottledValue( createEffect(() => { setThrottledValue(value() as any) - onCleanup(() => { - throttler.cancel() - }) }) return [throttledValue, throttler] diff --git a/packages/solid-pacer/src/throttler/createThrottler.ts b/packages/solid-pacer/src/throttler/createThrottler.ts index d93150260..ad4f73e4e 100644 --- a/packages/solid-pacer/src/throttler/createThrottler.ts +++ b/packages/solid-pacer/src/throttler/createThrottler.ts @@ -1,5 +1,5 @@ import { Throttler } from '@tanstack/pacer/throttler' -import { createSignal } from 'solid-js' +import { createEffect, createSignal, onCleanup } from 'solid-js' import { bindInstanceMethods } from '@tanstack/pacer/utils' import type { Accessor } from 'solid-js' import type { AnyFunction } from '@tanstack/pacer/types' @@ -8,11 +8,9 @@ import type { ThrottlerOptions } 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< - TFn extends AnyFunction, - TArgs extends Parameters, -> extends Omit< - Throttler, +export interface SolidThrottler + extends Omit< + Throttler, | 'getExecutionCount' | 'getIsPending' | 'getLastExecutionTime' @@ -58,14 +56,11 @@ export interface SolidThrottler< * console.log(throttler.nextExecutionTime()); // timestamp of next allowed execution * ``` */ -export function createThrottler< - TFn extends AnyFunction, - TArgs extends Parameters, ->( +export function createThrottler( fn: TFn, - initialOptions: ThrottlerOptions, -): SolidThrottler { - const throttler = new Throttler(fn, initialOptions) + initialOptions: ThrottlerOptions, +): SolidThrottler { + const throttler = bindInstanceMethods(new Throttler(fn, initialOptions)) const [executionCount, setExecutionCount] = createSignal( throttler.getExecutionCount(), @@ -78,7 +73,7 @@ export function createThrottler< throttler.getNextExecutionTime(), ) - function setOptions(newOptions: Partial>) { + function setOptions(newOptions: Partial>) { throttler.setOptions({ ...newOptions, onExecute: (throttler) => { @@ -95,12 +90,18 @@ export function createThrottler< setOptions(initialOptions) + createEffect(() => { + onCleanup(() => { + throttler.cancel() + }) + }) + return { - ...bindInstanceMethods(throttler), + ...throttler, executionCount, isPending, lastExecutionTime, nextExecutionTime, setOptions, - } + } as SolidThrottler } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf899b702..d4fe36924 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,13 +22,13 @@ importers: version: 1.2.0 '@tanstack/config': specifier: 0.18.0 - version: 0.18.0(@types/node@22.14.1)(eslint@9.25.1(jiti@2.4.2))(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 0.18.0(@types/node@22.15.2)(eslint@9.25.1(jiti@2.4.2))(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(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.14.1 - version: 22.14.1 + specifier: ^22.15.2 + version: 22.15.2 eslint: specifier: ^9.25.1 version: 9.25.1(jiti@2.4.2) @@ -40,10 +40,10 @@ importers: version: 26.1.0 knip: specifier: ^5.50.5 - version: 5.50.5(@types/node@22.14.1)(typescript@5.8.3) + version: 5.50.5(@types/node@22.15.2)(typescript@5.8.3) nx: - specifier: ^20.8.0 - version: 20.8.0 + specifier: ^20.8.1 + version: 20.8.1 premove: specifier: ^4.0.0 version: 4.0.0 @@ -66,11 +66,11 @@ importers: specifier: 5.8.3 version: 5.8.3 vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) vitest: specifier: ^3.1.2 - version: 3.1.2(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(tsx@4.19.3)(yaml@2.7.0) + version: 3.1.2(@types/node@22.15.2)(jiti@2.4.2)(jsdom@26.1.0)(tsx@4.19.3)(yaml@2.7.0) examples/react/asyncDebounce: dependencies: @@ -92,10 +92,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/asyncRateLimit: dependencies: @@ -117,10 +117,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/asyncThrottle: dependencies: @@ -142,10 +142,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/debounce: dependencies: @@ -167,10 +167,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/queue: dependencies: @@ -192,10 +192,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/rateLimit: dependencies: @@ -217,10 +217,103 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + + examples/react/react-query-debounced-prefetch: + dependencies: + '@tanstack/react-pacer': + specifier: ^0.2.0 + version: link:../../../packages/react-pacer + '@tanstack/react-query': + specifier: ^5.74.4 + version: 5.74.4(react@19.1.0) + '@tanstack/react-query-devtools': + specifier: ^5.74.4 + version: 5.74.6(@tanstack/react-query@5.74.4(react@19.1.0))(react@19.1.0) + 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.2 + version: 19.1.2 + '@types/react-dom': + specifier: ^19.1.2 + version: 19.1.2(@types/react@19.1.2) + '@vitejs/plugin-react': + specifier: ^4.4.1 + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite: + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + + examples/react/react-query-queued-prefetch: + dependencies: + '@tanstack/react-pacer': + specifier: ^0.2.0 + version: link:../../../packages/react-pacer + '@tanstack/react-query': + specifier: ^5.74.4 + version: 5.74.4(react@19.1.0) + '@tanstack/react-query-devtools': + specifier: ^5.74.4 + version: 5.74.6(@tanstack/react-query@5.74.4(react@19.1.0))(react@19.1.0) + 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.2 + version: 19.1.2 + '@types/react-dom': + specifier: ^19.1.2 + version: 19.1.2(@types/react@19.1.2) + '@vitejs/plugin-react': + specifier: ^4.4.1 + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite: + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + + examples/react/react-query-throttled-prefetch: + dependencies: + '@tanstack/react-pacer': + specifier: ^0.2.0 + version: link:../../../packages/react-pacer + '@tanstack/react-query': + specifier: ^5.74.4 + version: 5.74.4(react@19.1.0) + '@tanstack/react-query-devtools': + specifier: ^5.74.4 + version: 5.74.6(@tanstack/react-query@5.74.4(react@19.1.0))(react@19.1.0) + 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.2 + version: 19.1.2 + '@types/react-dom': + specifier: ^19.1.2 + version: 19.1.2(@types/react@19.1.2) + '@vitejs/plugin-react': + specifier: ^4.4.1 + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite: + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/throttle: dependencies: @@ -242,10 +335,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useAsyncDebouncer: dependencies: @@ -267,12 +360,12 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - examples/react/useAsyncQueuer: + examples/react/useAsyncQueuedState: dependencies: '@tanstack/react-pacer': specifier: ^0.2.0 @@ -292,12 +385,12 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - examples/react/useAsyncQueuerState: + examples/react/useAsyncQueuer: dependencies: '@tanstack/react-pacer': specifier: ^0.2.0 @@ -317,10 +410,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useAsyncRateLimiter: dependencies: @@ -342,10 +435,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useAsyncThrottler: dependencies: @@ -367,10 +460,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useDebouncedCallback: dependencies: @@ -392,10 +485,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useDebouncedState: dependencies: @@ -417,10 +510,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useDebouncedValue: dependencies: @@ -442,10 +535,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useDebouncer: dependencies: @@ -467,12 +560,12 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - examples/react/useQueuer: + examples/react/useQueuedState: dependencies: '@tanstack/react-pacer': specifier: ^0.2.0 @@ -492,12 +585,12 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - examples/react/useQueuerState: + examples/react/useQueuedValue: dependencies: '@tanstack/react-pacer': specifier: ^0.2.0 @@ -517,10 +610,35 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + + examples/react/useQueuer: + dependencies: + '@tanstack/react-pacer': + specifier: ^0.2.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.2 + version: 19.1.2 + '@types/react-dom': + specifier: ^19.1.2 + version: 19.1.2(@types/react@19.1.2) + '@vitejs/plugin-react': + specifier: ^4.4.1 + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite: + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useRateLimitedCallback: dependencies: @@ -542,10 +660,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useRateLimitedState: dependencies: @@ -567,10 +685,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useRateLimitedValue: dependencies: @@ -592,10 +710,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useRateLimiter: dependencies: @@ -617,10 +735,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useThrottledCallback: dependencies: @@ -642,10 +760,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useThrottledState: dependencies: @@ -667,10 +785,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useThrottledValue: dependencies: @@ -692,10 +810,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/react/useThrottler: dependencies: @@ -717,10 +835,10 @@ importers: version: 19.1.2(@types/react@19.1.2) '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) examples/solid/asyncDebounce: dependencies: @@ -732,11 +850,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/asyncRateLimit: dependencies: @@ -748,11 +866,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/asyncThrottle: dependencies: @@ -764,11 +882,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createAsyncDebouncer: dependencies: @@ -780,11 +898,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createAsyncQueuer: dependencies: @@ -796,11 +914,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createAsyncRateLimiter: dependencies: @@ -812,11 +930,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createAsyncThrottler: dependencies: @@ -828,11 +946,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createDebouncedSignal: dependencies: @@ -844,11 +962,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createDebouncedValue: dependencies: @@ -860,11 +978,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createDebouncer: dependencies: @@ -876,11 +994,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createQueuer: dependencies: @@ -892,11 +1010,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createRateLimitedSignal: dependencies: @@ -908,11 +1026,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createRateLimitedValue: dependencies: @@ -924,11 +1042,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createRateLimiter: dependencies: @@ -940,11 +1058,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createThrottledSignal: dependencies: @@ -956,11 +1074,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createThrottledValue: dependencies: @@ -972,11 +1090,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/createThrottler: dependencies: @@ -988,11 +1106,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/debounce: dependencies: @@ -1004,11 +1122,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/queue: dependencies: @@ -1020,11 +1138,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/rateLimit: dependencies: @@ -1036,11 +1154,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) examples/solid/throttle: dependencies: @@ -1052,11 +1170,11 @@ importers: version: 1.9.5 devDependencies: vite: - specifier: ^6.3.2 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) packages/pacer: {} @@ -1070,14 +1188,14 @@ importers: version: 19.0.0(react@19.1.0) devDependencies: '@eslint-react/eslint-plugin': - specifier: ^1.48.4 - version: 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + specifier: ^1.48.5 + version: 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@types/react': specifier: ^19.1.2 version: 19.1.2 '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) eslint-plugin-react-compiler: specifier: 19.1.0-rc.1 version: 19.1.0-rc.1(eslint@9.25.1(jiti@2.4.2)) @@ -1099,7 +1217,7 @@ importers: version: 1.9.5 vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) packages: @@ -1533,20 +1651,20 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-react/ast@1.48.4': - resolution: {integrity: sha512-AzEfLguhjTQbJrJIaws4HPyrElNnfi2UXu7F58Crs337k2CKY+ZezFyj5T1nfBxTLIw2tx7r7WxGS3jG4zFkQg==} + '@eslint-react/ast@1.48.5': + resolution: {integrity: sha512-4PfH7LYI0InkoSKim7CmNe0Ly6Ykina0tCVmUPr3cQXcF0MOvLxI6OmCxRudFnxdhLgemQDcCbFMnB7LMjAl3A==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint-react/core@1.48.4': - resolution: {integrity: sha512-/U+EtGVpXaDs/h6BBycrmBTTUQzeFKXBQ9lv3Jx/NG1tb1VIWOwNf514HeHNdDcoWBQ1ViswWpB6aMkZQ7m2DA==} + '@eslint-react/core@1.48.5': + resolution: {integrity: sha512-BtKZJHPnqBkRtYvcNsxSXKbSawFRdkOcLS2Hm4HfjSkEr39j7CVipeikK1CJA4uJN6RNWMlwTVL2QyQ2oM4KRg==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint-react/eff@1.48.4': - resolution: {integrity: sha512-A+4L/QQWNliTNjebVuSqfyBpF/7jl+LCwYVmcAdS7j1y/6GbtjOzzpB5dZEEiOAI50Ni1NprDLPTG74zFACaZw==} + '@eslint-react/eff@1.48.5': + resolution: {integrity: sha512-7YSZybpPq9GOtLottQVfCfKudoPSyYaGfiKKTAHKPTfYkN3EbwE6EVebHn00zw0UnETlnxVZqqf8EEUjIqugug==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint-react/eslint-plugin@1.48.4': - resolution: {integrity: sha512-h2Biwa5C8NRKjPHJJq+QVT3UXTG0nFQzjNi7tpYn6+B+t1nL1fvoyTr1GHp+PBCoA+jPXJX34XqJ14s9vjzNyQ==} + '@eslint-react/eslint-plugin@1.48.5': + resolution: {integrity: sha512-K3s08AgSWex6HT/4o1fv1BG4ofPRXgh0G8GKhSiWO0qLPtWSXK2MITVPGodiKSV1Gr7xEREi758/lDs8gp3upg==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1555,16 +1673,16 @@ packages: typescript: optional: true - '@eslint-react/kit@1.48.4': - resolution: {integrity: sha512-5+85zZ2nFB4lQOir52X6cnNDJk8upC5FXGd4+qArYbMnug33RjBNo2fGacLBtnoz5GQEd8enop6KnPD0WRuAiQ==} + '@eslint-react/kit@1.48.5': + resolution: {integrity: sha512-fk6oDQWTftVDZp4QE/NhLM2mS3OuuXQSPD6RuBo5GWcI93ApGkOHCXHbsaQZFsT/YMrE7REsXWa2qq+a/UlWeQ==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint-react/shared@1.48.4': - resolution: {integrity: sha512-QEyIMcHP2x2/Ai6zxlaFrBE0aSJQMLZq2GHEjM9FsP095WE+IaQJbmvLPDyUlTDordcvgB+VN8kxrm7Rr/T3IQ==} + '@eslint-react/shared@1.48.5': + resolution: {integrity: sha512-+DY5NjKHVvrrv+SbDZ6ZqUt3xohm7NsWeyWKmxc3SFZOs4bdlZyyMZdiO25NgetbN2iox6vjPfp9uupqGFxO/w==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} - '@eslint-react/var@1.48.4': - resolution: {integrity: sha512-MgfI8NaXU1vQslft5eobCcOmssYdh/C4nnJguQtdCY/kBUwY7WpkwvPXyqgRimE8emLnSP6rua8H8KpfgJ0eJg==} + '@eslint-react/var@1.48.5': + resolution: {integrity: sha512-KFc8BueXL7pJgmP3FsL7hSZcMz2k7vNj7QzN33L2lUyNGkbydV07K9PMK9ndF5jOARguK2XLg6KLC4CUEUYhpg==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} '@eslint/config-array@0.20.0': @@ -1688,62 +1806,62 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nx/nx-darwin-arm64@20.8.0': - resolution: {integrity: sha512-A6Te2KlINtcOo/depXJzPyjbk9E0cmgbom/sm/49XdQ8G94aDfyIIY1RIdwmDCK5NVd74KFG3JIByTk5+VnAhA==} + '@nx/nx-darwin-arm64@20.8.1': + resolution: {integrity: sha512-Gat4Io66cV70Oa1CjrMJPsEx5ICpAGayv9hejOtBUEDb6XjR12L2e4wV+4EHliF0UbEcuZAr8/lTROEPk0RGWQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@nx/nx-darwin-x64@20.8.0': - resolution: {integrity: sha512-UpqayUjgalArXaDvOoshqSelTrEp42cGDsZGy0sqpxwBpm3oPQ8wE1d7oBAmRo208rAxOuFP0LZRFUqRrwGvLA==} + '@nx/nx-darwin-x64@20.8.1': + resolution: {integrity: sha512-TB9mZk7neGFKgBr2wSBgY6c4kFF9vvChNSp3TrEeXR3FppFcYG5eK4AaKfzWCpYb0wMtseAm7NMX1Lu74utClQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@nx/nx-freebsd-x64@20.8.0': - resolution: {integrity: sha512-dUR2fsLyKZYMHByvjy2zvmdMbsdXAiP+6uTlIAuu8eHMZ2FPQCAtt7lPYLwOFUxUXChbek2AJ+uCI0gRAgK/eg==} + '@nx/nx-freebsd-x64@20.8.1': + resolution: {integrity: sha512-7UQu0/Afna5Af2GagEQ6rbKfUh75NfUn+g66wsoQoUGBvDW0U7B8P3Ph5Bk4Urub0BSfMVcNg2X7CgfypLFN/g==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@nx/nx-linux-arm-gnueabihf@20.8.0': - resolution: {integrity: sha512-GuZ7t0SzSX5ksLYva7koKZovQ5h/Kr1pFbOsQcBf3VLREBqFPSz6t7CVYpsIsMhiu/I3EKq6FZI3wDOJbee5uw==} + '@nx/nx-linux-arm-gnueabihf@20.8.1': + resolution: {integrity: sha512-Tjh8JkTP+x1jSrzx+ofx1pKpkhIbXd7bi0bPdpYt6NI1lZz2HB/dv8vtdzP80jXEDztHf0AeGnEJVgJKsgI6yg==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@nx/nx-linux-arm64-gnu@20.8.0': - resolution: {integrity: sha512-CiI955Q+XZmBBZ7cQqQg0MhGEFwZIgSpJnjPfWBt3iOYP8aE6nZpNOkmD7O8XcN/nEwwyeCOF8euXqEStwsk8w==} + '@nx/nx-linux-arm64-gnu@20.8.1': + resolution: {integrity: sha512-2+qPIwav2vrytH6pe7fukBe8+yN5JGbEDCnDO8wKQsHeeZMLAQJiZ7EJH/+vynRkI7oWf87mihIKNQME19+w6A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@nx/nx-linux-arm64-musl@20.8.0': - resolution: {integrity: sha512-Iy9DpvVisxsfNh4gOinmMQ4cLWdBlgvt1wmry1UwvcXg479p1oJQ1Kp1wksUZoWYqrAG8VPZUmkE0f7gjyHTGg==} + '@nx/nx-linux-arm64-musl@20.8.1': + resolution: {integrity: sha512-DsKc+DiMsuHqpBWchUUUg6zv4OaexRqpFXys6auZlrpFpn80kSqLQ3S4zZ5AUu+26wxZqEVJs+uxHGwFbhEssQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@nx/nx-linux-x64-gnu@20.8.0': - resolution: {integrity: sha512-kZrrXXzVSbqwmdTmQ9xL4Jhi0/FSLrePSxYCL9oOM3Rsj0lmo/aC9kz4NBv1ZzuqT7fumpBOnhqiL1QyhOWOeQ==} + '@nx/nx-linux-x64-gnu@20.8.1': + resolution: {integrity: sha512-Kzru44beVKAmSG84ShuMIIfyu2Uu5r8gsHdtiQPBIOGkZa0Z/e6YtUxcN3w1UZ7yvvzoQ4pQLvqU6UZRSWZtEg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@nx/nx-linux-x64-musl@20.8.0': - resolution: {integrity: sha512-0l9jEMN8NhULKYCFiDF7QVpMMNG40duya+OF8dH0OzFj52N0zTsvsgLY72TIhslCB/cC74oAzsmWEIiFslscnA==} + '@nx/nx-linux-x64-musl@20.8.1': + resolution: {integrity: sha512-cSVVb7DVMhrxCaj/n55okBZS6lZoP5a5vynOBGIV4z3/OJLev+xI9A+3imn/aXnBl8iS69HogYyrW0YTXv4Xaw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@nx/nx-win32-arm64-msvc@20.8.0': - resolution: {integrity: sha512-5miZJmRSwx1jybBsiB3NGocXL9TxGdT2D+dOqR2fsLklpGz0ItEWm8+i8lhDjgOdAr2nFcuQUfQMY57f9FOHrA==} + '@nx/nx-win32-arm64-msvc@20.8.1': + resolution: {integrity: sha512-gte5HcvI24CN6b9I6IYTXh/A0CtRfnlAFaJomPpfT8Wcq637aOZzS0arAEZVoU8QZty1350hj6sfu+wSIjoP7A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@nx/nx-win32-x64-msvc@20.8.0': - resolution: {integrity: sha512-0P5r+bDuSNvoWys+6C1/KqGpYlqwSHpigCcyRzR62iZpT3OooZv+nWO06RlURkxMR8LNvYXTSSLvoLkjxqM8uQ==} + '@nx/nx-win32-x64-msvc@20.8.1': + resolution: {integrity: sha512-6c2fVEPdPwJdnRbckBatRDF/g6JAp6p3Mfl90DpuaEF2DZC5pmCXKOsXE0aSIZ+gODom2JIchM++2KmDZPJUoA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1934,6 +2052,23 @@ packages: resolution: {integrity: sha512-nI4F7/SpT6BMoigq1VmrrNe3A6Hsua9XcZNql+qzK2zJUOcKBRqMvC22n3eKcjsbZuWIFvkIC0ThsuBVYCKXfA==} engines: {node: '>=18'} + '@tanstack/query-core@5.74.4': + resolution: {integrity: sha512-YuG0A0+3i9b2Gfo9fkmNnkUWh5+5cFhWBN0pJAHkHilTx6A0nv8kepkk4T4GRt4e5ahbtFj2eTtkiPcVU1xO4A==} + + '@tanstack/query-devtools@5.74.6': + resolution: {integrity: sha512-djaFT11mVCOW3e0Ezfyiq7T6OoHy2LRI1fUFQvj+G6+/4A1FkuRMNUhQkdP1GXlx8id0f1/zd5fgDpIy5SU/Iw==} + + '@tanstack/react-query-devtools@5.74.6': + resolution: {integrity: sha512-vlsDwz4/FsblK0h7VAlXUdJ+9OV+i1n8OLb8CLLAZqu0M9GCnbajytZwsRmns33PXBZ6wQBJ859kg6aajx+e9Q==} + peerDependencies: + '@tanstack/react-query': ^5.74.4 + react: ^18 || ^19 + + '@tanstack/react-query@5.74.4': + resolution: {integrity: sha512-mAbxw60d4ffQ4qmRYfkO1xzRBPUEf/72Dgo3qqea0J66nIKuDTLEqQt0ku++SDFlMGMnB6uKDnEG1xD/TDse4Q==} + peerDependencies: + react: ^18 || ^19 + '@tanstack/typedoc-config@0.2.0': resolution: {integrity: sha512-1ak0ZirlLRxd3dNNOFnMoYORBeC83nK4C+OiXpE0dxsO8ZVrBqCtNCKr8SG+W9zICXcWGiFu9qYLsgNKTayOqw==} engines: {node: '>=18'} @@ -1982,8 +2117,8 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@22.14.1': - resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} + '@types/node@22.15.2': + resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==} '@types/react-dom@19.1.2': resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} @@ -2175,11 +2310,11 @@ packages: resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} hasBin: true - '@zod/core@0.8.1': - resolution: {integrity: sha512-djj8hPhxIHcG8ptxITaw/Bout5HJZ9NyRbKr95Eilqwt9R0kvITwUQGDU+n+MVdsBIka5KwztmZSLti22F+P0A==} + '@zod/core@0.9.0': + resolution: {integrity: sha512-bVfPiV2kDUkAJ4ArvV4MHcPZA8y3xOX6/SjzSy2kX2ACopbaaAP4wk6hd/byRmfi9MLNai+4SFJMmcATdOyclg==} - '@zod/mini@4.0.0-beta.20250420T053007': - resolution: {integrity: sha512-PnQwkzlUbUj6A2GKbqhskH/v58m2WfmP+dbXevzzMdvVA7H/KNao37YWhHOTjBzf+xykCXNoLwfwnPMIsU0hOA==} + '@zod/mini@4.0.0-beta.20250424T163858': + resolution: {integrity: sha512-dUrw6GDXsFbbjvHBlqOrkWiFFNbo9EW+/D3NcoO+28rJdLM330tSMK16m3NrPhDbLbZU6VLEd0yL650w5Kqmkg==} JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} @@ -2617,8 +2752,8 @@ packages: peerDependencies: eslint: '>=7' - eslint-plugin-react-debug@1.48.4: - resolution: {integrity: sha512-qDHXi11k6yQGCezkeHJOWOO/DzQ48Im5LxwsmbYyCN1dCKkfZ3sYtgMa+y5p7ffl6gandWDioFYNRSlXFb0Phw==} + eslint-plugin-react-debug@1.48.5: + resolution: {integrity: sha512-0ohplE9yStl5U45GLzmTioK8eJ5kg5b3ICCDFZWDkvGKnXtX8IGh39u4/NPjXlOsRr/deDuGHTZCETtuhNFELA==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2627,8 +2762,8 @@ packages: typescript: optional: true - eslint-plugin-react-dom@1.48.4: - resolution: {integrity: sha512-jVH3KwN5w0tth6sfpGcXvKO6kJfmWr1SbMcu6lETy+3FYOiYMmzMUhP/ECMdWyHPOJm6vA/STibi2NOSfgkmQw==} + eslint-plugin-react-dom@1.48.5: + resolution: {integrity: sha512-ZjfqZYDSqOM9M/5UD2jpyCn4jROkKawM2pNFXWGzJMoopq4x5eqx6rba6jsIQFayb5inAy3IzRnzXC/may2EPg==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2637,8 +2772,8 @@ packages: typescript: optional: true - eslint-plugin-react-hooks-extra@1.48.4: - resolution: {integrity: sha512-3qgLFZrOGVmxB4onUvY8gy+kdYFESIAz5Yd/kcbBrcw1iT5WTPT6Yqkuvig34sUV+We+wBrH4Zy4Lj8hlMcTeA==} + eslint-plugin-react-hooks-extra@1.48.5: + resolution: {integrity: sha512-/AVmDabUP1xLtOdLN/rlnb76mhdGr31V/fywiHvbe6A4wD/FkHMVpC/e+3qNJ7KhgZhZ1nZ1snSL3b/LulQCiw==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2653,8 +2788,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.48.4: - resolution: {integrity: sha512-4KU6h7Jn88fOCxPMPST5Vk235JsK8P18LWXT5AEF/OODK2fktakNI8+SkfJCbhehvos2ZQboCFHDrH4MQl9OEQ==} + eslint-plugin-react-naming-convention@1.48.5: + resolution: {integrity: sha512-UlGf+z2Klxyfdo2Z/aA367NKzqnDId/OWuFC5KoBRI+KYbxX3/OEE/O9S+MoN8r7KT/eg8NMBDqMj+UAzMoNWQ==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2663,8 +2798,8 @@ packages: typescript: optional: true - eslint-plugin-react-web-api@1.48.4: - resolution: {integrity: sha512-18u+8x2ErJxfK+3gPfx8t015mOXfvmDRZPz8iy2O9FVKnmojSGQLa9raUpd3roWFUJtmGLQtxoa1HwaVjO8kaA==} + eslint-plugin-react-web-api@1.48.5: + resolution: {integrity: sha512-/78LZhfdhRl3v+4ytghzNZOozF8CHXjDnrEfOQ31bwrEyVifGCGsSYnjVOUZ8rfIGB0QNq7rm3Up3CxoOezDYQ==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2673,8 +2808,8 @@ packages: typescript: optional: true - eslint-plugin-react-x@1.48.4: - resolution: {integrity: sha512-HsR+zBxLQ88AOy2TcmF2Mqvu0ByTQUQF5kZWtz5gdFuB0SdD3KZtpnlGNGTONQ4AHDJJPgG7kCTB9EaEe3lwxA==} + eslint-plugin-react-x@1.48.5: + resolution: {integrity: sha512-XuuKEUbbMjvLPfqO/lPwaq+EGOe1GFhlwGigdPfkuFliY3Vz8tA+/wRgwKQ1Gs/JYqKv3Fn+t2l6OSni9wrNGA==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3339,8 +3474,8 @@ packages: nwsapi@2.2.18: resolution: {integrity: sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA==} - nx@20.8.0: - resolution: {integrity: sha512-+BN5B5DFBB5WswD8flDDTnr4/bf1VTySXOv60aUAllHqR+KS6deT0p70TTMZF4/A2n/L2UCWDaDro37MGaYozA==} + nx@20.8.1: + resolution: {integrity: sha512-73Uw8YXpsjeLqHSl7NMCmGdCs+8ynPzoNJFWAqVanPETEY9zPd5wevVQmeyzYtNNQU35uj6Os4iUzYunmwnFaA==} hasBin: true peerDependencies: '@swc-node/register': ^1.8.0 @@ -4036,8 +4171,8 @@ packages: vite: optional: true - vite@6.3.2: - resolution: {integrity: sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==} + vite@6.3.3: + resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -4765,9 +4900,9 @@ snapshots: '@eslint-community/regexpp@4.12.1': {} - '@eslint-react/ast@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + '@eslint-react/ast@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-react/eff': 1.48.4 + '@eslint-react/eff': 1.48.5 '@typescript-eslint/types': 8.31.0 '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) @@ -4778,13 +4913,13 @@ snapshots: - supports-color - typescript - '@eslint-react/core@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + '@eslint-react/core@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-react/ast': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.48.4 - '@eslint-react/kit': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/ast': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.48.5 + '@eslint-react/kit': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.31.0 '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/types': 8.31.0 @@ -4796,57 +4931,57 @@ snapshots: - supports-color - typescript - '@eslint-react/eff@1.48.4': {} + '@eslint-react/eff@1.48.5': {} - '@eslint-react/eslint-plugin@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + '@eslint-react/eslint-plugin@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-react/eff': 1.48.4 - '@eslint-react/kit': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.48.5 + '@eslint-react/kit': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.31.0 '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/types': 8.31.0 '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) eslint: 9.25.1(jiti@2.4.2) - eslint-plugin-react-debug: 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-react-dom: 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-react-hooks-extra: 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-react-naming-convention: 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-react-web-api: 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - eslint-plugin-react-x: 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-debug: 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-dom: 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-hooks-extra: 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-naming-convention: 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-web-api: 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-x: 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - supports-color - ts-api-utils - '@eslint-react/kit@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + '@eslint-react/kit@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-react/eff': 1.48.4 + '@eslint-react/eff': 1.48.5 '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@zod/mini': 4.0.0-beta.20250420T053007 + '@zod/mini': 4.0.0-beta.20250424T163858 ts-pattern: 5.7.0 transitivePeerDependencies: - eslint - supports-color - typescript - '@eslint-react/shared@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + '@eslint-react/shared@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-react/eff': 1.48.4 - '@eslint-react/kit': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.48.5 + '@eslint-react/kit': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@zod/mini': 4.0.0-beta.20250420T053007 + '@zod/mini': 4.0.0-beta.20250424T163858 ts-pattern: 5.7.0 transitivePeerDependencies: - eslint - supports-color - typescript - '@eslint-react/var@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + '@eslint-react/var@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-react/ast': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.48.4 + '@eslint-react/ast': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.48.5 '@typescript-eslint/scope-manager': 8.31.0 '@typescript-eslint/types': 8.31.0 '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) @@ -4962,23 +5097,23 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@microsoft/api-extractor-model@7.29.6(@types/node@22.14.1)': + '@microsoft/api-extractor-model@7.29.6(@types/node@22.15.2)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.7.0(@types/node@22.14.1) + '@rushstack/node-core-library': 5.7.0(@types/node@22.15.2) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.47.7(@types/node@22.14.1)': + '@microsoft/api-extractor@7.47.7(@types/node@22.15.2)': dependencies: - '@microsoft/api-extractor-model': 7.29.6(@types/node@22.14.1) + '@microsoft/api-extractor-model': 7.29.6(@types/node@22.15.2) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.7.0(@types/node@22.14.1) + '@rushstack/node-core-library': 5.7.0(@types/node@22.15.2) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.14.0(@types/node@22.14.1) - '@rushstack/ts-command-line': 4.22.6(@types/node@22.14.1) + '@rushstack/terminal': 0.14.0(@types/node@22.15.2) + '@rushstack/ts-command-line': 4.22.6(@types/node@22.15.2) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.10 @@ -5015,34 +5150,34 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nx/nx-darwin-arm64@20.8.0': + '@nx/nx-darwin-arm64@20.8.1': optional: true - '@nx/nx-darwin-x64@20.8.0': + '@nx/nx-darwin-x64@20.8.1': optional: true - '@nx/nx-freebsd-x64@20.8.0': + '@nx/nx-freebsd-x64@20.8.1': optional: true - '@nx/nx-linux-arm-gnueabihf@20.8.0': + '@nx/nx-linux-arm-gnueabihf@20.8.1': optional: true - '@nx/nx-linux-arm64-gnu@20.8.0': + '@nx/nx-linux-arm64-gnu@20.8.1': optional: true - '@nx/nx-linux-arm64-musl@20.8.0': + '@nx/nx-linux-arm64-musl@20.8.1': optional: true - '@nx/nx-linux-x64-gnu@20.8.0': + '@nx/nx-linux-x64-gnu@20.8.1': optional: true - '@nx/nx-linux-x64-musl@20.8.0': + '@nx/nx-linux-x64-musl@20.8.1': optional: true - '@nx/nx-win32-arm64-msvc@20.8.0': + '@nx/nx-win32-arm64-msvc@20.8.1': optional: true - '@nx/nx-win32-x64-msvc@20.8.0': + '@nx/nx-win32-x64-msvc@20.8.1': optional: true '@publint/pack@0.1.2': {} @@ -5112,7 +5247,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.35.0': optional: true - '@rushstack/node-core-library@5.7.0(@types/node@22.14.1)': + '@rushstack/node-core-library@5.7.0(@types/node@22.15.2)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -5123,23 +5258,23 @@ snapshots: resolve: 1.22.10 semver: 7.5.4 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.2 '@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.14.1)': + '@rushstack/terminal@0.14.0(@types/node@22.15.2)': dependencies: - '@rushstack/node-core-library': 5.7.0(@types/node@22.14.1) + '@rushstack/node-core-library': 5.7.0(@types/node@22.15.2) supports-color: 8.1.1 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.2 - '@rushstack/ts-command-line@4.22.6(@types/node@22.14.1)': + '@rushstack/ts-command-line@4.22.6(@types/node@22.15.2)': dependencies: - '@rushstack/terminal': 0.14.0(@types/node@22.14.1) + '@rushstack/terminal': 0.14.0(@types/node@22.15.2) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -5193,12 +5328,12 @@ snapshots: transitivePeerDependencies: - encoding - '@tanstack/config@0.18.0(@types/node@22.14.1)(eslint@9.25.1(jiti@2.4.2))(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': + '@tanstack/config@0.18.0(@types/node@22.15.2)(eslint@9.25.1(jiti@2.4.2))(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@tanstack/eslint-config': 0.1.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@tanstack/publish-config': 0.1.0 '@tanstack/typedoc-config': 0.2.0(typescript@5.8.3) - '@tanstack/vite-config': 0.2.0(@types/node@22.14.1)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + '@tanstack/vite-config': 0.2.0(@types/node@22.15.2)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) transitivePeerDependencies: - '@types/node' - eslint @@ -5230,6 +5365,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/query-core@5.74.4': {} + + '@tanstack/query-devtools@5.74.6': {} + + '@tanstack/react-query-devtools@5.74.6(@tanstack/react-query@5.74.4(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/query-devtools': 5.74.6 + '@tanstack/react-query': 5.74.4(react@19.1.0) + react: 19.1.0 + + '@tanstack/react-query@5.74.4(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.74.4 + react: 19.1.0 + '@tanstack/typedoc-config@0.2.0(typescript@5.8.3)': dependencies: typedoc: 0.27.9(typescript@5.8.3) @@ -5238,12 +5388,12 @@ snapshots: transitivePeerDependencies: - typescript - '@tanstack/vite-config@0.2.0(@types/node@22.14.1)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': + '@tanstack/vite-config@0.2.0(@types/node@22.15.2)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(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.14.1)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) - vite-plugin-externalize-deps: 0.9.0(vite@6.3.2(@types/node@22.14.1)(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.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite-plugin-dts: 4.2.3(@types/node@22.15.2)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite-plugin-externalize-deps: 0.9.0(vite@6.3.3(@types/node@22.15.2)(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.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) transitivePeerDependencies: - '@types/node' - rollup @@ -5290,7 +5440,7 @@ snapshots: '@types/conventional-commits-parser@5.0.1': dependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.2 '@types/doctrine@0.0.9': {} @@ -5304,7 +5454,7 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@22.14.1': + '@types/node@22.15.2': dependencies: undici-types: 6.21.0 @@ -5480,14 +5630,14 @@ snapshots: '@typescript-eslint/types': 8.31.0 eslint-visitor-keys: 4.2.0 - '@vitejs/plugin-react@4.4.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': + '@vitejs/plugin-react@4.4.1(vite@6.3.3(@types/node@22.15.2)(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) '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color @@ -5498,13 +5648,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.2(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': + '@vitest/mocker@3.1.2(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@vitest/spy': 3.1.2 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) '@vitest/pretty-format@3.1.2': dependencies: @@ -5587,11 +5737,11 @@ snapshots: dependencies: argparse: 2.0.1 - '@zod/core@0.8.1': {} + '@zod/core@0.9.0': {} - '@zod/mini@4.0.0-beta.20250420T053007': + '@zod/mini@4.0.0-beta.20250424T163858': dependencies: - '@zod/core': 0.8.1 + '@zod/core': 0.9.0 JSONStream@1.3.5: dependencies: @@ -6043,14 +6193,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-debug@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): + eslint-plugin-react-debug@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.48.4 - '@eslint-react/kit': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/ast': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.48.5 + '@eslint-react/kit': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.31.0 '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/types': 8.31.0 @@ -6063,14 +6213,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-dom@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): + eslint-plugin-react-dom@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.48.4 - '@eslint-react/kit': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/ast': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.48.5 + '@eslint-react/kit': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.31.0 '@typescript-eslint/types': 8.31.0 '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) @@ -6083,14 +6233,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks-extra@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): + eslint-plugin-react-hooks-extra@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.48.4 - '@eslint-react/kit': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/ast': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.48.5 + '@eslint-react/kit': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.31.0 '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/types': 8.31.0 @@ -6107,14 +6257,14 @@ snapshots: dependencies: eslint: 9.25.1(jiti@2.4.2) - eslint-plugin-react-naming-convention@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): + eslint-plugin-react-naming-convention@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.48.4 - '@eslint-react/kit': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/ast': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.48.5 + '@eslint-react/kit': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.31.0 '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/types': 8.31.0 @@ -6127,14 +6277,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-web-api@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): + eslint-plugin-react-web-api@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.48.4 - '@eslint-react/kit': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/ast': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.48.5 + '@eslint-react/kit': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.31.0 '@typescript-eslint/types': 8.31.0 '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) @@ -6146,14 +6296,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-x@1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): + eslint-plugin-react-x@1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/core': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/eff': 1.48.4 - '@eslint-react/kit': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/shared': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) - '@eslint-react/var': 1.48.4(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/ast': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/core': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/eff': 1.48.5 + '@eslint-react/kit': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/shared': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@eslint-react/var': 1.48.5(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.31.0 '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/types': 8.31.0 @@ -6630,10 +6780,10 @@ snapshots: dependencies: json-buffer: 3.0.1 - knip@5.50.5(@types/node@22.14.1)(typescript@5.8.3): + knip@5.50.5(@types/node@22.15.2)(typescript@5.8.3): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 22.14.1 + '@types/node': 22.15.2 easy-table: 1.2.0 enhanced-resolve: 5.18.1 fast-glob: 3.3.3 @@ -6799,7 +6949,7 @@ snapshots: nwsapi@2.2.18: {} - nx@20.8.0: + nx@20.8.1: dependencies: '@napi-rs/wasm-runtime': 0.2.4 '@yarnpkg/lockfile': 1.1.0 @@ -6836,16 +6986,16 @@ snapshots: yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@nx/nx-darwin-arm64': 20.8.0 - '@nx/nx-darwin-x64': 20.8.0 - '@nx/nx-freebsd-x64': 20.8.0 - '@nx/nx-linux-arm-gnueabihf': 20.8.0 - '@nx/nx-linux-arm64-gnu': 20.8.0 - '@nx/nx-linux-arm64-musl': 20.8.0 - '@nx/nx-linux-x64-gnu': 20.8.0 - '@nx/nx-linux-x64-musl': 20.8.0 - '@nx/nx-win32-arm64-msvc': 20.8.0 - '@nx/nx-win32-x64-msvc': 20.8.0 + '@nx/nx-darwin-arm64': 20.8.1 + '@nx/nx-darwin-x64': 20.8.1 + '@nx/nx-freebsd-x64': 20.8.1 + '@nx/nx-linux-arm-gnueabihf': 20.8.1 + '@nx/nx-linux-arm64-gnu': 20.8.1 + '@nx/nx-linux-arm64-musl': 20.8.1 + '@nx/nx-linux-x64-gnu': 20.8.1 + '@nx/nx-linux-x64-musl': 20.8.1 + '@nx/nx-win32-arm64-msvc': 20.8.1 + '@nx/nx-win32-x64-msvc': 20.8.1 transitivePeerDependencies: - debug @@ -7436,13 +7586,13 @@ snapshots: validate-html-nesting@1.2.2: {} - vite-node@3.1.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): + vite-node@3.1.2(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - jiti @@ -7457,9 +7607,9 @@ snapshots: - tsx - yaml - vite-plugin-dts@4.2.3(@types/node@22.14.1)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): + vite-plugin-dts@4.2.3(@types/node@22.15.2)(rollup@4.35.0)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): dependencies: - '@microsoft/api-extractor': 7.47.7(@types/node@22.14.1) + '@microsoft/api-extractor': 7.47.7(@types/node@22.15.2) '@rollup/pluginutils': 5.1.4(rollup@4.35.0) '@volar/typescript': 2.4.12 '@vue/language-core': 2.1.6(typescript@5.8.3) @@ -7470,17 +7620,17 @@ snapshots: magic-string: 0.30.17 typescript: 5.8.3 optionalDependencies: - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.2)(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.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): + vite-plugin-externalize-deps@0.9.0(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): dependencies: - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.2)(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.5)(vite@6.3.2(@types/node@22.14.1)(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.5)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): dependencies: '@babel/core': 7.26.9 '@types/babel__core': 7.20.5 @@ -7488,47 +7638,47 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.5 solid-refresh: 0.6.3(solid-js@1.9.5) - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - vitefu: 1.0.6(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vitefu: 1.0.6(vite@6.3.3(@types/node@22.15.2)(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.2(@types/node@22.14.1)(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.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.8.3) optionalDependencies: - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color - typescript - vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): + vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): dependencies: esbuild: 0.25.0 - fdir: 6.4.3(picomatch@4.0.2) + fdir: 6.4.4(picomatch@4.0.2) picomatch: 4.0.2 postcss: 8.5.3 rollup: 4.35.0 - tinyglobby: 0.2.12 + tinyglobby: 0.2.13 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.2 fsevents: 2.3.3 jiti: 2.4.2 tsx: 4.19.3 yaml: 2.7.0 - vitefu@1.0.6(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): + vitefu@1.0.6(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): optionalDependencies: - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - vitest@3.1.2(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(tsx@4.19.3)(yaml@2.7.0): + vitest@3.1.2(@types/node@22.15.2)(jiti@2.4.2)(jsdom@26.1.0)(tsx@4.19.3)(yaml@2.7.0): dependencies: '@vitest/expect': 3.1.2 - '@vitest/mocker': 3.1.2(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + '@vitest/mocker': 3.1.2(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) '@vitest/pretty-format': 3.1.2 '@vitest/runner': 3.1.2 '@vitest/snapshot': 3.1.2 @@ -7545,11 +7695,11 @@ snapshots: tinyglobby: 0.2.13 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) - vite-node: 3.1.2(@types/node@22.14.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite-node: 3.1.2(@types/node@22.15.2)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.2 jsdom: 26.1.0 transitivePeerDependencies: - jiti
      Execution Count:{throttler.executionCount()}
      Instant Search: {instantSearch()}