From 7fb68db31299ed2ea7f7a73436152aea1f5c4fdb Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Sun, 27 Apr 2025 14:30:29 -0500 Subject: [PATCH 01/10] work on unifying queueing more and create first react query examples --- .changeset/tricky-pants-hear-react.md | 7 + .changeset/tricky-pants-hear.md | 6 + README.md | 2 +- docs/config.json | 14 +- .../reference/functions/useasyncdebouncer.md | 2 +- ...cqueuerstate.md => useasyncqueuedstate.md} | 12 +- .../reference/functions/useasyncqueuer.md | 2 +- .../functions/useasyncratelimiter.md | 2 +- .../reference/functions/useasyncthrottler.md | 2 +- .../functions/usedebouncedcallback.md | 2 +- .../reference/functions/usedebouncedstate.md | 2 +- .../reference/functions/usedebouncedvalue.md | 2 +- .../react/reference/functions/usedebouncer.md | 2 +- .../{usequeuerstate.md => usequeuedstate.md} | 14 +- .../reference/functions/usequeuedvalue.md | 67 ++ .../react/reference/functions/usequeuer.md | 4 +- .../functions/useratelimitedcallback.md | 2 +- .../functions/useratelimitedstate.md | 2 +- .../functions/useratelimitedvalue.md | 2 +- .../reference/functions/useratelimiter.md | 2 +- .../functions/usethrottledcallback.md | 2 +- .../reference/functions/usethrottledstate.md | 2 +- .../reference/functions/usethrottledvalue.md | 2 +- .../react/reference/functions/usethrottler.md | 2 +- docs/framework/react/reference/index.md | 5 +- docs/guides/async-queueing.md | 2 +- docs/overview.md | 11 +- docs/reference/classes/asyncqueuer.md | 65 +- docs/reference/classes/queuer.md | 59 +- docs/reference/functions/asyncqueue.md | 4 +- docs/reference/functions/queue.md | 2 +- .../interfaces/asyncqueueroptions.md | 88 +- docs/reference/interfaces/queueroptions.md | 86 +- docs/reference/type-aliases/queueposition.md | 2 +- examples/react/asyncDebounce/package.json | 2 +- examples/react/asyncRateLimit/package.json | 2 +- examples/react/asyncThrottle/package.json | 2 +- examples/react/debounce/package.json | 2 +- examples/react/queue/package.json | 2 +- examples/react/rateLimit/package.json | 2 +- .../.eslintrc.cjs | 0 .../.gitignore | 0 .../README.md | 0 .../index.html | 0 .../package.json | 36 + .../public/emblem-light.svg | 0 .../src/index.tsx | 141 +++ .../tsconfig.json | 0 .../vite.config.ts | 0 .../.eslintrc.cjs | 0 .../.gitignore | 0 .../README.md | 0 .../index.html | 0 .../react-query-queued-prefetch/package.json | 36 + .../public/emblem-light.svg | 0 .../react-query-queued-prefetch/src/index.tsx | 151 +++ .../tsconfig.json | 0 .../vite.config.ts | 0 .../.eslintrc.cjs | 13 + .../react-query-throttled-prefetch/.gitignore | 27 + .../react-query-throttled-prefetch/README.md | 6 + .../react-query-throttled-prefetch/index.html | 16 + .../package.json | 36 + .../public/emblem-light.svg | 13 + .../src/index.tsx | 141 +++ .../tsconfig.json | 23 + .../vite.config.ts | 13 + examples/react/throttle/package.json | 2 +- examples/react/useAsyncDebouncer/package.json | 2 +- .../react/useAsyncQueuedState/.eslintrc.cjs | 13 + examples/react/useAsyncQueuedState/.gitignore | 27 + examples/react/useAsyncQueuedState/README.md | 6 + examples/react/useAsyncQueuedState/index.html | 16 + .../package.json | 2 +- .../public/emblem-light.svg | 13 + .../src/index.tsx | 7 +- .../react/useAsyncQueuedState/tsconfig.json | 23 + .../react/useAsyncQueuedState/vite.config.ts | 13 + examples/react/useAsyncQueuer/package.json | 2 +- examples/react/useAsyncQueuer/src/index.tsx | 1 + .../react/useAsyncRateLimiter/package.json | 2 +- examples/react/useAsyncThrottler/package.json | 2 +- .../react/useDebouncedCallback/package.json | 2 +- examples/react/useDebouncedState/package.json | 2 +- examples/react/useDebouncedValue/package.json | 2 +- examples/react/useDebouncer/package.json | 2 +- examples/react/useQueuedState/.eslintrc.cjs | 13 + examples/react/useQueuedState/.gitignore | 27 + examples/react/useQueuedState/README.md | 6 + examples/react/useQueuedState/index.html | 16 + .../package.json | 4 +- .../useQueuedState/public/emblem-light.svg | 13 + .../src/index.tsx | 9 +- examples/react/useQueuedState/tsconfig.json | 23 + examples/react/useQueuedState/vite.config.ts | 13 + examples/react/useQueuedValue/.eslintrc.cjs | 13 + examples/react/useQueuedValue/.gitignore | 27 + examples/react/useQueuedValue/README.md | 6 + examples/react/useQueuedValue/index.html | 16 + examples/react/useQueuedValue/package.json | 34 + .../useQueuedValue/public/emblem-light.svg | 13 + examples/react/useQueuedValue/src/index.tsx | 73 ++ examples/react/useQueuedValue/tsconfig.json | 23 + examples/react/useQueuedValue/vite.config.ts | 13 + examples/react/useQueuer/package.json | 2 +- examples/react/useQueuer/src/index.tsx | 1 + .../react/useRateLimitedCallback/package.json | 2 +- .../react/useRateLimitedState/package.json | 2 +- .../react/useRateLimitedValue/package.json | 2 +- examples/react/useRateLimiter/package.json | 2 +- .../react/useThrottledCallback/package.json | 2 +- examples/react/useThrottledState/package.json | 2 +- examples/react/useThrottledValue/package.json | 2 +- examples/react/useThrottler/package.json | 2 +- examples/solid/asyncDebounce/package.json | 2 +- examples/solid/asyncRateLimit/package.json | 2 +- examples/solid/asyncThrottle/package.json | 2 +- .../solid/createAsyncDebouncer/package.json | 2 +- examples/solid/createAsyncQueuer/package.json | 2 +- .../solid/createAsyncRateLimiter/package.json | 2 +- .../solid/createAsyncThrottler/package.json | 2 +- .../solid/createDebouncedSignal/package.json | 2 +- .../solid/createDebouncedValue/package.json | 2 +- examples/solid/createDebouncer/package.json | 2 +- examples/solid/createQueuer/package.json | 2 +- .../createRateLimitedSignal/package.json | 2 +- .../solid/createRateLimitedValue/package.json | 2 +- examples/solid/createRateLimiter/package.json | 2 +- .../solid/createThrottledSignal/package.json | 2 +- .../solid/createThrottledValue/package.json | 2 +- examples/solid/createThrottler/package.json | 2 +- examples/solid/debounce/package.json | 2 +- examples/solid/queue/package.json | 2 +- examples/solid/rateLimit/package.json | 2 +- examples/solid/throttle/package.json | 2 +- package.json | 6 +- packages/pacer/src/async-queuer.ts | 96 +- packages/pacer/src/queuer.ts | 91 ++ packages/pacer/tests/async-queuer.test.ts | 14 +- packages/react-pacer/package.json | 2 +- .../react-pacer/src/async-queuer/index.ts | 2 +- ...cQueuerState.ts => useAsyncQueuedState.ts} | 4 +- packages/react-pacer/src/index.ts | 5 +- packages/react-pacer/src/queuer/index.ts | 3 +- .../{useQueuerState.ts => useQueuedState.ts} | 8 +- .../react-pacer/src/queuer/useQueuedValue.ts | 60 ++ packages/react-pacer/src/queuer/useQueuer.ts | 2 +- pnpm-lock.yaml | 870 ++++++++++-------- 148 files changed, 2302 insertions(+), 549 deletions(-) create mode 100644 .changeset/tricky-pants-hear-react.md create mode 100644 .changeset/tricky-pants-hear.md rename docs/framework/react/reference/functions/{useasyncqueuerstate.md => useasyncqueuedstate.md} (78%) rename docs/framework/react/reference/functions/{usequeuerstate.md => usequeuedstate.md} (80%) create mode 100644 docs/framework/react/reference/functions/usequeuedvalue.md rename examples/react/{useAsyncQueuerState => react-query-debounced-prefetch}/.eslintrc.cjs (100%) rename examples/react/{useAsyncQueuerState => react-query-debounced-prefetch}/.gitignore (100%) rename examples/react/{useAsyncQueuerState => react-query-debounced-prefetch}/README.md (100%) rename examples/react/{useAsyncQueuerState => react-query-debounced-prefetch}/index.html (100%) create mode 100644 examples/react/react-query-debounced-prefetch/package.json rename examples/react/{useAsyncQueuerState => react-query-debounced-prefetch}/public/emblem-light.svg (100%) create mode 100644 examples/react/react-query-debounced-prefetch/src/index.tsx rename examples/react/{useAsyncQueuerState => react-query-debounced-prefetch}/tsconfig.json (100%) rename examples/react/{useAsyncQueuerState => react-query-debounced-prefetch}/vite.config.ts (100%) rename examples/react/{useQueuerState => react-query-queued-prefetch}/.eslintrc.cjs (100%) rename examples/react/{useQueuerState => react-query-queued-prefetch}/.gitignore (100%) rename examples/react/{useQueuerState => react-query-queued-prefetch}/README.md (100%) rename examples/react/{useQueuerState => react-query-queued-prefetch}/index.html (100%) create mode 100644 examples/react/react-query-queued-prefetch/package.json rename examples/react/{useQueuerState => react-query-queued-prefetch}/public/emblem-light.svg (100%) create mode 100644 examples/react/react-query-queued-prefetch/src/index.tsx rename examples/react/{useQueuerState => react-query-queued-prefetch}/tsconfig.json (100%) rename examples/react/{useQueuerState => react-query-queued-prefetch}/vite.config.ts (100%) create mode 100644 examples/react/react-query-throttled-prefetch/.eslintrc.cjs create mode 100644 examples/react/react-query-throttled-prefetch/.gitignore create mode 100644 examples/react/react-query-throttled-prefetch/README.md create mode 100644 examples/react/react-query-throttled-prefetch/index.html create mode 100644 examples/react/react-query-throttled-prefetch/package.json create mode 100644 examples/react/react-query-throttled-prefetch/public/emblem-light.svg create mode 100644 examples/react/react-query-throttled-prefetch/src/index.tsx create mode 100644 examples/react/react-query-throttled-prefetch/tsconfig.json create mode 100644 examples/react/react-query-throttled-prefetch/vite.config.ts create mode 100644 examples/react/useAsyncQueuedState/.eslintrc.cjs create mode 100644 examples/react/useAsyncQueuedState/.gitignore create mode 100644 examples/react/useAsyncQueuedState/README.md create mode 100644 examples/react/useAsyncQueuedState/index.html rename examples/react/{useAsyncQueuerState => useAsyncQueuedState}/package.json (97%) create mode 100644 examples/react/useAsyncQueuedState/public/emblem-light.svg rename examples/react/{useAsyncQueuerState => useAsyncQueuedState}/src/index.tsx (94%) create mode 100644 examples/react/useAsyncQueuedState/tsconfig.json create mode 100644 examples/react/useAsyncQueuedState/vite.config.ts create mode 100644 examples/react/useQueuedState/.eslintrc.cjs create mode 100644 examples/react/useQueuedState/.gitignore create mode 100644 examples/react/useQueuedState/README.md create mode 100644 examples/react/useQueuedState/index.html rename examples/react/{useQueuerState => useQueuedState}/package.json (88%) create mode 100644 examples/react/useQueuedState/public/emblem-light.svg rename examples/react/{useQueuerState => useQueuedState}/src/index.tsx (90%) create mode 100644 examples/react/useQueuedState/tsconfig.json create mode 100644 examples/react/useQueuedState/vite.config.ts create mode 100644 examples/react/useQueuedValue/.eslintrc.cjs create mode 100644 examples/react/useQueuedValue/.gitignore create mode 100644 examples/react/useQueuedValue/README.md create mode 100644 examples/react/useQueuedValue/index.html create mode 100644 examples/react/useQueuedValue/package.json create mode 100644 examples/react/useQueuedValue/public/emblem-light.svg create mode 100644 examples/react/useQueuedValue/src/index.tsx create mode 100644 examples/react/useQueuedValue/tsconfig.json create mode 100644 examples/react/useQueuedValue/vite.config.ts rename packages/react-pacer/src/async-queuer/{useAsyncQueuerState.ts => useAsyncQueuedState.ts} (94%) rename packages/react-pacer/src/queuer/{useQueuerState.ts => useQueuedState.ts} (91%) create mode 100644 packages/react-pacer/src/queuer/useQueuedValue.ts 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.md b/.changeset/tricky-pants-hear.md new file mode 100644 index 000000000..e44f02081 --- /dev/null +++ b/.changeset/tricky-pants-hear.md @@ -0,0 +1,6 @@ +--- +'@tanstack/pacer': minor +--- + +- feat: add queuer expiration feature to `AsyncQueuer` and `Queuer` +- breaking: Set `started` to `true` by default in `AsyncQueuer` and `Queuer` 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..ce8a83cb7 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" } ] }, diff --git a/docs/framework/react/reference/functions/useasyncdebouncer.md b/docs/framework/react/reference/functions/useasyncdebouncer.md index 9ea08f281..657e10751 100644 --- a/docs/framework/react/reference/functions/useasyncdebouncer.md +++ b/docs/framework/react/reference/functions/useasyncdebouncer.md @@ -11,7 +11,7 @@ title: useAsyncDebouncer 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. 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..f28bbfac1 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 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..9880ce233 100644 --- a/docs/framework/react/reference/functions/useasyncratelimiter.md +++ b/docs/framework/react/reference/functions/useasyncratelimiter.md @@ -11,7 +11,7 @@ title: useAsyncRateLimiter 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. diff --git a/docs/framework/react/reference/functions/useasyncthrottler.md b/docs/framework/react/reference/functions/useasyncthrottler.md index 73d4c9ca2..612d55fee 100644 --- a/docs/framework/react/reference/functions/useasyncthrottler.md +++ b/docs/framework/react/reference/functions/useasyncthrottler.md @@ -11,7 +11,7 @@ title: useAsyncThrottler 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. diff --git a/docs/framework/react/reference/functions/usedebouncedcallback.md b/docs/framework/react/reference/functions/usedebouncedcallback.md index d8c3ddbcf..540a83184 100644 --- a/docs/framework/react/reference/functions/usedebouncedcallback.md +++ b/docs/framework/react/reference/functions/usedebouncedcallback.md @@ -11,7 +11,7 @@ title: useDebouncedCallback 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 diff --git a/docs/framework/react/reference/functions/usedebouncedstate.md b/docs/framework/react/reference/functions/usedebouncedstate.md index ecd283154..d58a8658e 100644 --- a/docs/framework/react/reference/functions/usedebouncedstate.md +++ b/docs/framework/react/reference/functions/usedebouncedstate.md @@ -11,7 +11,7 @@ title: useDebouncedState function useDebouncedState(value, options): [TValue, Dispatch>, Debouncer>, [TValue]>] ``` -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. diff --git a/docs/framework/react/reference/functions/usedebouncedvalue.md b/docs/framework/react/reference/functions/usedebouncedvalue.md index e4f9c9099..32b5eac27 100644 --- a/docs/framework/react/reference/functions/usedebouncedvalue.md +++ b/docs/framework/react/reference/functions/usedebouncedvalue.md @@ -11,7 +11,7 @@ title: useDebouncedValue function useDebouncedValue(value, options): [TValue, Debouncer>, [TValue]>] ``` -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 diff --git a/docs/framework/react/reference/functions/usedebouncer.md b/docs/framework/react/reference/functions/usedebouncer.md index 94faa3d90..21908bfa8 100644 --- a/docs/framework/react/reference/functions/usedebouncer.md +++ b/docs/framework/react/reference/functions/usedebouncer.md @@ -11,7 +11,7 @@ title: useDebouncer 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. diff --git a/docs/framework/react/reference/functions/usequeuerstate.md b/docs/framework/react/reference/functions/usequeuedstate.md similarity index 80% rename from docs/framework/react/reference/functions/usequeuerstate.md rename to docs/framework/react/reference/functions/usequeuedstate.md index 51fbde728..ee9d32e5f 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 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..9df870752 --- /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 + +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..9f7cebb31 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 diff --git a/docs/framework/react/reference/functions/useratelimitedstate.md b/docs/framework/react/reference/functions/useratelimitedstate.md index 255e17695..2c39d0959 100644 --- a/docs/framework/react/reference/functions/useratelimitedstate.md +++ b/docs/framework/react/reference/functions/useratelimitedstate.md @@ -11,7 +11,7 @@ title: useRateLimitedState function useRateLimitedState(value, options): [TValue, Dispatch>, RateLimiter>, [TValue]>] ``` -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. diff --git a/docs/framework/react/reference/functions/useratelimitedvalue.md b/docs/framework/react/reference/functions/useratelimitedvalue.md index 896ff4641..127592357 100644 --- a/docs/framework/react/reference/functions/useratelimitedvalue.md +++ b/docs/framework/react/reference/functions/useratelimitedvalue.md @@ -11,7 +11,7 @@ title: useRateLimitedValue function useRateLimitedValue(value, options): [TValue, RateLimiter>, [TValue]>] ``` -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:55](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts#L55) 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. diff --git a/docs/framework/react/reference/functions/useratelimiter.md b/docs/framework/react/reference/functions/useratelimiter.md index 9e0fdc536..768f2ea23 100644 --- a/docs/framework/react/reference/functions/useratelimiter.md +++ b/docs/framework/react/reference/functions/useratelimiter.md @@ -11,7 +11,7 @@ title: useRateLimiter 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. diff --git a/docs/framework/react/reference/functions/usethrottledcallback.md b/docs/framework/react/reference/functions/usethrottledcallback.md index 1898d3784..6ed48651e 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 diff --git a/docs/framework/react/reference/functions/usethrottledstate.md b/docs/framework/react/reference/functions/usethrottledstate.md index 780825175..6ed11f830 100644 --- a/docs/framework/react/reference/functions/usethrottledstate.md +++ b/docs/framework/react/reference/functions/usethrottledstate.md @@ -11,7 +11,7 @@ title: useThrottledState function useThrottledState(value, options): [TValue, Dispatch>, Throttler>, [TValue]>] ``` -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. diff --git a/docs/framework/react/reference/functions/usethrottledvalue.md b/docs/framework/react/reference/functions/usethrottledvalue.md index cfac412e8..1396b457b 100644 --- a/docs/framework/react/reference/functions/usethrottledvalue.md +++ b/docs/framework/react/reference/functions/usethrottledvalue.md @@ -11,7 +11,7 @@ title: useThrottledValue function useThrottledValue(value, options): [TValue, Throttler>, [TValue]>] ``` -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:36](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledValue.ts#L36) A high-level React hook that creates a throttled version of a value that updates at most once within a specified time window. This hook uses React's useState internally to manage the throttled state. diff --git a/docs/framework/react/reference/functions/usethrottler.md b/docs/framework/react/reference/functions/usethrottler.md index 2ec0a50e0..70df358ee 100644 --- a/docs/framework/react/reference/functions/usethrottler.md +++ b/docs/framework/react/reference/functions/usethrottler.md @@ -11,7 +11,7 @@ title: useThrottler 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. 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/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/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/asyncqueuer.md b/docs/reference/classes/asyncqueuer.md index a441d35af..2796b96a7 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:327](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L327) 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:307](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L307) 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:465](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L465) 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:458](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L458) 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:479](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L479) 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:541](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L541) + +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:437](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L437) 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:444](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L444) 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:500](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L500) 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:493](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L493) 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:401](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L401) 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:162](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L162) 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:425](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L425) 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:472](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L472) 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:486](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L486) 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:451](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L451) 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:519](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L519) 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:529](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L529) 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:507](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L507) 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:315](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L315) Resets the queuer to its initial state @@ -439,7 +456,7 @@ Resets the queuer to its initial state setOptions(newOptions): AsyncQueuerOptions ``` -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 @@ -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:275](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L275) 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:298](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L298) Stops the queuer from processing items diff --git a/docs/reference/classes/queuer.md b/docs/reference/classes/queuer.md index 4373580cc..97af00d06 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:309](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L309) 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:288](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L288) 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:431](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L431) 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:438](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L438) 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:452](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L452) + +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:410](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L410) 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:417](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L417) 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:466](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L466) 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:459](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L459) 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:366](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L366) 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:180](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L180) 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:398](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L398) 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:445](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L445) 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:424](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L424) 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:296](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L296) Resets the queuer to its initial state @@ -369,7 +390,7 @@ Resets the queuer to its initial state setOptions(newOptions): QueuerOptions ``` -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 @@ -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:276](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L276) 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:267](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L267) Stops the queuer from processing items diff --git a/docs/reference/functions/asyncqueue.md b/docs/reference/functions/asyncqueue.md index 23734bfba..5f49c694c 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:563](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L563) 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/queue.md b/docs/reference/functions/queue.md index 8c347c503..e91637860 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:499](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L499) 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/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/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/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/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/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/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/queue/package.json b/examples/react/queue/package.json index 3f4264f40..efb564fbf 100644 --- a/examples/react/queue/package.json +++ b/examples/react/queue/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/rateLimit/package.json b/examples/react/rateLimit/package.json index edc9a1575..19ee02221 100644 --- a/examples/react/rateLimit/package.json +++ b/examples/react/rateLimit/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/useAsyncQueuerState/.eslintrc.cjs b/examples/react/react-query-debounced-prefetch/.eslintrc.cjs similarity index 100% rename from examples/react/useAsyncQueuerState/.eslintrc.cjs rename to examples/react/react-query-debounced-prefetch/.eslintrc.cjs diff --git a/examples/react/useAsyncQueuerState/.gitignore b/examples/react/react-query-debounced-prefetch/.gitignore similarity index 100% rename from examples/react/useAsyncQueuerState/.gitignore rename to examples/react/react-query-debounced-prefetch/.gitignore diff --git a/examples/react/useAsyncQueuerState/README.md b/examples/react/react-query-debounced-prefetch/README.md similarity index 100% rename from examples/react/useAsyncQueuerState/README.md rename to examples/react/react-query-debounced-prefetch/README.md diff --git a/examples/react/useAsyncQueuerState/index.html b/examples/react/react-query-debounced-prefetch/index.html similarity index 100% rename from examples/react/useAsyncQueuerState/index.html rename to examples/react/react-query-debounced-prefetch/index.html diff --git a/examples/react/react-query-debounced-prefetch/package.json b/examples/react/react-query-debounced-prefetch/package.json new file mode 100644 index 000000000..04421af35 --- /dev/null +++ b/examples/react/react-query-debounced-prefetch/package.json @@ -0,0 +1,36 @@ +{ + "name": "@tanstack/pacer-example-react-query-debounced-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/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..9afd45e96 --- /dev/null +++ b/examples/react/react-query-debounced-prefetch/src/index.tsx @@ -0,0 +1,141 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { + QueryClient, + QueryClientProvider, + usePrefetchQuery, + 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 + usePrefetchQuery({ + queryKey: ['post', debouncedHoveredPostId], + queryFn: () => fetchPost(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..f4e220aff --- /dev/null +++ b/examples/react/react-query-queued-prefetch/src/index.tsx @@ -0,0 +1,151 @@ +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 && + !queryClient.getQueryData(['post', queuedHoveredPostId]) + ) { + // queue up the hovered post id to be processed in order + queryClient.prefetchQuery({ + queryKey: ['post', queuedHoveredPostId], + queryFn: () => fetchPost(queuedHoveredPostId as number), + }) + } + }, [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..1ae9a5b67 --- /dev/null +++ b/examples/react/react-query-throttled-prefetch/src/index.tsx @@ -0,0 +1,141 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { + QueryClient, + QueryClientProvider, + usePrefetchQuery, + 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 + usePrefetchQuery({ + queryKey: ['post', throttledHoveredPostId], + queryFn: () => fetchPost(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/useAsyncDebouncer/package.json b/examples/react/useAsyncDebouncer/package.json index 77ca5dad2..e6645e703 100644 --- a/examples/react/useAsyncDebouncer/package.json +++ b/examples/react/useAsyncDebouncer/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/.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/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/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/useDebouncedState/package.json b/examples/react/useDebouncedState/package.json index f5ed9e602..7c37bf40b 100644 --- a/examples/react/useDebouncedState/package.json +++ b/examples/react/useDebouncedState/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/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/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/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..897db1949 --- /dev/null +++ b/examples/react/useQueuedValue/src/index.tsx @@ -0,0 +1,73 @@ +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/useRateLimitedState/package.json b/examples/react/useRateLimitedState/package.json index 29338a929..cc9e1720c 100644 --- a/examples/react/useRateLimitedState/package.json +++ b/examples/react/useRateLimitedState/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/useRateLimitedValue/package.json b/examples/react/useRateLimitedValue/package.json index c99a0edc5..d350fb23c 100644 --- a/examples/react/useRateLimitedValue/package.json +++ b/examples/react/useRateLimitedValue/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/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/useThrottledCallback/package.json b/examples/react/useThrottledCallback/package.json index e4fcd4dad..c3b0ca435 100644 --- a/examples/react/useThrottledCallback/package.json +++ b/examples/react/useThrottledCallback/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/useThrottledState/package.json b/examples/react/useThrottledState/package.json index 5a23521aa..1d28ee64c 100644 --- a/examples/react/useThrottledState/package.json +++ b/examples/react/useThrottledState/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/useThrottledValue/package.json b/examples/react/useThrottledValue/package.json index 2f631e16b..285be47c7 100644 --- a/examples/react/useThrottledValue/package.json +++ b/examples/react/useThrottledValue/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/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/solid/asyncDebounce/package.json b/examples/solid/asyncDebounce/package.json index bc69ebe5f..cf6cac3bb 100644 --- a/examples/solid/asyncDebounce/package.json +++ b/examples/solid/asyncDebounce/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/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/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/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/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/createAsyncRateLimiter/package.json b/examples/solid/createAsyncRateLimiter/package.json index 7c230af09..02351f79a 100644 --- a/examples/solid/createAsyncRateLimiter/package.json +++ b/examples/solid/createAsyncRateLimiter/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/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/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/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/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/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/createRateLimitedValue/package.json b/examples/solid/createRateLimitedValue/package.json index 39e2fede2..dd9d83d15 100644 --- a/examples/solid/createRateLimitedValue/package.json +++ b/examples/solid/createRateLimitedValue/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/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/createThrottledSignal/package.json b/examples/solid/createThrottledSignal/package.json index 6fc3c9d71..974279e54 100644 --- a/examples/solid/createThrottledSignal/package.json +++ b/examples/solid/createThrottledSignal/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/createThrottledValue/package.json b/examples/solid/createThrottledValue/package.json index 5d4bd0be9..fca17eadb 100644 --- a/examples/solid/createThrottledValue/package.json +++ b/examples/solid/createThrottledValue/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/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/debounce/package.json b/examples/solid/debounce/package.json index 08269120e..925e0f4d2 100644 --- a/examples/solid/debounce/package.json +++ b/examples/solid/debounce/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/queue/package.json b/examples/solid/queue/package.json index 129ee10f0..10b1e4b20 100644 --- a/examples/solid/queue/package.json +++ b/examples/solid/queue/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/rateLimit/package.json b/examples/solid/rateLimit/package.json index d3f1e48d7..73c13a7cc 100644 --- a/examples/solid/rateLimit/package.json +++ b/examples/solid/rateLimit/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/throttle/package.json b/examples/solid/throttle/package.json index 5dfafef26..9f5c6edf3 100644 --- a/examples/solid/throttle/package.json +++ b/examples/solid/throttle/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/package.json b/package.json index b8f056731..8afbf855f 100644 --- a/package.json +++ b/package.json @@ -57,12 +57,12 @@ "@svitejs/changesets-changelog-github-compact": "^1.2.0", "@tanstack/config": "0.18.0", "@testing-library/jest-dom": "^6.6.3", - "@types/node": "^22.14.1", + "@types/node": "^22.15.2", "eslint": "^9.25.1", "eslint-plugin-unused-imports": "^4.1.4", "jsdom": "^26.1.0", "knip": "^5.50.5", - "nx": "^20.8.0", + "nx": "^20.8.1", "premove": "^4.0.0", "prettier": "^3.5.3", "prettier-plugin-svelte": "^3.3.3", @@ -70,7 +70,7 @@ "sherif": "^1.5.0", "size-limit": "^11.2.0", "typescript": "5.8.3", - "vite": "^6.3.2", + "vite": "^6.3.3", "vitest": "^3.1.2" }, "overrides": { diff --git a/packages/pacer/src/async-queuer.ts b/packages/pacer/src/async-queuer.ts index 9c720b9ad..53a24a796 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> = [] @@ -152,6 +172,9 @@ export class AsyncQueuer { return } + // Check for expired items + this.checkExpiredItems() + while ( this._activeItems.size < this._options.concurrency && !this.getIsEmpty() @@ -196,6 +219,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 +368,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 +405,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 +534,13 @@ export class AsyncQueuer { ) } } + + /** + * Returns the number of items that have expired from the queuer + */ + getExpirationCount(): number { + return this._expirationCount + } } /** @@ -474,7 +560,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/queuer.ts b/packages/pacer/src/queuer.ts index d88b2bbf3..ee3c7f833 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 @@ -165,6 +189,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 +211,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 +326,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 +370,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 +446,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/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/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-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/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/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 From 1db6d47f4912fa4829d2e492dcf6ee62cc94b017 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Sat, 3 May 2025 18:44:14 -0500 Subject: [PATCH 02/10] fix issues with debouncer leading and trailing, start simplifying generics --- .changeset/tricky-pants-hear-react.md | 1 + .changeset/tricky-pants-hear-solid.md | 8 + .changeset/tricky-pants-hear.md | 2 + .../reference/functions/useasyncdebouncer.md | 8 +- .../functions/useasyncqueuedstate.md | 2 +- .../functions/usedebouncedcallback.md | 8 +- .../reference/functions/usedebouncedstate.md | 6 +- .../reference/functions/usedebouncedvalue.md | 14 +- .../react/reference/functions/usedebouncer.md | 8 +- .../reference/functions/usequeuedstate.md | 2 +- .../reference/functions/usequeuedvalue.md | 2 +- .../functions/createasyncdebouncer.md | 10 +- .../functions/createdebouncedsignal.md | 6 +- .../functions/createdebouncedvalue.md | 18 +- .../reference/functions/createdebouncer.md | 10 +- .../interfaces/solidasyncdebouncer.md | 10 +- .../reference/interfaces/soliddebouncer.md | 10 +- docs/reference/classes/asyncdebouncer.md | 38 +- docs/reference/classes/debouncer.md | 32 +- docs/reference/functions/asyncdebounce.md | 10 +- docs/reference/functions/debounce.md | 7 +- .../interfaces/asyncdebounceroptions.md | 18 +- docs/reference/interfaces/debounceroptions.md | 17 +- .../type-aliases/anyasyncfunction.md | 16 +- docs/reference/type-aliases/anyfunction.md | 16 +- examples/react/debounce/src/index.tsx | 1 + .../src/index.tsx | 2 +- .../react/useDebouncedCallback/src/index.tsx | 3 +- .../react/useDebouncedState/src/index.tsx | 29 +- .../react/useDebouncedValue/src/index.tsx | 24 +- examples/react/useDebouncer/src/index.tsx | 53 +- .../solid/createDebouncedSignal/src/index.tsx | 11 + .../solid/createDebouncedValue/src/index.tsx | 14 +- examples/solid/createDebouncer/src/index.tsx | 11 + examples/solid/debounce/src/index.tsx | 1 + packages/pacer/src/async-debouncer.ts | 38 +- packages/pacer/src/debouncer.ts | 50 +- packages/pacer/src/types.ts | 12 +- packages/pacer/tests/debouncer.test.ts | 970 +++++++++++------- .../src/async-debouncer/useAsyncDebouncer.ts | 19 +- .../src/debouncer/useDebouncedCallback.ts | 15 +- .../src/debouncer/useDebouncedState.ts | 7 +- .../src/debouncer/useDebouncedValue.ts | 20 +- .../react-pacer/src/debouncer/useDebouncer.ts | 18 +- .../async-debouncer/createAsyncDebouncer.ts | 22 +- .../src/debouncer/createDebouncedSignal.ts | 8 +- .../src/debouncer/createDebouncedValue.ts | 24 +- .../src/debouncer/createDebouncer.ts | 27 +- 48 files changed, 914 insertions(+), 744 deletions(-) create mode 100644 .changeset/tricky-pants-hear-solid.md diff --git a/.changeset/tricky-pants-hear-react.md b/.changeset/tricky-pants-hear-react.md index 33e9654d4..eca323743 100644 --- a/.changeset/tricky-pants-hear-react.md +++ b/.changeset/tricky-pants-hear-react.md @@ -4,4 +4,5 @@ - breaking: renamed `useQueuerState` hook to `useQueuedState` - breaking: changed return signature of `useQueuedState` to include the `addItem` function +- breaking: changed return signature of `useDebouncedValue` to return a single value and not a tuple - 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..e6411462c --- /dev/null +++ b/.changeset/tricky-pants-hear-solid.md @@ -0,0 +1,8 @@ +--- +'@tanstack/solid-pacer': minor +--- + +- breaking: renamed `useQueuerState` hook to `useQueuedState` +- breaking: changed return signature of `useQueuedState` to include the `addItem` function +- breaking: changed return signature of `createDebouncedValue` to return a single value and not a tuple +- feat: add `useQueuedValue` hook diff --git a/.changeset/tricky-pants-hear.md b/.changeset/tricky-pants-hear.md index e44f02081..61f0c38ef 100644 --- a/.changeset/tricky-pants-hear.md +++ b/.changeset/tricky-pants-hear.md @@ -4,3 +4,5 @@ - feat: add queuer expiration feature to `AsyncQueuer` and `Queuer` - 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` diff --git a/docs/framework/react/reference/functions/useasyncdebouncer.md b/docs/framework/react/reference/functions/useasyncdebouncer.md index 657e10751..0eae19e83 100644 --- a/docs/framework/react/reference/functions/useasyncdebouncer.md +++ b/docs/framework/react/reference/functions/useasyncdebouncer.md @@ -8,7 +8,7 @@ title: useAsyncDebouncer # Function: useAsyncDebouncer() ```ts -function useAsyncDebouncer(fn, options): AsyncDebouncer +function useAsyncDebouncer(fn, options): AsyncDebouncer ``` 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) @@ -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/useasyncqueuedstate.md b/docs/framework/react/reference/functions/useasyncqueuedstate.md index f28bbfac1..3d7944daf 100644 --- a/docs/framework/react/reference/functions/useasyncqueuedstate.md +++ b/docs/framework/react/reference/functions/useasyncqueuedstate.md @@ -11,7 +11,7 @@ title: useAsyncQueuedState function useAsyncQueuedState(options): [() => Promise[], AsyncQueuer] ``` -Defined in: react-pacer/src/async-queuer/useAsyncQueuedState.ts:53 +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. diff --git a/docs/framework/react/reference/functions/usedebouncedcallback.md b/docs/framework/react/reference/functions/usedebouncedcallback.md index 540a83184..d9da81160 100644 --- a/docs/framework/react/reference/functions/usedebouncedcallback.md +++ b/docs/framework/react/reference/functions/usedebouncedcallback.md @@ -8,7 +8,7 @@ title: useDebouncedCallback # Function: useDebouncedCallback() ```ts -function useDebouncedCallback(fn, options): (...args) => void +function useDebouncedCallback(fn, options): (...args) => void ``` Defined in: [react-pacer/src/debouncer/useDebouncedCallback.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedCallback.ts#L42) @@ -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 d58a8658e..c2be1d8d6 100644 --- a/docs/framework/react/reference/functions/usedebouncedstate.md +++ b/docs/framework/react/reference/functions/usedebouncedstate.md @@ -8,7 +8,7 @@ title: useDebouncedState # Function: useDebouncedState() ```ts -function useDebouncedState(value, options): [TValue, Dispatch>, Debouncer>, [TValue]>] +function useDebouncedState(value, options): [TValue, Dispatch>, Debouncer>>] ``` Defined in: [react-pacer/src/debouncer/useDebouncedState.ts:38](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedState.ts#L38) @@ -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 32b5eac27..d3869f36a 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 ``` -Defined in: [react-pacer/src/debouncer/useDebouncedValue.ts:41](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedValue.ts#L41) +Defined in: [react-pacer/src/debouncer/useDebouncedValue.ts:39](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedValue.ts#L39) 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,7 @@ 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. ## Type Parameters @@ -41,18 +39,18 @@ 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` ## Example ```tsx // Debounce a search query const [searchQuery, setSearchQuery] = useState(''); -const [debouncedQuery, debouncer] = useDebouncedValue(searchQuery, { +const debouncedQuery = useDebouncedValue(searchQuery, { wait: 500 // Wait 500ms after last change }); diff --git a/docs/framework/react/reference/functions/usedebouncer.md b/docs/framework/react/reference/functions/usedebouncer.md index 21908bfa8..22c162551 100644 --- a/docs/framework/react/reference/functions/usedebouncer.md +++ b/docs/framework/react/reference/functions/usedebouncer.md @@ -8,7 +8,7 @@ title: useDebouncer # Function: useDebouncer() ```ts -function useDebouncer(fn, options): Debouncer +function useDebouncer(fn, options): Debouncer ``` Defined in: [react-pacer/src/debouncer/useDebouncer.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncer.ts#L42) @@ -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/usequeuedstate.md b/docs/framework/react/reference/functions/usequeuedstate.md index ee9d32e5f..f220a392e 100644 --- a/docs/framework/react/reference/functions/usequeuedstate.md +++ b/docs/framework/react/reference/functions/usequeuedstate.md @@ -11,7 +11,7 @@ title: useQueuedState function useQueuedState(options): [TValue[], (item, position?, runOnUpdate?) => boolean, Queuer] ``` -Defined in: react-pacer/src/queuer/useQueuedState.ts:54 +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. diff --git a/docs/framework/react/reference/functions/usequeuedvalue.md b/docs/framework/react/reference/functions/usequeuedvalue.md index 9df870752..b2b84c35c 100644 --- a/docs/framework/react/reference/functions/usequeuedvalue.md +++ b/docs/framework/react/reference/functions/usequeuedvalue.md @@ -11,7 +11,7 @@ title: useQueuedValue function useQueuedValue(initialValue, options): [TValue, Queuer] ``` -Defined in: react-pacer/src/queuer/useQueuedValue.ts:40 +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. diff --git a/docs/framework/solid/reference/functions/createasyncdebouncer.md b/docs/framework/solid/reference/functions/createasyncdebouncer.md index 55607ad73..cd4ff526a 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:49](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L49) 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/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..69d633d44 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 ``` -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:40](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncedValue.ts#L40) 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 @@ -25,9 +25,7 @@ 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 (as an Accessor) -- The debouncer instance with control methods and state signals +The hook returns an Accessor that provides the current debounced value. ## Type Parameters @@ -41,18 +39,18 @@ 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`\> ## Example ```tsx // Debounce a search query const [searchQuery, setSearchQuery] = createSignal(''); -const [debouncedQuery, debouncer] = createDebouncedValue(searchQuery, { +const debouncedQuery = createDebouncedValue(searchQuery, { wait: 500 // Wait 500ms after last change }); @@ -61,10 +59,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); 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/interfaces/solidasyncdebouncer.md b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md index 4bb02fe4f..024349df6 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md +++ b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md @@ -5,20 +5,18 @@ 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`\>, `"getExecutionCount"` \| `"getIsPending"`\> ## Type Parameters • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### executionCount @@ -27,7 +25,7 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:8](https://github.com/TanSt executionCount: 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:10](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L10) *** @@ -37,4 +35,4 @@ 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:11](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L11) 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/reference/classes/asyncdebouncer.md b/docs/reference/classes/asyncdebouncer.md index bbec0fc40..5e8bde273 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:68](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L68) 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:77](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L77) #### 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,7 +67,7 @@ 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:166](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L166) Cancels any pending execution @@ -85,7 +83,7 @@ Cancels any pending execution getExecutionCount(): 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:182](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L182) Returns the number of times the function has been executed @@ -101,7 +99,7 @@ Returns the number of times the function has been executed 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:189](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L189) Returns `true` if there is a pending execution @@ -114,16 +112,16 @@ Returns `true` if there is a pending execution ### 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:104](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L104) Returns the current debouncer options #### Returns -`Required`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`, `TArgs`\>\> +`Required`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`\>\> *** @@ -133,7 +131,7 @@ Returns the current debouncer options 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:112](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L112) Attempts to execute the debounced function If a call is already in progress, it will be queued @@ -142,7 +140,7 @@ If a call is already in progress, it will be queued ##### args -...`TArgs` +...`Parameters`\<`TFn`\> #### Returns @@ -153,10 +151,10 @@ If a call is already in progress, it will be queued ### setOptions() ```ts -setOptions(newOptions): Required> +setOptions(newOptions): Required> ``` -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:91](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L91) Updates the debouncer options Returns the new options state @@ -165,8 +163,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`\>\> +`Required`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`\>\> diff --git a/docs/reference/classes/debouncer.md b/docs/reference/classes/debouncer.md index 908f03e2f..e522cb08a 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 @@ -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:105](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L105) 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:113](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L113) 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): Required> ``` -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`\>\> +`Required`\<[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`\>\> diff --git a/docs/reference/functions/asyncdebounce.md b/docs/reference/functions/asyncdebounce.md index 50ec16491..82a76ba34 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:213](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L213) 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,7 +42,7 @@ If a call is already in progress, it will be queued #### args -...`TArgs` +...`Parameters`\<`TFn`\> ### Returns diff --git a/docs/reference/functions/debounce.md b/docs/reference/functions/debounce.md index 5f99aead4..896473e4c 100644 --- a/docs/reference/functions/debounce.md +++ b/docs/reference/functions/debounce.md @@ -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/interfaces/asyncdebounceroptions.md b/docs/reference/interfaces/asyncdebounceroptions.md index 7d8199619..f36242226 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. @@ -51,7 +49,7 @@ Defaults to false. optional onError: (error) => 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 @@ -73,7 +71,7 @@ Optional error handler for when the debounced function throws optional onExecute: (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 @@ -81,7 +79,7 @@ Optional function to call when the debounced function is executed ##### debouncer -[`AsyncDebouncer`](../classes/asyncdebouncer.md)\<`TFn`, `TArgs`\> +[`AsyncDebouncer`](../classes/asyncdebouncer.md)\<`TFn`\> #### Returns @@ -95,7 +93,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:29](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L29) Whether to execute on the trailing edge of the timeout. Defaults to true. @@ -108,7 +106,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:34](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L34) Delay in milliseconds to wait after the last call before executing 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/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/examples/react/debounce/src/index.tsx b/examples/react/debounce/src/index.tsx index 35d97a14f..2fb1cc1b2 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 }), [], ) diff --git a/examples/react/react-query-debounced-prefetch/src/index.tsx b/examples/react/react-query-debounced-prefetch/src/index.tsx index 9afd45e96..7541f18d7 100644 --- a/examples/react/react-query-debounced-prefetch/src/index.tsx +++ b/examples/react/react-query-debounced-prefetch/src/index.tsx @@ -41,7 +41,7 @@ function PostList({ >(null) // debounce the hovered post id to avoid excessive prefetches - const [debouncedHoveredPostId] = useDebouncedValue(currentHoveredPostId, { + const debouncedHoveredPostId = useDebouncedValue(currentHoveredPostId, { wait: 100, // adjust this value to see the difference }) diff --git a/examples/react/useDebouncedCallback/src/index.tsx b/examples/react/useDebouncedCallback/src/index.tsx index 167965515..015b68d22 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() { diff --git a/examples/react/useDebouncedState/src/index.tsx b/examples/react/useDebouncedState/src/index.tsx index b7432448a..6192cbb9e 100644 --- a/examples/react/useDebouncedState/src/index.tsx +++ b/examples/react/useDebouncedState/src/index.tsx @@ -12,6 +12,7 @@ function App1() { { wait: 500, // enabled: instantCount > 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 }, ) @@ -87,13 +97,22 @@ function App2() {
Execution Count:{debouncer.getExecutionCount()}Enabled:{debouncer.getOptions().enabled.toString()}
Is Pending: {debouncer.getIsPending().toString()}
Execution Count:{debouncer.getExecutionCount()}
+
+
Instant Count: {instantCount}
- - + + + + + + + + + diff --git a/examples/react/useDebouncedValue/src/index.tsx b/examples/react/useDebouncedValue/src/index.tsx index f2707b87e..dd11619ea 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 Search: {instantSearch}
- - - - - - - - @@ -50,9 +42,9 @@ 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, { + 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) { @@ -73,14 +65,6 @@ function App2() {
Execution Count:{debouncer.getExecutionCount()}
Is Pending:{debouncer.getIsPending().toString()}
Instant Count: {instantCount}
- - - - - - - - diff --git a/examples/react/useDebouncer/src/index.tsx b/examples/react/useDebouncer/src/index.tsx index 2fb05da1d..6e733e8e3 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) { @@ -75,43 +81,34 @@ function App2() { return (

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/solid/createDebouncedSignal/src/index.tsx b/examples/solid/createDebouncedSignal/src/index.tsx index 12c23dda2..9d033e9a5 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() { + + + @@ -83,6 +89,11 @@ function App2() { + + + diff --git a/examples/solid/createDebouncedValue/src/index.tsx b/examples/solid/createDebouncedValue/src/index.tsx index 6ebc6abb3..a88c5ee2b 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

Execution Count:{setSearchDebouncer.getExecutionCount()}Enabled:{setSearchDebouncer.getOptions().enabled.toString()}
Is Pending: {setSearchDebouncer.getIsPending().toString()}
Execution Count:{setSearchDebouncer.getExecutionCount()}
+
+
Instant Search: {searchText}Execution Count: {debouncer.executionCount()}
+
+
Instant Count: {instantCount()}Execution Count: {debouncer.executionCount()}
+
+
Instant Search: {instantSearch()}
- - - - @@ -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, }) @@ -67,10 +63,6 @@ function App2() {
Execution Count:{debouncer.executionCount()}
Instant Count: {instantCount()}
- - - - diff --git a/examples/solid/createDebouncer/src/index.tsx b/examples/solid/createDebouncer/src/index.tsx index e25fc8543..8f135f88d 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() { + + + @@ -92,6 +98,11 @@ function App2() { + + + diff --git a/examples/solid/debounce/src/index.tsx b/examples/solid/debounce/src/index.tsx index d5aa66df2..cb0f538d5 100644 --- a/examples/solid/debounce/src/index.tsx +++ b/examples/solid/debounce/src/index.tsx @@ -10,6 +10,7 @@ function App1() { // Create debounced setter function - Stable reference required! const debouncedSetCount = debounce(setDebouncedCount, { wait: 500, + // leading: true, // optional, defaults to false }) function increment() { diff --git a/packages/pacer/src/async-debouncer.ts b/packages/pacer/src/async-debouncer.ts index 9f2ff23aa..a19bdedca 100644 --- a/packages/pacer/src/async-debouncer.ts +++ b/packages/pacer/src/async-debouncer.ts @@ -3,10 +3,7 @@ import type { AnyAsyncFunction } from './types' /** * Options for configuring an async debounced function */ -export interface AsyncDebouncerOptions< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, -> { +export interface AsyncDebouncerOptions { /** * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions. * Defaults to true. @@ -24,7 +21,7 @@ export interface AsyncDebouncerOptions< /** * Optional function to call when the debounced function is executed */ - onExecute?: (debouncer: AsyncDebouncer) => void + onExecute?: (debouncer: AsyncDebouncer) => void /** * Whether to execute on the trailing edge of the timeout. * Defaults to true. @@ -37,7 +34,7 @@ export interface AsyncDebouncerOptions< wait: number } -const defaultOptions: Required> = { +const defaultOptions: Required> = { enabled: true, leading: false, trailing: true, @@ -68,21 +65,18 @@ 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 _lastArgs: Parameters | undefined + private _options: Required> private _timeoutId: ReturnType | null = null private _canLeadingExecute = true constructor( private fn: TFn, - initialOptions: AsyncDebouncerOptions, + initialOptions: AsyncDebouncerOptions, ) { this._options = { ...defaultOptions, @@ -95,8 +89,8 @@ export class AsyncDebouncer< * Returns the new options state */ setOptions( - newOptions: Partial>, - ): Required> { + newOptions: Partial>, + ): Required> { this._options = { ...this._options, ...newOptions, @@ -107,7 +101,7 @@ export class AsyncDebouncer< /** * Returns the current debouncer options */ - getOptions(): Required> { + getOptions(): Required> { return this._options } @@ -115,7 +109,7 @@ 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 { + async maybeExecute(...args: Parameters): Promise { this.cancel() this._lastArgs = args @@ -159,7 +153,7 @@ export class AsyncDebouncer< }) } - private async executeFunction(...args: TArgs): Promise { + private async executeFunction(...args: Parameters): Promise { if (!this._options.enabled) return this._executionCount++ await this.fn(...args) @@ -216,10 +210,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/debouncer.ts b/packages/pacer/src/debouncer.ts index f3a57166b..17d34f165 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 _options: Required> private _timeoutId: NodeJS.Timeout | undefined constructor( private fn: TFn, - initialOptions: DebouncerOptions, + initialOptions: DebouncerOptions, ) { this._options = { ...defaultOptions, @@ -86,8 +84,8 @@ export class Debouncer> { * Returns the new options state */ setOptions( - newOptions: Partial>, - ): Required> { + newOptions: Partial>, + ): Required> { this._options = { ...this._options, ...newOptions, @@ -104,7 +102,7 @@ export class Debouncer> { /** * Returns the current debouncer options */ - getOptions(): Required> { + getOptions(): Required> { return this._options } @@ -112,35 +110,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 +193,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/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/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/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/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..932bf8cc8 100644 --- a/packages/react-pacer/src/debouncer/useDebouncedValue.ts +++ b/packages/react-pacer/src/debouncer/useDebouncedValue.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { useDebouncedState } from './useDebouncedState' -import type { Debouncer, DebouncerOptions } from '@tanstack/pacer/debouncer' +import type { DebouncerOptions } from '@tanstack/pacer/debouncer' /** * A React hook that creates a debounced value that updates only after a specified delay. @@ -15,15 +15,13 @@ 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. * * @example * ```tsx * // Debounce a search query * const [searchQuery, setSearchQuery] = useState(''); - * const [debouncedQuery, debouncer] = useDebouncedValue(searchQuery, { + * const debouncedQuery = useDebouncedValue(searchQuery, { * wait: 500 // Wait 500ms after last change * }); * @@ -40,11 +38,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 { const [debouncedValue, setDebouncedValue, debouncer] = useDebouncedState( value, options, @@ -52,10 +47,7 @@ export function useDebouncedValue( useEffect(() => { setDebouncedValue(value) - return () => { - debouncer.cancel() - } }, [value, setDebouncedValue, debouncer]) - return [debouncedValue, debouncer] + return debouncedValue } 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/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts index 6fdec0678..1c8d5cb19 100644 --- a/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts +++ b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts @@ -5,13 +5,8 @@ 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, 'getExecutionCount' | 'getIsPending'> { executionCount: Accessor isPending: Accessor } @@ -51,21 +46,18 @@ 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 [isPending, setIsPending] = createSignal(asyncDebouncer.getIsPending()) - function setOptions(newOptions: Partial>) { + function setOptions(newOptions: Partial>) { asyncDebouncer.setOptions({ ...newOptions, onExecute: (asyncDebouncer) => { 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..93a6a4eca 100644 --- a/packages/solid-pacer/src/debouncer/createDebouncedValue.ts +++ b/packages/solid-pacer/src/debouncer/createDebouncedValue.ts @@ -1,6 +1,5 @@ -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' import type { DebouncerOptions } from '@tanstack/pacer/debouncer' @@ -17,15 +16,13 @@ import type { 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 (as an Accessor) - * - The debouncer instance with control methods and state signals + * The hook returns an Accessor that provides the current debounced value. * * @example * ```tsx * // Debounce a search query * const [searchQuery, setSearchQuery] = createSignal(''); - * const [debouncedQuery, debouncer] = createDebouncedValue(searchQuery, { + * const debouncedQuery = createDebouncedValue(searchQuery, { * wait: 500 // Wait 500ms after last change * }); * @@ -34,10 +31,6 @@ 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); @@ -46,19 +39,16 @@ import type { DebouncerOptions } from '@tanstack/pacer/debouncer' */ export function createDebouncedValue( value: Accessor, - initialOptions: DebouncerOptions, [Accessor]>, -): [Accessor, SolidDebouncer, [Accessor]>] { - const [debouncedValue, setDebouncedValue, debouncer] = createDebouncedSignal( + initialOptions: DebouncerOptions>, +): Accessor { + const [debouncedValue, setDebouncedValue] = createDebouncedSignal( value(), initialOptions, ) createEffect(() => { setDebouncedValue(value() as any) - onCleanup(() => { - debouncer.cancel() - }) }) - return [debouncedValue, debouncer] + return debouncedValue } diff --git a/packages/solid-pacer/src/debouncer/createDebouncer.ts b/packages/solid-pacer/src/debouncer/createDebouncer.ts index b9b9709a7..099cd1823 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 = 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,6 +76,12 @@ export function createDebouncer< setOptions(initialOptions) + createEffect(() => { + onCleanup(() => { + debouncer.cancel() + }) + }) + return { ...bindInstanceMethods(debouncer), executionCount, From c392374131a768d6392409a023abc8ff99d70a98 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Sat, 3 May 2025 23:29:37 -0500 Subject: [PATCH 03/10] fix issues with leading and trailing in async debouncer. Add return values and types to asyncDebouncer --- .changeset/tricky-pants-hear-solid.md | 6 +- .changeset/tricky-pants-hear.md | 5 +- .../functions/createasyncdebouncer.md | 2 +- .../interfaces/solidasyncdebouncer.md | 10 +++ docs/reference/classes/asyncdebouncer.md | 36 ++++++--- docs/reference/functions/asyncdebounce.md | 6 +- .../interfaces/asyncdebounceroptions.md | 42 ++++++++-- examples/react/asyncDebounce/src/index.tsx | 1 + examples/react/asyncRateLimit/src/index.tsx | 1 + examples/react/asyncThrottle/src/index.tsx | 1 + examples/react/debounce/src/index.tsx | 1 + examples/react/queue/src/index.tsx | 1 + examples/react/rateLimit/src/index.tsx | 1 + examples/react/throttle/src/index.tsx | 1 + .../react/useAsyncDebouncer/src/index.tsx | 21 +++-- .../react/useAsyncRateLimiter/src/index.tsx | 1 + .../react/useAsyncThrottler/src/index.tsx | 1 + .../react/useDebouncedCallback/src/index.tsx | 1 + .../react/useDebouncedState/src/index.tsx | 1 + .../react/useDebouncedValue/src/index.tsx | 1 + examples/react/useDebouncer/src/index.tsx | 1 + examples/react/useQueuedValue/src/index.tsx | 1 + .../useRateLimitedCallback/src/index.tsx | 1 + .../react/useRateLimitedState/src/index.tsx | 1 + .../react/useRateLimitedValue/src/index.tsx | 1 + examples/react/useRateLimiter/src/index.tsx | 1 + .../react/useThrottledCallback/src/index.tsx | 1 + .../react/useThrottledState/src/index.tsx | 1 + .../react/useThrottledValue/src/index.tsx | 1 + examples/react/useThrottler/src/index.tsx | 1 + examples/solid/asyncDebounce/src/index.tsx | 1 + examples/solid/asyncRateLimit/src/index.tsx | 1 + examples/solid/asyncThrottle/src/index.tsx | 1 + .../solid/createAsyncDebouncer/src/index.tsx | 14 ++-- .../createAsyncRateLimiter/src/index.tsx | 1 + .../solid/createAsyncThrottler/src/index.tsx | 1 + .../solid/createDebouncedSignal/src/index.tsx | 1 + .../solid/createDebouncedValue/src/index.tsx | 1 + examples/solid/createDebouncer/src/index.tsx | 1 + .../createRateLimitedSignal/src/index.tsx | 1 + .../createRateLimitedValue/src/index.tsx | 1 + .../solid/createRateLimiter/src/index.tsx | 1 + .../solid/createThrottledSignal/src/index.tsx | 1 + .../solid/createThrottledValue/src/index.tsx | 1 + examples/solid/createThrottler/src/index.tsx | 1 + examples/solid/debounce/src/index.tsx | 1 + examples/solid/queue/src/index.tsx | 1 + examples/solid/rateLimit/src/index.tsx | 1 + examples/solid/throttle/src/index.tsx | 1 + packages/pacer/src/async-debouncer.ts | 76 +++++++++++++------ packages/pacer/src/debouncer.ts | 2 +- packages/pacer/tests/async-debouncer.test.ts | 6 +- .../async-debouncer/createAsyncDebouncer.ts | 13 +++- 53 files changed, 203 insertions(+), 76 deletions(-) diff --git a/.changeset/tricky-pants-hear-solid.md b/.changeset/tricky-pants-hear-solid.md index e6411462c..44281c9bb 100644 --- a/.changeset/tricky-pants-hear-solid.md +++ b/.changeset/tricky-pants-hear-solid.md @@ -2,7 +2,7 @@ '@tanstack/solid-pacer': minor --- -- breaking: renamed `useQueuerState` hook to `useQueuedState` -- breaking: changed return signature of `useQueuedState` to include the `addItem` function +- breaking: renamed `createQueuerState` hook to `createQueuedState` +- breaking: changed return signature of `createQueuedState` to include the `addItem` function - breaking: changed return signature of `createDebouncedValue` to return a single value and not a tuple -- feat: add `useQueuedValue` hook +- feat: add `createQueuedValue` hook diff --git a/.changeset/tricky-pants-hear.md b/.changeset/tricky-pants-hear.md index 61f0c38ef..e2d0081d1 100644 --- a/.changeset/tricky-pants-hear.md +++ b/.changeset/tricky-pants-hear.md @@ -3,6 +3,9 @@ --- - feat: add queuer expiration feature to `AsyncQueuer` and `Queuer` +- feat: add return values and types to `AsyncDebouncer` +- feat: standardize `onSuccess`, `onSettled`, and `onError` in `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` +- 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` +- fix: fixed leading and trailing edge behavior of `Debouncer` and `AsyncDebouncer` diff --git a/docs/framework/solid/reference/functions/createasyncdebouncer.md b/docs/framework/solid/reference/functions/createasyncdebouncer.md index cd4ff526a..dd3ff3f39 100644 --- a/docs/framework/solid/reference/functions/createasyncdebouncer.md +++ b/docs/framework/solid/reference/functions/createasyncdebouncer.md @@ -11,7 +11,7 @@ title: createAsyncDebouncer function createAsyncDebouncer(fn, initialOptions): SolidAsyncDebouncer ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:49](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L49) +Defined in: [async-debouncer/createAsyncDebouncer.ts:50](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L50) A low-level Solid hook that creates an `AsyncDebouncer` instance to delay execution of an async function. diff --git a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md index 024349df6..1a67ed9dd 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md +++ b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md @@ -36,3 +36,13 @@ isPending: Accessor; ``` Defined in: [async-debouncer/createAsyncDebouncer.ts:11](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L11) + +*** + +### lastResult + +```ts +lastResult: Accessor>; +``` + +Defined in: [async-debouncer/createAsyncDebouncer.ts:12](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L12) diff --git a/docs/reference/classes/asyncdebouncer.md b/docs/reference/classes/asyncdebouncer.md index 5e8bde273..40120598a 100644 --- a/docs/reference/classes/asyncdebouncer.md +++ b/docs/reference/classes/asyncdebouncer.md @@ -7,7 +7,7 @@ title: AsyncDebouncer # Class: AsyncDebouncer\ -Defined in: [async-debouncer.ts:68](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L68) +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. @@ -43,7 +43,7 @@ inputElement.addEventListener('input', () => { new AsyncDebouncer(fn, initialOptions): AsyncDebouncer ``` -Defined in: [async-debouncer.ts:77](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L77) +Defined in: [async-debouncer.ts:83](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L83) #### Parameters @@ -67,7 +67,7 @@ Defined in: [async-debouncer.ts:77](https://github.com/TanStack/pacer/blob/main/ cancel(): void ``` -Defined in: [async-debouncer.ts:166](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L166) +Defined in: [async-debouncer.ts:186](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L186) Cancels any pending execution @@ -83,7 +83,7 @@ Cancels any pending execution getExecutionCount(): number ``` -Defined in: [async-debouncer.ts:182](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L182) +Defined in: [async-debouncer.ts:208](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L208) Returns the number of times the function has been executed @@ -99,7 +99,7 @@ Returns the number of times the function has been executed getIsPending(): boolean ``` -Defined in: [async-debouncer.ts:189](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L189) +Defined in: [async-debouncer.ts:215](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L215) Returns `true` if there is a pending execution @@ -109,13 +109,29 @@ Returns `true` if there is a pending execution *** +### getLastResult() + +```ts +getLastResult(): undefined | ReturnType +``` + +Defined in: [async-debouncer.ts:201](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L201) + +Returns the last result of the debounced function + +#### Returns + +`undefined` \| `ReturnType`\<`TFn`\> + +*** + ### getOptions() ```ts getOptions(): Required> ``` -Defined in: [async-debouncer.ts:104](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L104) +Defined in: [async-debouncer.ts:110](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L110) Returns the current debouncer options @@ -128,10 +144,10 @@ Returns the current debouncer options ### maybeExecute() ```ts -maybeExecute(...args): Promise +maybeExecute(...args): Promise> ``` -Defined in: [async-debouncer.ts:112](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L112) +Defined in: [async-debouncer.ts:118](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L118) Attempts to execute the debounced function If a call is already in progress, it will be queued @@ -144,7 +160,7 @@ If a call is already in progress, it will be queued #### Returns -`Promise`\<`void`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> *** @@ -154,7 +170,7 @@ If a call is already in progress, it will be queued setOptions(newOptions): Required> ``` -Defined in: [async-debouncer.ts:91](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L91) +Defined in: [async-debouncer.ts:97](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L97) Updates the debouncer options Returns the new options state diff --git a/docs/reference/functions/asyncdebounce.md b/docs/reference/functions/asyncdebounce.md index 82a76ba34..e5ec941ee 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:213](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L213) +Defined in: [async-debouncer.ts:239](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L239) 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. @@ -46,7 +46,7 @@ If a call is already in progress, it will be queued ### Returns -`Promise`\<`void`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> ## Example diff --git a/docs/reference/interfaces/asyncdebounceroptions.md b/docs/reference/interfaces/asyncdebounceroptions.md index f36242226..6b14b52b9 100644 --- a/docs/reference/interfaces/asyncdebounceroptions.md +++ b/docs/reference/interfaces/asyncdebounceroptions.md @@ -46,7 +46,7 @@ Defaults to false. ### onError()? ```ts -optional onError: (error) => void; +optional onError: (error, debouncer) => void; ``` Defined in: [async-debouncer.ts:20](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L20) @@ -59,24 +59,54 @@ 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: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`\> + +#### 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`\> @@ -93,7 +123,7 @@ Optional function to call when the debounced function is executed optional trailing: boolean; ``` -Defined in: [async-debouncer.ts:29](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L29) +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. @@ -106,7 +136,7 @@ Defaults to true. wait: number; ``` -Defined in: [async-debouncer.ts:34](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L34) +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/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/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/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/src/index.tsx b/examples/react/debounce/src/index.tsx index 2fb1cc1b2..03ac882f4 100644 --- a/examples/react/debounce/src/index.tsx +++ b/examples/react/debounce/src/index.tsx @@ -70,6 +70,7 @@ function App2() {

TanStack Pacer debounce Example 2

TanStack Pacer queue Example 2
TanStack Pacer rateLimit Example 2
TanStack Pacer throttle Example 2
{ // optional error handler @@ -58,19 +63,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 +76,7 @@ function App() {

TanStack Pacer useAsyncDebouncer Example

TanStack Pacer useAsyncRateLimiter Example
TanStack Pacer useAsyncThrottler Example
TanStack Pacer useDebouncedCallback Example 2
TanStack Pacer useDebouncedState Example 2
TanStack Pacer useDebouncedValue Example 2
TanStack Pacer useDebouncer Example 2
{ 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
TanStack Pacer useRateLimitedValue Example 2
TanStack Pacer useRateLimiter Example 2
TanStack Pacer useThrottledCallback Example 2
TanStack Pacer useThrottledState Example 2
TanStack Pacer useThrottledValue Example 2
TanStack Pacer useThrottler Example 2
TanStack Pacer asyncDebounce Example
{ diff --git a/examples/solid/asyncRateLimit/src/index.tsx b/examples/solid/asyncRateLimit/src/index.tsx index 4799df63d..3ebddf42d 100644 --- a/examples/solid/asyncRateLimit/src/index.tsx +++ b/examples/solid/asyncRateLimit/src/index.tsx @@ -46,6 +46,7 @@ function SearchApp() {

TanStack Pacer asyncRateLimit Example

{ diff --git a/examples/solid/asyncThrottle/src/index.tsx b/examples/solid/asyncThrottle/src/index.tsx index 942e8e30a..ad7efad00 100644 --- a/examples/solid/asyncThrottle/src/index.tsx +++ b/examples/solid/asyncThrottle/src/index.tsx @@ -41,6 +41,7 @@ function SearchApp() {

TanStack Pacer asyncThrottle Example

{ diff --git a/examples/solid/createAsyncDebouncer/src/index.tsx b/examples/solid/createAsyncDebouncer/src/index.tsx index 5d27d7137..27d668424 100644 --- a/examples/solid/createAsyncDebouncer/src/index.tsx +++ b/examples/solid/createAsyncDebouncer/src/index.tsx @@ -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) => { @@ -42,11 +41,13 @@ function App() { setIsLoading(false) setError(null) - console.log(setSearchAsyncDebouncer.executionCount()) + console.log('execution count: ', setSearchAsyncDebouncer.executionCount()) + return data // this could alternatively be a void function without a return } // 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 +55,6 @@ function App() { setError(error as Error) setResults([]) }, - onExecute: (asyncDebouncer) => { - setExecutionCount(asyncDebouncer.getExecutionCount()) - }, }) // get and name our debounced function @@ -66,7 +64,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) // optionally await result if you need to + console.log('result', result) } return ( @@ -74,6 +73,7 @@ function App() {

TanStack Pacer createAsyncDebouncer Example

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

API calls made: {executionCount()}

+

API calls made: {setSearchAsyncDebouncer.executionCount()}

{results().length > 0 && (
    {results().map((item) => ( diff --git a/examples/solid/createAsyncRateLimiter/src/index.tsx b/examples/solid/createAsyncRateLimiter/src/index.tsx index e16289947..212ba19c5 100644 --- a/examples/solid/createAsyncRateLimiter/src/index.tsx +++ b/examples/solid/createAsyncRateLimiter/src/index.tsx @@ -71,6 +71,7 @@ function App() {

    TanStack Pacer createAsyncRateLimiter Example

    TanStack Pacer createAsyncThrottler Example
    TanStack Pacer createDebouncedSignal Example 2
    TanStack Pacer createDebouncedValue Example 2
    TanStack Pacer createDebouncer Example 2
    TanStack Pacer createRateLimitedState Example 2
    TanStack Pacer createRateLimitedValue Example 2
    TanStack Pacer createRateLimiter Example 2
    TanStack Pacer createThrottledSignal Example 2
    TanStack Pacer createThrottledValue Example 2
    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
    { /** * 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,9 +41,10 @@ export interface AsyncDebouncerOptions { const defaultOptions: Required> = { enabled: true, leading: false, - trailing: true, onError: () => {}, - onExecute: () => {}, + onSettled: () => {}, + onSuccess: () => {}, + trailing: true, wait: 0, } @@ -67,12 +72,13 @@ const defaultOptions: Required> = { */ export class AsyncDebouncer { private _abortController: AbortController | null = null + private _canLeadingExecute = true private _executionCount = 0 private _isExecuting = false private _lastArgs: Parameters | undefined + private _lastResult: ReturnType | undefined private _options: Required> private _timeoutId: ReturnType | null = null - private _canLeadingExecute = true constructor( private fn: TFn, @@ -109,55 +115,69 @@ export class AsyncDebouncer { * Attempts to execute the debounced function * If a call is already in progress, it will be queued */ - async maybeExecute(...args: Parameters): Promise { + async maybeExecute( + ...args: Parameters + ): Promise | undefined> { this.cancel() this._lastArgs = args + let _didLeadingExecute = false + // Handle leading execution if (this._options.leading && this._canLeadingExecute) { this._canLeadingExecute = false - await this.executeFunction(...args) + _didLeadingExecute = true + try { + this._lastResult = await this.executeFunction(...args) + return this._lastResult + } catch (error) { + this._options.onError(error, this) + } finally { + this._isExecuting = false + this._abortController = null + this._options.onSettled(this) + } } return new Promise((resolve) => { this._timeoutId = setTimeout(async () => { if (this._isExecuting) { - resolve() + resolve(this._lastResult) return } - - this._canLeadingExecute = true // Execute trailing only if enabled - if (this._options.trailing) { + if (this._options.trailing && !_didLeadingExecute) { this._abortController = new AbortController() try { this._isExecuting = true if (this._lastArgs) { - await this.executeFunction(...this._lastArgs) + this._lastResult = await this.executeFunction(...this._lastArgs) } } catch (error) { - try { - this._options.onError(error) - } catch { - console.error('Error in error handler', error) - } + this._options.onError(error, this) } finally { this._isExecuting = false this._abortController = null - resolve() + this._options.onSettled(this) + resolve(this._lastResult) } } else { - resolve() + resolve(this._lastResult) } + // Reset _canLeadingExecute after the trailing execution is complete + this._canLeadingExecute = true }, this._options.wait) }) } - private async executeFunction(...args: Parameters): Promise { - if (!this._options.enabled) return + private async executeFunction( + ...args: Parameters + ): Promise | undefined> { + if (!this._options.enabled) return undefined + this._lastResult = await this.fn(...args) // EXECUTE! this._executionCount++ - await this.fn(...args) - this._options.onExecute(this) + this._options.onSuccess(this._lastResult!, this) + return this._lastResult } /** @@ -173,7 +193,13 @@ export class AsyncDebouncer { this._abortController = null } this._lastArgs = undefined - this._canLeadingExecute = true + } + + /** + * Returns the last result of the debounced function + */ + getLastResult(): ReturnType | undefined { + return this._lastResult } /** diff --git a/packages/pacer/src/debouncer.ts b/packages/pacer/src/debouncer.ts index 17d34f165..b47919634 100644 --- a/packages/pacer/src/debouncer.ts +++ b/packages/pacer/src/debouncer.ts @@ -64,8 +64,8 @@ const defaultOptions: Required> = { */ export class Debouncer { private _canLeadingExecute = true - private _isPending = false private _executionCount = 0 + private _isPending = false private _options: Required> private _timeoutId: NodeJS.Timeout | undefined diff --git a/packages/pacer/tests/async-debouncer.test.ts b/packages/pacer/tests/async-debouncer.test.ts index 4b16ca937..0a2d7beb4 100644 --- a/packages/pacer/tests/async-debouncer.test.ts +++ b/packages/pacer/tests/async-debouncer.test.ts @@ -66,15 +66,13 @@ describe('AsyncDebouncer', () => { const promise = debouncer.maybeExecute() vi.advanceTimersByTime(100) await promise - expect(onError).toHaveBeenCalledWith(error) + expect(onError).toHaveBeenCalledWith(error, debouncer) }) 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 onError = vi.fn().mockRejectedValue(new Error('Error handler error')) const debouncer = new AsyncDebouncer(mockFn, { wait: 100, onError }) const promise = debouncer.maybeExecute() diff --git a/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts index 1c8d5cb19..573053835 100644 --- a/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts +++ b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts @@ -9,6 +9,7 @@ export interface SolidAsyncDebouncer extends Omit, 'getExecutionCount' | 'getIsPending'> { executionCount: Accessor isPending: Accessor + lastResult: Accessor | undefined> } /** @@ -56,16 +57,19 @@ export function createAsyncDebouncer( asyncDebouncer.getExecutionCount(), ) const [isPending, setIsPending] = createSignal(asyncDebouncer.getIsPending()) + const [lastResult, setLastResult] = createSignal( + asyncDebouncer.getLastResult(), + ) function setOptions(newOptions: Partial>) { asyncDebouncer.setOptions({ ...newOptions, - onExecute: (asyncDebouncer) => { + onSettled: (asyncDebouncer) => { setExecutionCount(asyncDebouncer.getExecutionCount()) setIsPending(asyncDebouncer.getIsPending()) - - const onExecute = newOptions.onExecute ?? initialOptions.onExecute - onExecute?.(asyncDebouncer) + setLastResult(asyncDebouncer.getLastResult()) + const onSettled = newOptions.onSettled ?? initialOptions.onSettled + onSettled?.(asyncDebouncer) }, }) } @@ -76,6 +80,7 @@ export function createAsyncDebouncer( ...bindInstanceMethods(asyncDebouncer), executionCount, isPending, + lastResult, setOptions, } } From 387995a508511c3b14aa7a2a06cb858407ee65a7 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Sun, 4 May 2025 12:23:00 -0500 Subject: [PATCH 04/10] more tests for async debouncer and fix pending logic --- .changeset/tricky-pants-hear.md | 4 +- .../functions/createasyncdebouncer.md | 2 +- .../interfaces/solidasyncdebouncer.md | 32 +- docs/reference/classes/asyncdebouncer.md | 72 +- docs/reference/functions/asyncdebounce.md | 2 +- .../react/useAsyncDebouncer/src/index.tsx | 7 +- .../solid/createAsyncDebouncer/src/index.tsx | 11 +- packages/pacer/src/async-debouncer.ts | 122 ++-- packages/pacer/tests/async-debouncer.test.ts | 625 +++++++++++++++--- .../async-debouncer/createAsyncDebouncer.ts | 31 +- 10 files changed, 711 insertions(+), 197 deletions(-) diff --git a/.changeset/tricky-pants-hear.md b/.changeset/tricky-pants-hear.md index e2d0081d1..2d615d229 100644 --- a/.changeset/tricky-pants-hear.md +++ b/.changeset/tricky-pants-hear.md @@ -3,9 +3,11 @@ --- - feat: add queuer expiration feature to `AsyncQueuer` and `Queuer` -- feat: add return values and types to `AsyncDebouncer` +- 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` - 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/docs/framework/solid/reference/functions/createasyncdebouncer.md b/docs/framework/solid/reference/functions/createasyncdebouncer.md index dd3ff3f39..e2d0c47b4 100644 --- a/docs/framework/solid/reference/functions/createasyncdebouncer.md +++ b/docs/framework/solid/reference/functions/createasyncdebouncer.md @@ -11,7 +11,7 @@ title: createAsyncDebouncer function createAsyncDebouncer(fn, initialOptions): SolidAsyncDebouncer ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:50](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L50) +Defined in: [async-debouncer/createAsyncDebouncer.ts:55](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L55) A low-level Solid hook that creates an `AsyncDebouncer` instance to delay execution of an async function. diff --git a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md index 1a67ed9dd..4c06e35ef 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md +++ b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md @@ -11,7 +11,7 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:8](https://github.com/TanSt ## Extends -- `Omit`\<`AsyncDebouncer`\<`TFn`\>, `"getExecutionCount"` \| `"getIsPending"`\> +- `Omit`\<`AsyncDebouncer`\<`TFn`\>, `"getErrorCount"` \| `"getSettleCount"` \| `"getSuccessCount"` \| `"getIsPending"`\> ## Type Parameters @@ -19,13 +19,13 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:8](https://github.com/TanSt ## Properties -### executionCount +### errorCount ```ts -executionCount: Accessor; +errorCount: Accessor; ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:10](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L10) +Defined in: [async-debouncer/createAsyncDebouncer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L13) *** @@ -35,7 +35,7 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:10](https://github.com/TanS isPending: Accessor; ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:11](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L11) +Defined in: [async-debouncer/createAsyncDebouncer.ts:14](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L14) *** @@ -45,4 +45,24 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:11](https://github.com/TanS lastResult: Accessor>; ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:12](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L12) +Defined in: [async-debouncer/createAsyncDebouncer.ts:15](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L15) + +*** + +### settleCount + +```ts +settleCount: Accessor; +``` + +Defined in: [async-debouncer/createAsyncDebouncer.ts:16](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L16) + +*** + +### successCount + +```ts +successCount: Accessor; +``` + +Defined in: [async-debouncer/createAsyncDebouncer.ts:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L17) diff --git a/docs/reference/classes/asyncdebouncer.md b/docs/reference/classes/asyncdebouncer.md index 40120598a..894edd961 100644 --- a/docs/reference/classes/asyncdebouncer.md +++ b/docs/reference/classes/asyncdebouncer.md @@ -43,7 +43,7 @@ inputElement.addEventListener('input', () => { 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 @@ -67,7 +67,7 @@ Defined in: [async-debouncer.ts:83](https://github.com/TanStack/pacer/blob/main/ cancel(): void ``` -Defined in: [async-debouncer.ts:186](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L186) +Defined in: [async-debouncer.ts:194](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L194) Cancels any pending execution @@ -77,15 +77,15 @@ Cancels any pending execution *** -### getExecutionCount() +### getErrorCount() ```ts -getExecutionCount(): number +getErrorCount(): number ``` -Defined in: [async-debouncer.ts:208](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L208) +Defined in: [async-debouncer.ts:223](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L223) -Returns the number of times the function has been executed +Returns the number of times the function has errored #### Returns @@ -93,15 +93,31 @@ Returns the number of times the function has been executed *** +### getIsExecuting() + +```ts +getIsExecuting(): boolean +``` + +Defined in: [async-debouncer.ts:237](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L237) + +Returns `true` if there is currently an execution in progress + +#### Returns + +`boolean` + +*** + ### getIsPending() ```ts getIsPending(): boolean ``` -Defined in: [async-debouncer.ts:215](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L215) +Defined in: [async-debouncer.ts:230](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L230) -Returns `true` if there is a pending execution +Returns `true` if there is a pending execution queued up for trailing execution #### Returns @@ -115,7 +131,7 @@ Returns `true` if there is a pending execution getLastResult(): undefined | ReturnType ``` -Defined in: [async-debouncer.ts:201](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L201) +Defined in: [async-debouncer.ts:202](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L202) Returns the last result of the debounced function @@ -131,7 +147,7 @@ Returns the last result of the debounced function 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:113](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L113) Returns the current debouncer options @@ -141,13 +157,45 @@ Returns the current debouncer options *** +### getSettleCount() + +```ts +getSettleCount(): number +``` + +Defined in: [async-debouncer.ts:216](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L216) + +Returns the number of times the function has settled (completed or errored) + +#### Returns + +`number` + +*** + +### getSuccessCount() + +```ts +getSuccessCount(): number +``` + +Defined in: [async-debouncer.ts:209](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L209) + +Returns the number of times the function has been executed successfully + +#### Returns + +`number` + +*** + ### maybeExecute() ```ts 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:121](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L121) Attempts to execute the debounced function If a call is already in progress, it will be queued @@ -170,7 +218,7 @@ If a call is already in progress, it will be queued setOptions(newOptions): Required> ``` -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 diff --git a/docs/reference/functions/asyncdebounce.md b/docs/reference/functions/asyncdebounce.md index e5ec941ee..ca8adc9f5 100644 --- a/docs/reference/functions/asyncdebounce.md +++ b/docs/reference/functions/asyncdebounce.md @@ -11,7 +11,7 @@ title: asyncDebounce function asyncDebounce(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-debouncer.ts:239](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L239) +Defined in: [async-debouncer.ts:259](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L259) 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. diff --git a/examples/react/useAsyncDebouncer/src/index.tsx b/examples/react/useAsyncDebouncer/src/index.tsx index 12801ad15..04322b521 100644 --- a/examples/react/useAsyncDebouncer/src/index.tsx +++ b/examples/react/useAsyncDebouncer/src/index.tsx @@ -41,10 +41,6 @@ function App() { setIsLoading(false) setError(null) - console.log( - 'execution count: ', - setSearchAsyncDebouncer.getExecutionCount(), - ) return data // this could alternatively be a void function without a return } @@ -67,6 +63,7 @@ function App() { async function onSearchChange(e: React.ChangeEvent) { const newTerm = e.target.value setSearchTerm(newTerm) + console.log(setSearchAsyncDebouncer.getIsPending()) const result = await handleSearchDebounced(newTerm) // optionally await result if you need to console.log('result', result) } @@ -87,7 +84,7 @@ function App() {
    {error &&
    Error: {error.message}
    }
    -

    API calls made: {setSearchAsyncDebouncer.getExecutionCount()}

    +

    API calls made: {setSearchAsyncDebouncer.getSuccessCount()}

    {results.length > 0 && (
      {results.map((item) => ( diff --git a/examples/solid/createAsyncDebouncer/src/index.tsx b/examples/solid/createAsyncDebouncer/src/index.tsx index 27d668424..539312b18 100644 --- a/examples/solid/createAsyncDebouncer/src/index.tsx +++ b/examples/solid/createAsyncDebouncer/src/index.tsx @@ -37,12 +37,11 @@ function App() { } const data = await fakeApi(term) - setResults(data) + setResults(data) // option 1: set results immediately setIsLoading(false) setError(null) - console.log('execution count: ', setSearchAsyncDebouncer.executionCount()) - return data // this could alternatively be a void function without a return + return data // option 2: return data if you need to } // hook that gives you an async debouncer instance @@ -64,8 +63,8 @@ function App() { async function onSearchChange(e: Event) { const newTerm = (e.target as HTMLInputElement).value setSearchTerm(newTerm) - const result = await handleSearchDebounced(newTerm) // optionally await result if you need to - console.log('result', result) + const result = await handleSearchDebounced(newTerm) // ^option 2: await results + console.log('result', result) // demo test to see awaited result } return ( @@ -84,7 +83,7 @@ function App() {
    {error() &&
    Error: {error()?.message}
    }
    -

    API calls made: {setSearchAsyncDebouncer.executionCount()}

    +

    API calls made: {setSearchAsyncDebouncer.successCount()}

    {results().length > 0 && (
      {results().map((item) => ( diff --git a/packages/pacer/src/async-debouncer.ts b/packages/pacer/src/async-debouncer.ts index 33fa511b3..eb96bcf39 100644 --- a/packages/pacer/src/async-debouncer.ts +++ b/packages/pacer/src/async-debouncer.ts @@ -73,12 +73,15 @@ const defaultOptions: Required> = { export class AsyncDebouncer { private _abortController: AbortController | null = null private _canLeadingExecute = true - private _executionCount = 0 + private _errorCount = 0 private _isExecuting = false + private _isPending = false private _lastArgs: Parameters | undefined private _lastResult: ReturnType | undefined private _options: Required> - private _timeoutId: ReturnType | null = null + private _settleCount = 0 + private _successCount = 0 + private _timeoutId: NodeJS.Timeout | null = null constructor( private fn: TFn, @@ -118,54 +121,31 @@ export class AsyncDebouncer { async maybeExecute( ...args: Parameters ): Promise | undefined> { - this.cancel() + this.reset() this._lastArgs = args - let _didLeadingExecute = false - // Handle leading execution if (this._options.leading && this._canLeadingExecute) { this._canLeadingExecute = false - _didLeadingExecute = true - try { - this._lastResult = await this.executeFunction(...args) - return this._lastResult - } catch (error) { - this._options.onError(error, this) - } finally { - this._isExecuting = false - this._abortController = null - this._options.onSettled(this) - } + 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(this._lastResult) - return - } - // Execute trailing only if enabled - if (this._options.trailing && !_didLeadingExecute) { - this._abortController = new AbortController() - try { - this._isExecuting = true - if (this._lastArgs) { - this._lastResult = await this.executeFunction(...this._lastArgs) - } - } catch (error) { - this._options.onError(error, this) - } finally { - this._isExecuting = false - this._abortController = null - this._options.onSettled(this) - resolve(this._lastResult) - } - } else { - resolve(this._lastResult) + // Execute trailing if enabled + if (this._options.trailing && this._lastArgs) { + await this.executeFunction(...this._lastArgs) } - // Reset _canLeadingExecute after the trailing execution is complete + + // Reset state and resolve this._canLeadingExecute = true + resolve(this._lastResult) }, this._options.wait) }) } @@ -174,16 +154,29 @@ export class AsyncDebouncer { ...args: Parameters ): Promise | undefined> { if (!this._options.enabled) return undefined - this._lastResult = await this.fn(...args) // EXECUTE! - this._executionCount++ - this._options.onSuccess(this._lastResult!, this) + 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 reset(): void { if (this._timeoutId) { clearTimeout(this._timeoutId) this._timeoutId = null @@ -195,6 +188,14 @@ export class AsyncDebouncer { this._lastArgs = undefined } + /** + * Cancels any pending execution + */ + cancel(): void { + this._canLeadingExecute = true + this.reset() + } + /** * Returns the last result of the debounced function */ @@ -203,19 +204,38 @@ export class AsyncDebouncer { } /** - * Returns the number of times the function has been executed + * Returns the number of times the function has been executed successfully */ - getExecutionCount(): number { - return this._executionCount + getSuccessCount(): number { + return this._successCount } /** - * Returns `true` if there is a pending execution + * 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 `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 } } diff --git a/packages/pacer/tests/async-debouncer.test.ts b/packages/pacer/tests/async-debouncer.test.ts index 0a2d7beb4..9342e37a7 100644 --- a/packages/pacer/tests/async-debouncer.test.ts +++ b/packages/pacer/tests/async-debouncer.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { AsyncDebouncer } from '../src/async-debouncer' describe('AsyncDebouncer', () => { @@ -6,148 +6,557 @@ describe('AsyncDebouncer', () => { 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() + }) - const promise = debouncer.maybeExecute('first') - expect(mockFn).not.toHaveBeenCalled() + 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 }) - vi.advanceTimersByTime(99) - expect(mockFn).not.toHaveBeenCalled() + const promise = debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() - vi.advanceTimersByTime(1) - await promise - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenLastCalledWith('first') - }) + vi.advanceTimersByTime(999) + expect(mockFn).not.toBeCalled() - it('should reset timer on subsequent calls', async () => { - const mockFn = vi.fn().mockResolvedValue(undefined) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + vi.advanceTimersByTime(1) + await promise + expect(mockFn).toBeCalledTimes(1) + }) - debouncer.maybeExecute('first') - vi.advanceTimersByTime(50) + it('should execute the async function after the specified wait', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) - debouncer.maybeExecute('second') - vi.advanceTimersByTime(50) - await Promise.resolve() - expect(mockFn).not.toHaveBeenCalled() + const promise = debouncer.maybeExecute() + expect(mockFn).not.toBeCalled() - vi.advanceTimersByTime(50) - await Promise.resolve() - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenLastCalledWith('second') - }) + vi.advanceTimersByTime(1000) + const result = await promise + expect(mockFn).toBeCalledTimes(1) + expect(result).toBe('result') + }) - it('should track execution count correctly', async () => { - const mockFn = vi.fn().mockResolvedValue(undefined) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + it('should debounce multiple async calls', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) - const promise1 = debouncer.maybeExecute() - expect(debouncer.getExecutionCount()).toBe(0) + // Make multiple calls + const promise1 = debouncer.maybeExecute() + vi.advanceTimersByTime(500) + const promise2 = debouncer.maybeExecute() + vi.advanceTimersByTime(500) + const promise3 = debouncer.maybeExecute() - vi.advanceTimersByTime(100) - await promise1 - expect(debouncer.getExecutionCount()).toBe(1) + // Function should not be called yet + expect(mockFn).not.toBeCalled() - const promise2 = debouncer.maybeExecute() - vi.advanceTimersByTime(100) - await promise2 - expect(debouncer.getExecutionCount()).toBe(2) - }) + // Wait for the full delay + vi.advanceTimersByTime(1000) + await Promise.any([promise1, promise2, promise3]) + + // Should only execute once + expect(mockFn).toBeCalledTimes(1) + }) - 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 }) + 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() - vi.advanceTimersByTime(100) - await promise - expect(onError).toHaveBeenCalledWith(error, debouncer) + 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') + }) }) - it('should ignore errors in onError callback', async () => { - const error = new Error('Test error') - const mockFn = vi.fn().mockRejectedValue(error) - const onError = vi.fn().mockRejectedValue(new Error('Error handler error')) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100, onError }) + 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') + }) - const promise = debouncer.maybeExecute() - vi.advanceTimersByTime(100) - await expect(promise).resolves.not.toThrow() + 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 + + // Next call should execute immediately + const promise5 = debouncer.maybeExecute('fifth') + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toHaveBeenLastCalledWith('fifth') + await promise5 + }) }) - it('should cancel pending execution', async () => { - const mockFn = vi.fn().mockResolvedValue(undefined) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + describe('Promise Handling', () => { + it('should properly handle promise resolution', async () => { + const mockFn = vi.fn().mockResolvedValue('resolved value') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) - debouncer.maybeExecute('test') - vi.advanceTimersByTime(50) - debouncer.cancel() + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + const result = await promise - vi.advanceTimersByTime(50) - await Promise.resolve() - expect(mockFn).not.toHaveBeenCalled() + 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) + }) + + 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 }) + + debouncer.maybeExecute('test') + debouncer.cancel() + vi.advanceTimersByTime(1100) + expect(mockFn).toBeCalledTimes(0) + }) }) - it('should allow new executions after cancel', 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) + }) + + 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) + }) + + 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, + }) + + // 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) + }) - debouncer.maybeExecute('first') - vi.advanceTimersByTime(50) - debouncer.cancel() + 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) + }) - debouncer.maybeExecute('second') - vi.advanceTimersByTime(100) - await Promise.resolve() + 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) + }) - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenCalledWith('second') + 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 handle multiple rapid calls', async () => { - const mockFn = vi.fn().mockResolvedValue(undefined) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + describe('Callback Execution', () => { + it('should call onSuccess after successful execution') + it('should call onSettled after execution completes') + it('should call onError when execution fails') + it('should maintain correct callback order') + it('should handle callback errors gracefully') + }) - debouncer.maybeExecute('first') - vi.advanceTimersByTime(20) - debouncer.maybeExecute('second') - vi.advanceTimersByTime(20) - debouncer.maybeExecute('third') - vi.advanceTimersByTime(20) - debouncer.maybeExecute('fourth') + describe('Execution Control', () => { + it('should cancel pending execution') + it('should properly handle canLeadingExecute flag after cancellation') + it('should abort in-progress execution when cancelled') + it('should handle rapid calls with cancellation') + it('should handle cancellation during leading execution') + it('should handle cancellation during trailing execution') + }) - vi.advanceTimersByTime(100) - await Promise.resolve() + describe('Result Management', () => { + it('should track last result') + it('should return last result when execution is pending') + it('should clear last result on cancellation') + it('should maintain last result across multiple executions') + it('should handle undefined/null results') + }) - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenCalledWith('fourth') + describe('Enabled/Disabled State', () => { + it('should not execute when enabled is false') + it('should not execute leading edge when disabled') + it('should default to enabled') + it('should allow enabling/disabling after construction') + it('should allow disabling mid-wait') }) - it('should handle long-running functions', async () => { - let resolveFirst: (value: unknown) => void - const firstCall = new Promise((resolve) => { - resolveFirst = resolve - }) + describe('Options Management', () => { + it('should allow updating multiple options at once') + it('should handle option changes during execution') + it('should maintain state across option changes') + }) - const mockFn = vi.fn().mockImplementation(() => firstCall) - const debouncer = new AsyncDebouncer(mockFn, { wait: 100 }) + describe('State Tracking', () => { + it('should track execution count') + it('should track execution count with leading and trailing') + it('should not increment count when execution is cancelled') + it('should track pending state correctly') + it('should track execution state correctly') + }) - const promise = debouncer.maybeExecute('test') - vi.advanceTimersByTime(100) + describe('Edge Cases and Error Handling', () => { + it('should handle wait time of 0') + it('should handle negative wait time by using 0') + it('should handle very large wait times') + it('should handle NaN wait time by using 0') + it('should handle undefined/null arguments') + it('should prevent memory leaks by clearing timeouts') + it('should handle rapid option changes') + it('should handle rapid enable/disable cycles') + }) +}) - expect(mockFn).toHaveBeenCalledTimes(1) - resolveFirst!({}) - await promise +describe('asyncDebounce helper function', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('Basic Functionality', () => { + it('should create a debounced async function with default options') + it('should pass arguments correctly') + it('should return a promise') + }) - // Subsequent call should work - debouncer.maybeExecute('next') - vi.advanceTimersByTime(100) - await Promise.resolve() - expect(mockFn).toHaveBeenCalledTimes(2) + describe('Execution Options', () => { + it('should respect leading option') + it('should handle multiple calls with trailing edge') + it('should support both leading and trailing execution') }) }) diff --git a/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts index 573053835..874e0b4dd 100644 --- a/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts +++ b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts @@ -6,10 +6,19 @@ import type { AnyAsyncFunction } from '@tanstack/pacer/types' import type { Accessor } from 'solid-js' export interface SolidAsyncDebouncer - extends Omit, 'getExecutionCount' | 'getIsPending'> { - executionCount: Accessor + extends Omit< + AsyncDebouncer, + | 'getErrorCount' + | 'getIsPending' + | 'getLastResult' + | 'getSettleCount' + | 'getSuccessCount' + > { + errorCount: Accessor isPending: Accessor lastResult: Accessor | undefined> + settleCount: Accessor + successCount: Accessor } /** @@ -53,8 +62,14 @@ export function createAsyncDebouncer( ): 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( @@ -65,7 +80,9 @@ export function createAsyncDebouncer( asyncDebouncer.setOptions({ ...newOptions, onSettled: (asyncDebouncer) => { - setExecutionCount(asyncDebouncer.getExecutionCount()) + setSuccessCount(asyncDebouncer.getSuccessCount()) + setErrorCount(asyncDebouncer.getErrorCount()) + setSettleCount(asyncDebouncer.getSettleCount()) setIsPending(asyncDebouncer.getIsPending()) setLastResult(asyncDebouncer.getLastResult()) const onSettled = newOptions.onSettled ?? initialOptions.onSettled @@ -78,9 +95,11 @@ export function createAsyncDebouncer( return { ...bindInstanceMethods(asyncDebouncer), - executionCount, + errorCount, isPending, lastResult, + settleCount, + successCount, setOptions, } } From b757fce092da5ce7fc6be4a28c886d83d89f7279 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Sun, 4 May 2025 13:13:11 -0500 Subject: [PATCH 05/10] more async debouncer tests --- .../functions/createasyncdebouncer.md | 2 +- .../interfaces/solidasyncdebouncer.md | 17 +- docs/reference/classes/asyncdebouncer.md | 14 +- docs/reference/functions/asyncdebounce.md | 2 +- .../react/useAsyncDebouncer/src/index.tsx | 1 - packages/pacer/src/async-debouncer.ts | 8 +- packages/pacer/tests/async-debouncer.test.ts | 665 ++++++++++++++++-- 7 files changed, 642 insertions(+), 67 deletions(-) diff --git a/docs/framework/solid/reference/functions/createasyncdebouncer.md b/docs/framework/solid/reference/functions/createasyncdebouncer.md index e2d0c47b4..fa0bf5b72 100644 --- a/docs/framework/solid/reference/functions/createasyncdebouncer.md +++ b/docs/framework/solid/reference/functions/createasyncdebouncer.md @@ -11,7 +11,7 @@ title: createAsyncDebouncer function createAsyncDebouncer(fn, initialOptions): SolidAsyncDebouncer ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:55](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L55) +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. diff --git a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md index 4c06e35ef..ee5169ed4 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md +++ b/docs/framework/solid/reference/interfaces/solidasyncdebouncer.md @@ -11,7 +11,12 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:8](https://github.com/TanSt ## Extends -- `Omit`\<`AsyncDebouncer`\<`TFn`\>, `"getErrorCount"` \| `"getSettleCount"` \| `"getSuccessCount"` \| `"getIsPending"`\> +- `Omit`\<`AsyncDebouncer`\<`TFn`\>, + \| `"getErrorCount"` + \| `"getIsPending"` + \| `"getLastResult"` + \| `"getSettleCount"` + \| `"getSuccessCount"`\> ## Type Parameters @@ -25,7 +30,7 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:8](https://github.com/TanSt errorCount: Accessor; ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:13](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L13) +Defined in: [async-debouncer/createAsyncDebouncer.ts:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L17) *** @@ -35,7 +40,7 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:13](https://github.com/TanS isPending: Accessor; ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:14](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L14) +Defined in: [async-debouncer/createAsyncDebouncer.ts:18](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L18) *** @@ -45,7 +50,7 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:14](https://github.com/TanS lastResult: 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:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L19) *** @@ -55,7 +60,7 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:15](https://github.com/TanS settleCount: 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:20](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L20) *** @@ -65,4 +70,4 @@ Defined in: [async-debouncer/createAsyncDebouncer.ts:16](https://github.com/TanS successCount: Accessor; ``` -Defined in: [async-debouncer/createAsyncDebouncer.ts:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts#L17) +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/reference/classes/asyncdebouncer.md b/docs/reference/classes/asyncdebouncer.md index 894edd961..e39522337 100644 --- a/docs/reference/classes/asyncdebouncer.md +++ b/docs/reference/classes/asyncdebouncer.md @@ -67,7 +67,7 @@ Defined in: [async-debouncer.ts:86](https://github.com/TanStack/pacer/blob/main/ cancel(): void ``` -Defined in: [async-debouncer.ts:194](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L194) +Defined in: [async-debouncer.ts:196](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L196) Cancels any pending execution @@ -83,7 +83,7 @@ Cancels any pending execution getErrorCount(): number ``` -Defined in: [async-debouncer.ts:223](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L223) +Defined in: [async-debouncer.ts:225](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L225) Returns the number of times the function has errored @@ -99,7 +99,7 @@ Returns the number of times the function has errored getIsExecuting(): boolean ``` -Defined in: [async-debouncer.ts:237](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L237) +Defined in: [async-debouncer.ts:239](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L239) Returns `true` if there is currently an execution in progress @@ -115,7 +115,7 @@ Returns `true` if there is currently an execution in progress getIsPending(): boolean ``` -Defined in: [async-debouncer.ts:230](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L230) +Defined in: [async-debouncer.ts:232](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L232) Returns `true` if there is a pending execution queued up for trailing execution @@ -131,7 +131,7 @@ Returns `true` if there is a pending execution queued up for trailing execution getLastResult(): undefined | ReturnType ``` -Defined in: [async-debouncer.ts:202](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L202) +Defined in: [async-debouncer.ts:204](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L204) Returns the last result of the debounced function @@ -163,7 +163,7 @@ Returns the current debouncer options getSettleCount(): number ``` -Defined in: [async-debouncer.ts:216](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L216) +Defined in: [async-debouncer.ts:218](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L218) Returns the number of times the function has settled (completed or errored) @@ -179,7 +179,7 @@ Returns the number of times the function has settled (completed or errored) getSuccessCount(): number ``` -Defined in: [async-debouncer.ts:209](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L209) +Defined in: [async-debouncer.ts:211](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L211) Returns the number of times the function has been executed successfully diff --git a/docs/reference/functions/asyncdebounce.md b/docs/reference/functions/asyncdebounce.md index ca8adc9f5..de4f41b82 100644 --- a/docs/reference/functions/asyncdebounce.md +++ b/docs/reference/functions/asyncdebounce.md @@ -11,7 +11,7 @@ title: asyncDebounce function asyncDebounce(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-debouncer.ts:259](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L259) +Defined in: [async-debouncer.ts:261](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L261) 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. diff --git a/examples/react/useAsyncDebouncer/src/index.tsx b/examples/react/useAsyncDebouncer/src/index.tsx index 04322b521..8942276b4 100644 --- a/examples/react/useAsyncDebouncer/src/index.tsx +++ b/examples/react/useAsyncDebouncer/src/index.tsx @@ -63,7 +63,6 @@ function App() { async function onSearchChange(e: React.ChangeEvent) { const newTerm = e.target.value setSearchTerm(newTerm) - console.log(setSearchAsyncDebouncer.getIsPending()) const result = await handleSearchDebounced(newTerm) // optionally await result if you need to console.log('result', result) } diff --git a/packages/pacer/src/async-debouncer.ts b/packages/pacer/src/async-debouncer.ts index eb96bcf39..9f217eed0 100644 --- a/packages/pacer/src/async-debouncer.ts +++ b/packages/pacer/src/async-debouncer.ts @@ -121,7 +121,7 @@ export class AsyncDebouncer { async maybeExecute( ...args: Parameters ): Promise | undefined> { - this.reset() + this._cancel() this._lastArgs = args // Handle leading execution @@ -176,7 +176,7 @@ export class AsyncDebouncer { /** * Cancel without resetting _canLeadingExecute */ - private reset(): void { + private _cancel(): void { if (this._timeoutId) { clearTimeout(this._timeoutId) this._timeoutId = null @@ -186,6 +186,8 @@ export class AsyncDebouncer { this._abortController = null } this._lastArgs = undefined + this._isPending = false + this._isExecuting = false } /** @@ -193,7 +195,7 @@ export class AsyncDebouncer { */ cancel(): void { this._canLeadingExecute = true - this.reset() + this._cancel() } /** diff --git a/packages/pacer/tests/async-debouncer.test.ts b/packages/pacer/tests/async-debouncer.test.ts index 9342e37a7..bb9d20356 100644 --- a/packages/pacer/tests/async-debouncer.test.ts +++ b/packages/pacer/tests/async-debouncer.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { AsyncDebouncer } from '../src/async-debouncer' +import { AsyncDebouncer, asyncDebounce } from '../src/async-debouncer' describe('AsyncDebouncer', () => { beforeEach(() => { @@ -481,61 +481,558 @@ describe('AsyncDebouncer', () => { }) describe('Callback Execution', () => { - it('should call onSuccess after successful execution') - it('should call onSettled after execution completes') - it('should call onError when execution fails') - it('should maintain correct callback order') - it('should handle callback errors gracefully') + 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 debouncer = new AsyncDebouncer(mockFn, { + wait: 1000, + onSuccess, + onSettled, + onError, + }) + + const promise = debouncer.maybeExecute() + vi.advanceTimersByTime(1000) + await promise + + // 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) + }) }) describe('Execution Control', () => { - it('should cancel pending execution') - it('should properly handle canLeadingExecute flag after cancellation') - it('should abort in-progress execution when cancelled') - it('should handle rapid calls with cancellation') - it('should handle cancellation during leading execution') - it('should handle cancellation during trailing execution') + 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, + }) + + // 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]) + }) }) describe('Result Management', () => { - it('should track last result') - it('should return last result when execution is pending') - it('should clear last result on cancellation') - it('should maintain last result across multiple executions') - it('should handle undefined/null results') + 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 + }) + + 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 + }) }) describe('Enabled/Disabled State', () => { - it('should not execute when enabled is false') - it('should not execute leading edge when disabled') - it('should default to enabled') - it('should allow enabling/disabling after construction') - it('should allow disabling mid-wait') + 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) + + // 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 }) + + // 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') + }) }) describe('Options Management', () => { - it('should allow updating multiple options at once') - it('should handle option changes during execution') - it('should maintain state across option changes') - }) + it('should allow updating multiple options at once', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debouncer = new AsyncDebouncer(mockFn, { wait: 1000 }) - describe('State Tracking', () => { - it('should track execution count') - it('should track execution count with leading and trailing') - it('should not increment count when execution is cancelled') - it('should track pending state correctly') - it('should track execution state correctly') - }) + // 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() - describe('Edge Cases and Error Handling', () => { - it('should handle wait time of 0') - it('should handle negative wait time by using 0') - it('should handle very large wait times') - it('should handle NaN wait time by using 0') - it('should handle undefined/null arguments') - it('should prevent memory leaks by clearing timeouts') - it('should handle rapid option changes') - it('should handle rapid enable/disable cycles') + // 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) + + // Change options + debouncer.setOptions({ wait: 500, leading: true }) + + // Verify state is maintained + expect(debouncer.getLastResult()).toBe('result') + expect(debouncer.getSuccessCount()).toBe(1) + + // Second execution with new options + const promise2 = debouncer.maybeExecute() + expect(mockFn).toBeCalledTimes(2) // Leading execution + vi.advanceTimersByTime(500) + await promise2 + expect(debouncer.getSuccessCount()).toBe(2) + }) + + 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() + }) }) }) @@ -549,14 +1046,86 @@ describe('asyncDebounce helper function', () => { }) describe('Basic Functionality', () => { - it('should create a debounced async function with default options') - it('should pass arguments correctly') - it('should return a promise') + it('should create a debounced async function with default options', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const debounced = asyncDebounce(mockFn, { wait: 1000 }) + + const promise = debounced() + expect(mockFn).not.toBeCalled() + + vi.advanceTimersByTime(1000) + const result = await promise + expect(mockFn).toBeCalledTimes(1) + expect(result).toBe('result') + }) + + 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) + }) }) describe('Execution Options', () => { - it('should respect leading option') - it('should handle multiple calls with trailing edge') - it('should support both leading and trailing execution') + 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) + }) + + 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, + }) + + // First call - should execute immediately + const promise1 = debounced('first') + expect(mockFn).toBeCalledTimes(1) + expect(mockFn).toBeCalledWith('first') + + // Second call - should queue for trailing + const promise2 = debounced('second') + expect(mockFn).toBeCalledTimes(1) + + vi.advanceTimersByTime(1000) + await Promise.all([promise1, promise2]) + expect(mockFn).toBeCalledTimes(2) + expect(mockFn).toBeCalledWith('second') + }) }) }) From ca096fec7788555a6b0ae28e838c252864766b98 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Sun, 4 May 2025 20:12:50 -0500 Subject: [PATCH 06/10] fix types and trailing/leading issues in throttlers and rate limiters --- .../functions/useasyncratelimiter.md | 8 +- .../reference/functions/useasyncthrottler.md | 8 +- .../functions/useratelimitedcallback.md | 2 +- .../functions/useratelimitedstate.md | 6 +- .../functions/useratelimitedvalue.md | 24 ++---- .../reference/functions/useratelimiter.md | 8 +- .../functions/usethrottledcallback.md | 2 +- .../reference/functions/usethrottledstate.md | 6 +- .../reference/functions/usethrottledvalue.md | 20 ++--- .../react/reference/functions/usethrottler.md | 8 +- .../reference/functions/createasyncqueuer.md | 2 +- .../functions/createasyncratelimiter.md | 10 +-- .../functions/createasyncthrottler.md | 10 +-- .../solid/reference/functions/createqueuer.md | 2 +- .../functions/createratelimitedsignal.md | 6 +- .../functions/createratelimitedvalue.md | 32 ++------ .../reference/functions/createratelimiter.md | 10 +-- .../functions/createthrottledsignal.md | 6 +- .../functions/createthrottledvalue.md | 28 +++---- .../reference/functions/createthrottler.md | 10 +-- .../reference/interfaces/solidasyncqueuer.md | 16 +++- .../interfaces/solidasyncratelimiter.md | 14 ++-- .../interfaces/solidasyncthrottler.md | 14 ++-- .../solid/reference/interfaces/solidqueuer.md | 14 +++- .../reference/interfaces/solidratelimiter.md | 14 ++-- .../reference/interfaces/solidthrottler.md | 14 ++-- docs/reference/classes/asyncdebouncer.md | 22 +++--- docs/reference/classes/asyncqueuer.md | 48 ++++++------ docs/reference/classes/asyncratelimiter.md | 42 +++++------ docs/reference/classes/asyncthrottler.md | 42 +++++------ docs/reference/classes/debouncer.md | 14 ++-- docs/reference/classes/queuer.md | 38 +++++----- docs/reference/classes/ratelimiter.md | 42 +++++------ docs/reference/classes/throttler.md | 42 +++++------ docs/reference/functions/asyncdebounce.md | 2 +- docs/reference/functions/asyncqueue.md | 2 +- docs/reference/functions/asyncratelimit.md | 10 +-- docs/reference/functions/asyncthrottle.md | 10 +-- .../functions/bindinstancemethods.md | 4 +- docs/reference/functions/debounce.md | 2 +- docs/reference/functions/queue.md | 2 +- docs/reference/functions/ratelimit.md | 6 +- docs/reference/functions/throttle.md | 10 +-- .../interfaces/asyncratelimiteroptions.md | 20 +++-- .../interfaces/asyncthrottleroptions.md | 14 ++-- .../interfaces/ratelimiteroptions.md | 18 ++--- docs/reference/interfaces/throttleroptions.md | 16 ++-- .../react-query-queued-prefetch/src/index.tsx | 2 +- .../src/index.tsx | 2 +- .../react/useRateLimitedValue/src/index.tsx | 30 +------- .../react/useThrottledValue/src/index.tsx | 12 +-- examples/react/useThrottler/src/index.tsx | 4 +- .../createRateLimitedValue/src/index.tsx | 30 +------- .../solid/createThrottledValue/src/index.tsx | 12 +-- packages/pacer/src/async-debouncer.ts | 13 ++-- packages/pacer/src/async-queuer.ts | 5 +- packages/pacer/src/async-rate-limiter.ts | 45 ++++-------- packages/pacer/src/async-throttler.ts | 44 ++++------- packages/pacer/src/debouncer.ts | 11 +-- packages/pacer/src/queuer.ts | 5 +- packages/pacer/src/rate-limiter.ts | 40 ++++------ packages/pacer/src/throttler.ts | 73 ++++++++----------- packages/pacer/src/utils.ts | 2 +- .../async-rate-limiter/useAsyncRateLimiter.ts | 11 +-- .../src/async-throttler/useAsyncThrottler.ts | 17 +++-- .../rate-limiter/useRateLimitedCallback.ts | 4 +- .../src/rate-limiter/useRateLimitedState.ts | 7 +- .../src/rate-limiter/useRateLimitedValue.ts | 39 +++------- .../src/rate-limiter/useRateLimiter.ts | 10 +-- .../src/throttler/useThrottledCallback.ts | 4 +- .../src/throttler/useThrottledState.ts | 7 +- .../src/throttler/useThrottledValue.ts | 33 +++------ .../react-pacer/src/throttler/useThrottler.ts | 18 +++-- .../async-debouncer/createAsyncDebouncer.ts | 2 +- .../src/async-queuer/createAsyncQueuer.ts | 8 +- .../createAsyncRateLimiter.ts | 25 +++---- .../async-throttler/createAsyncThrottler.ts | 25 +++---- .../src/debouncer/createDebouncer.ts | 6 +- .../solid-pacer/src/queuer/createQueuer.ts | 10 ++- .../rate-limiter/createRateLimitedSignal.ts | 8 +- .../rate-limiter/createRateLimitedValue.ts | 37 +++------- .../src/rate-limiter/createRateLimiter.ts | 29 ++++---- .../src/throttler/createThrottledSignal.ts | 8 +- .../src/throttler/createThrottledValue.ts | 32 +++----- .../src/throttler/createThrottler.ts | 33 +++++---- 85 files changed, 567 insertions(+), 836 deletions(-) diff --git a/docs/framework/react/reference/functions/useasyncratelimiter.md b/docs/framework/react/reference/functions/useasyncratelimiter.md index 9880ce233..7be183836 100644 --- a/docs/framework/react/reference/functions/useasyncratelimiter.md +++ b/docs/framework/react/reference/functions/useasyncratelimiter.md @@ -8,7 +8,7 @@ title: useAsyncRateLimiter # Function: useAsyncRateLimiter() ```ts -function useAsyncRateLimiter(fn, options): AsyncRateLimiter +function useAsyncRateLimiter(fn, options): AsyncRateLimiter ``` 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) @@ -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 612d55fee..6ca0998b7 100644 --- a/docs/framework/react/reference/functions/useasyncthrottler.md +++ b/docs/framework/react/reference/functions/useasyncthrottler.md @@ -8,7 +8,7 @@ title: useAsyncThrottler # Function: useAsyncThrottler() ```ts -function useAsyncThrottler(fn, options): AsyncThrottler +function useAsyncThrottler(fn, options): AsyncThrottler ``` 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) @@ -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/useratelimitedcallback.md b/docs/framework/react/reference/functions/useratelimitedcallback.md index 9f7cebb31..32f179b97 100644 --- a/docs/framework/react/reference/functions/useratelimitedcallback.md +++ b/docs/framework/react/reference/functions/useratelimitedcallback.md @@ -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 2c39d0959..4686d112a 100644 --- a/docs/framework/react/reference/functions/useratelimitedstate.md +++ b/docs/framework/react/reference/functions/useratelimitedstate.md @@ -8,7 +8,7 @@ title: useRateLimitedState # Function: useRateLimitedState() ```ts -function useRateLimitedState(value, options): [TValue, Dispatch>, RateLimiter>, [TValue]>] +function useRateLimitedState(value, options): [TValue, Dispatch>, RateLimiter>>] ``` 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) @@ -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 127592357..c6ebd5fd7 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 ``` -Defined in: [react-pacer/src/rate-limiter/useRateLimitedValue.ts:55](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts#L55) +Defined in: [react-pacer/src/rate-limiter/useRateLimitedValue.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts#L42) 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,7 @@ 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 the rate-limited value that updates according to the configured rate limit. For more direct control over rate limiting behavior without React state management, consider using the lower-level useRateLimiter hook instead. @@ -43,37 +43,27 @@ 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` ## Example ```tsx // Basic rate limiting - update at most 5 times per minute -const [rateLimitedValue] = useRateLimitedValue(rawValue, { +const rateLimitedValue = useRateLimitedValue(rawValue, { limit: 5, window: 60000 }); // With rejection callback -const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { +const rateLimitedValue = useRateLimitedValue(rawValue, { limit: 3, window: 5000, onReject: (rateLimiter) => { 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 768f2ea23..59953e338 100644 --- a/docs/framework/react/reference/functions/useratelimiter.md +++ b/docs/framework/react/reference/functions/useratelimiter.md @@ -8,7 +8,7 @@ title: useRateLimiter # Function: useRateLimiter() ```ts -function useRateLimiter(fn, options): RateLimiter +function useRateLimiter(fn, options): RateLimiter ``` 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) @@ -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 6ed48651e..429ede263 100644 --- a/docs/framework/react/reference/functions/usethrottledcallback.md +++ b/docs/framework/react/reference/functions/usethrottledcallback.md @@ -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 6ed11f830..1e4f7e845 100644 --- a/docs/framework/react/reference/functions/usethrottledstate.md +++ b/docs/framework/react/reference/functions/usethrottledstate.md @@ -8,7 +8,7 @@ title: useThrottledState # Function: useThrottledState() ```ts -function useThrottledState(value, options): [TValue, Dispatch>, Throttler>, [TValue]>] +function useThrottledState(value, options): [TValue, Dispatch>, Throttler>>] ``` Defined in: [react-pacer/src/throttler/useThrottledState.ts:40](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledState.ts#L40) @@ -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 1396b457b..8456383d7 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 ``` -Defined in: [react-pacer/src/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:30](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledValue.ts#L30) 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,7 @@ 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 the throttled value that updates according to the leading/trailing edge behavior specified in the options. For more direct control over throttling behavior without React state management, consider using the lower-level useThrottler hook instead. @@ -37,27 +36,22 @@ 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` ## Example ```tsx // Basic throttling - update at most once per second -const [throttledValue] = useThrottledValue(rawValue, { wait: 1000 }); +const throttledValue = useThrottledValue(rawValue, { wait: 1000 }); // With custom leading/trailing behavior -const [throttledValue, throttler] = useThrottledValue(rawValue, { +const throttledValue = useThrottledValue(rawValue, { wait: 1000, 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 70df358ee..24ba9e71e 100644 --- a/docs/framework/react/reference/functions/usethrottler.md +++ b/docs/framework/react/reference/functions/usethrottler.md @@ -8,7 +8,7 @@ title: useThrottler # Function: useThrottler() ```ts -function useThrottler(fn, options): Throttler +function useThrottler(fn, options): Throttler ``` Defined in: [react-pacer/src/throttler/useThrottler.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottler.ts#L42) @@ -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/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..ddf232b03 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:58](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L58) 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..6b30c79b1 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:59](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L59) 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/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..ba13c790c 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 ``` -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:37](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts#L37) 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,7 @@ 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 an accessor function that provides the rate-limited value. For more direct control over rate limiting behavior without Solid state management, consider using the lower-level createRateLimiter hook instead. @@ -43,37 +43,21 @@ 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`\> ## Example ```tsx // Basic rate limiting - update at most 5 times per minute -const [rateLimitedValue] = createRateLimitedValue(rawValue, { +const rateLimitedValue = 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`); - } -}); - -// 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'); - } -}; +// Use the rate-limited value +console.log(rateLimitedValue()); // Access the current rate-limited value ``` 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..6097f541f 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 ``` -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:28](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottledValue.ts#L28) 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,7 @@ 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 an accessor function that provides the throttled value. 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 +37,18 @@ 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`\> ## 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 = createThrottledValue(rawValue, { wait: 1000 }); + +// Use the throttled value +console.log(throttledValue()); // Access the current throttled value ``` 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/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..5d5c04df2 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md +++ b/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md @@ -5,13 +5,13 @@ 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`\>, +- `Omit`\<`AsyncRateLimiter`\<`TFn`\>, \| `"getExecutionCount"` \| `"getRejectionCount"` \| `"getRemainingInWindow"` @@ -21,8 +21,6 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:8](https://github.com/ • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### executionCount @@ -31,7 +29,7 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:8](https://github.com/ executionCount: 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:16](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L16) *** @@ -41,7 +39,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:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L19) *** @@ -51,7 +49,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:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L17) *** @@ -61,4 +59,4 @@ 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: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..d1fe3abae 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncthrottler.md +++ b/docs/framework/solid/reference/interfaces/solidasyncthrottler.md @@ -5,13 +5,13 @@ 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`\>, +- `Omit`\<`AsyncThrottler`\<`TFn`\>, \| `"getExecutionCount"` \| `"getIsPending"` \| `"getLastExecutionTime"` @@ -21,8 +21,6 @@ Defined in: [async-throttler/createAsyncThrottler.ts:8](https://github.com/TanSt • **TFn** *extends* `AnyAsyncFunction` -• **TArgs** *extends* `Parameters`\<`TFn`\> - ## Properties ### executionCount @@ -31,7 +29,7 @@ Defined in: [async-throttler/createAsyncThrottler.ts:8](https://github.com/TanSt executionCount: 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:16](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L16) *** @@ -41,7 +39,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:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L17) *** @@ -51,7 +49,7 @@ 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:18](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L18) *** @@ -61,4 +59,4 @@ Defined in: [async-throttler/createAsyncThrottler.ts:20](https://github.com/TanS nextExecutionTime: Accessor; ``` -Defined in: [async-throttler/createAsyncThrottler.ts:21](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L21) +Defined in: [async-throttler/createAsyncThrottler.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L19) 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/reference/classes/asyncdebouncer.md b/docs/reference/classes/asyncdebouncer.md index e39522337..c9afc84ec 100644 --- a/docs/reference/classes/asyncdebouncer.md +++ b/docs/reference/classes/asyncdebouncer.md @@ -67,7 +67,7 @@ Defined in: [async-debouncer.ts:86](https://github.com/TanStack/pacer/blob/main/ cancel(): void ``` -Defined in: [async-debouncer.ts:196](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L196) +Defined in: [async-debouncer.ts:195](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L195) Cancels any pending execution @@ -83,7 +83,7 @@ Cancels any pending execution getErrorCount(): number ``` -Defined in: [async-debouncer.ts:225](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L225) +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 errored @@ -99,7 +99,7 @@ Returns the number of times the function has errored getIsExecuting(): boolean ``` -Defined in: [async-debouncer.ts:239](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L239) +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 @@ -115,7 +115,7 @@ Returns `true` if there is currently an execution in progress getIsPending(): boolean ``` -Defined in: [async-debouncer.ts:232](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L232) +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 queued up for trailing execution @@ -131,7 +131,7 @@ Returns `true` if there is a pending execution queued up for trailing execution getLastResult(): undefined | ReturnType ``` -Defined in: [async-debouncer.ts:204](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L204) +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 @@ -147,7 +147,7 @@ Returns the last result of the debounced function getOptions(): Required> ``` -Defined in: [async-debouncer.ts:113](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L113) +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 @@ -163,7 +163,7 @@ Returns the current debouncer options getSettleCount(): number ``` -Defined in: [async-debouncer.ts:218](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L218) +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) @@ -179,7 +179,7 @@ Returns the number of times the function has settled (completed or errored) getSuccessCount(): number ``` -Defined in: [async-debouncer.ts:211](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L211) +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 @@ -195,7 +195,7 @@ Returns the number of times the function has been executed successfully maybeExecute(...args): Promise> ``` -Defined in: [async-debouncer.ts:121](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L121) +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 @@ -215,7 +215,7 @@ If a call is already in progress, it will be queued ### setOptions() ```ts -setOptions(newOptions): Required> +setOptions(newOptions): void ``` Defined in: [async-debouncer.ts:100](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L100) @@ -231,4 +231,4 @@ Returns the new options state #### Returns -`Required`\<[`AsyncDebouncerOptions`](../interfaces/asyncdebounceroptions.md)\<`TFn`\>\> +`void` diff --git a/docs/reference/classes/asyncqueuer.md b/docs/reference/classes/asyncqueuer.md index 2796b96a7..2cc91d632 100644 --- a/docs/reference/classes/asyncqueuer.md +++ b/docs/reference/classes/asyncqueuer.md @@ -74,7 +74,7 @@ addItem( runOnUpdate): Promise ``` -Defined in: [async-queuer.ts:327](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L327) +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 @@ -104,7 +104,7 @@ Adds a task to the queuer clear(): void ``` -Defined in: [async-queuer.ts:307](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L307) +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 @@ -120,7 +120,7 @@ Removes all items from the queuer getActiveItems(): () => Promise[] ``` -Defined in: [async-queuer.ts:465](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L465) +Defined in: [async-queuer.ts:462](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L462) Returns the active items @@ -136,7 +136,7 @@ Returns the active items getAllItems(): () => Promise[] ``` -Defined in: [async-queuer.ts:458](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L458) +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 @@ -152,7 +152,7 @@ Returns a copy of all items in the queuer getExecutionCount(): number ``` -Defined in: [async-queuer.ts:479](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L479) +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 @@ -168,7 +168,7 @@ Returns the number of items that have been removed from the queuer getExpirationCount(): number ``` -Defined in: [async-queuer.ts:541](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L541) +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 @@ -184,7 +184,7 @@ Returns the number of items that have expired from the queuer getIsEmpty(): boolean ``` -Defined in: [async-queuer.ts:437](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L437) +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 @@ -200,7 +200,7 @@ Returns true if the queuer is empty getIsFull(): boolean ``` -Defined in: [async-queuer.ts:444](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L444) +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 @@ -216,7 +216,7 @@ Returns true if the queuer is full getIsIdle(): boolean ``` -Defined in: [async-queuer.ts:500](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L500) +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 @@ -232,7 +232,7 @@ Returns true if the queuer is running but has no items to process getIsRunning(): boolean ``` -Defined in: [async-queuer.ts:493](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L493) +Defined in: [async-queuer.ts:490](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L490) Returns true if the queuer is running @@ -248,7 +248,7 @@ Returns true if the queuer is running getNextItem(position): undefined | () => Promise ``` -Defined in: [async-queuer.ts:401](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L401) +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 @@ -270,7 +270,7 @@ Removes and returns an item from the queuer getOptions(): Required> ``` -Defined in: [async-queuer.ts:162](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L162) +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 @@ -286,7 +286,7 @@ Returns the current queuer options getPeek(position): undefined | () => Promise ``` -Defined in: [async-queuer.ts:425](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L425) +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 @@ -308,7 +308,7 @@ Returns an item without removing it getPendingItems(): () => Promise[] ``` -Defined in: [async-queuer.ts:472](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L472) +Defined in: [async-queuer.ts:469](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L469) Returns the pending items @@ -324,7 +324,7 @@ Returns the pending items getRejectionCount(): number ``` -Defined in: [async-queuer.ts:486](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L486) +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 @@ -340,7 +340,7 @@ Returns the number of items that have been rejected from the queuer getSize(): number ``` -Defined in: [async-queuer.ts:451](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L451) +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 @@ -356,7 +356,7 @@ Returns the current size of the queuer onError(cb): () => void ``` -Defined in: [async-queuer.ts:519](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L519) +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 @@ -382,7 +382,7 @@ Adds a callback to be called when a task errors onSettled(cb): () => void ``` -Defined in: [async-queuer.ts:529](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L529) +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 @@ -408,7 +408,7 @@ Adds a callback to be called when a task is settled onSuccess(cb): () => void ``` -Defined in: [async-queuer.ts:507](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L507) +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 @@ -434,7 +434,7 @@ Adds a callback to be called when a task succeeds reset(withInitialItems?): void ``` -Defined in: [async-queuer.ts:315](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L315) +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 @@ -453,7 +453,7 @@ Resets the queuer to its initial state ### setOptions() ```ts -setOptions(newOptions): AsyncQueuerOptions +setOptions(newOptions): void ``` Defined in: [async-queuer.ts:152](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L152) @@ -469,7 +469,7 @@ Returns the new options state #### Returns -[`AsyncQueuerOptions`](../interfaces/asyncqueueroptions.md)\<`TValue`\> +`void` *** @@ -479,7 +479,7 @@ Returns the new options state start(): Promise ``` -Defined in: [async-queuer.ts:275](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L275) +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 @@ -495,7 +495,7 @@ Starts the queuer and processes items stop(): void ``` -Defined in: [async-queuer.ts:298](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L298) +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..a3b5b728f 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:68](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L68) 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:74](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L74) #### Parameters @@ -58,11 +56,11 @@ 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 @@ -72,7 +70,7 @@ Defined in: [async-rate-limiter.ts:80](https://github.com/TanStack/pacer/blob/ma getExecutionCount(): 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:167](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L167) 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: [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:189](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L189) 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:95](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L95) 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:174](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L174) 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:181](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L181) 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): 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:115](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L115) 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,7 +160,7 @@ If execution is allowed, waits for any previous execution to complete before pro ##### args -...`TArgs` +...`Parameters`\<`TFn`\> #### Returns @@ -188,7 +186,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:196](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L196) Resets the rate limiter state @@ -201,10 +199,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:88](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L88) Updates the rate limiter options Returns the new options state @@ -213,8 +211,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..20405a77a 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:56](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L56) 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:66](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L66) #### 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,7 +67,7 @@ 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:157](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L157) Cancels any pending execution @@ -85,7 +83,7 @@ Cancels any pending execution getExecutionCount(): 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:169](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L169) Returns the number of times the function has been executed @@ -101,7 +99,7 @@ Returns the number of times the function has been executed 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:190](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L190) Returns the current pending state @@ -117,7 +115,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:176](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L176) Returns the last execution time @@ -133,7 +131,7 @@ Returns the last execution time 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:183](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L183) Returns the next execution time @@ -146,16 +144,16 @@ 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:87](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L87) Returns the current options #### Returns -`Required`\<[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`, `TArgs`\>\> +`Required`\<[`AsyncThrottlerOptions`](../interfaces/asyncthrottleroptions.md)\<`TFn`\>\> *** @@ -165,7 +163,7 @@ Returns the current options 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:95](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L95) 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,7 +172,7 @@ If a call is already in progress, it may be blocked or queued depending on the ` ##### args -...`TArgs` +...`Parameters`\<`TFn`\> #### Returns @@ -185,10 +183,10 @@ If a call is already in progress, it may be blocked or queued depending on the ` ### 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:80](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L80) Updates the throttler options Returns the new options state @@ -197,8 +195,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 e522cb08a..8cc48a0d3 100644 --- a/docs/reference/classes/debouncer.md +++ b/docs/reference/classes/debouncer.md @@ -68,7 +68,7 @@ Defined in: [debouncer.ts:72](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 @@ -84,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 @@ -100,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 @@ -116,7 +116,7 @@ Returns `true` if debouncing getOptions(): Required> ``` -Defined in: [debouncer.ts:105](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L105) +Defined in: [debouncer.ts:98](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L98) Returns the current debouncer options @@ -132,7 +132,7 @@ Returns the current debouncer options maybeExecute(...args): void ``` -Defined in: [debouncer.ts:113](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L113) +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 @@ -152,7 +152,7 @@ If a call is already in progress, it will be queued ### setOptions() ```ts -setOptions(newOptions): Required> +setOptions(newOptions): void ``` Defined in: [debouncer.ts:86](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/debouncer.ts#L86) @@ -168,4 +168,4 @@ Returns the new options state #### Returns -`Required`\<[`DebouncerOptions`](../interfaces/debounceroptions.md)\<`TFn`\>\> +`void` diff --git a/docs/reference/classes/queuer.md b/docs/reference/classes/queuer.md index 97af00d06..ca0c02307 100644 --- a/docs/reference/classes/queuer.md +++ b/docs/reference/classes/queuer.md @@ -98,7 +98,7 @@ addItem( runOnUpdate): boolean ``` -Defined in: [queuer.ts:309](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L309) +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 @@ -130,7 +130,7 @@ true if item was added, false if queuer is full clear(): void ``` -Defined in: [queuer.ts:288](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L288) +Defined in: [queuer.ts:285](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L285) Removes all items from the queuer @@ -146,7 +146,7 @@ Removes all items from the queuer getAllItems(): TValue[] ``` -Defined in: [queuer.ts:431](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L431) +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 @@ -162,7 +162,7 @@ Returns a copy of all items in the queuer getExecutionCount(): number ``` -Defined in: [queuer.ts:438](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L438) +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 @@ -178,7 +178,7 @@ Returns the number of items that have been removed from the queuer getExpirationCount(): number ``` -Defined in: [queuer.ts:452](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L452) +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 @@ -194,7 +194,7 @@ Returns the number of items that have expired from the queuer getIsEmpty(): boolean ``` -Defined in: [queuer.ts:410](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L410) +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 @@ -210,7 +210,7 @@ Returns true if the queuer is empty getIsFull(): boolean ``` -Defined in: [queuer.ts:417](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L417) +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 @@ -226,7 +226,7 @@ Returns true if the queuer is full getIsIdle(): boolean ``` -Defined in: [queuer.ts:466](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L466) +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 @@ -242,7 +242,7 @@ Returns true if the queuer is running but has no items to process getIsRunning(): boolean ``` -Defined in: [queuer.ts:459](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L459) +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 @@ -258,7 +258,7 @@ Returns true if the queuer is running getNextItem(position): undefined | TValue ``` -Defined in: [queuer.ts:366](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L366) +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 @@ -289,7 +289,7 @@ queuer.getNextItem('back') getOptions(): Required> ``` -Defined in: [queuer.ts:180](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L180) +Defined in: [queuer.ts:177](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L177) Returns the current queuer options @@ -305,7 +305,7 @@ Returns the current queuer options getPeek(position): undefined | TValue ``` -Defined in: [queuer.ts:398](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L398) +Defined in: [queuer.ts:395](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L395) Returns an item without removing it @@ -336,7 +336,7 @@ queuer.getPeek('back') getRejectionCount(): number ``` -Defined in: [queuer.ts:445](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L445) +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 @@ -352,7 +352,7 @@ Returns the number of items that have been rejected from the queuer getSize(): number ``` -Defined in: [queuer.ts:424](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L424) +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 @@ -368,7 +368,7 @@ Returns the current size of the queuer reset(withInitialItems?): void ``` -Defined in: [queuer.ts:296](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L296) +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 @@ -387,7 +387,7 @@ Resets the queuer to its initial state ### setOptions() ```ts -setOptions(newOptions): QueuerOptions +setOptions(newOptions): void ``` Defined in: [queuer.ts:170](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L170) @@ -403,7 +403,7 @@ Returns the new options state #### Returns -[`QueuerOptions`](../interfaces/queueroptions.md)\<`TValue`\> +`void` *** @@ -413,7 +413,7 @@ Returns the new options state start(): void ``` -Defined in: [queuer.ts:276](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L276) +Defined in: [queuer.ts:273](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L273) Starts the queuer and processes items @@ -429,7 +429,7 @@ Starts the queuer and processes items stop(): void ``` -Defined in: [queuer.ts:267](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L267) +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..db99e3a98 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:166](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L166) 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:177](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L177) 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:184](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L184) 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:191](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L191) 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:198](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L198) 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:95](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L95) 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:121](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L121) 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 de4f41b82..9c42ae372 100644 --- a/docs/reference/functions/asyncdebounce.md +++ b/docs/reference/functions/asyncdebounce.md @@ -11,7 +11,7 @@ title: asyncDebounce function asyncDebounce(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-debouncer.ts:261](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L261) +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. diff --git a/docs/reference/functions/asyncqueue.md b/docs/reference/functions/asyncqueue.md index 5f49c694c..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:563](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-queuer.ts#L563) +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. diff --git a/docs/reference/functions/asyncratelimit.md b/docs/reference/functions/asyncratelimit.md index e976f9d74..b157e5bcd 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:233](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L233) 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,7 +49,7 @@ If execution is allowed, waits for any previous execution to complete before pro #### args -...`TArgs` +...`Parameters`\<`TFn`\> ### Returns diff --git a/docs/reference/functions/asyncthrottle.md b/docs/reference/functions/asyncthrottle.md index faa76312c..81d1f1e75 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:211](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L211) 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,7 +42,7 @@ If a call is already in progress, it may be blocked or queued depending on the ` #### args -...`TArgs` +...`Parameters`\<`TFn`\> ### Returns 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 896473e4c..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. diff --git a/docs/reference/functions/queue.md b/docs/reference/functions/queue.md index e91637860..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:499](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L499) +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..3cc6a1acf 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:229](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L229) 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/asyncratelimiteroptions.md b/docs/reference/interfaces/asyncratelimiteroptions.md index 247683cdb..8cf19fd3a 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 @@ -50,7 +48,7 @@ Maximum number of executions allowed within the time window optional onError: (error) => 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 @@ -72,7 +70,7 @@ Optional error handler for when the rate-limited function throws optional onExecute: (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 +78,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 @@ -94,7 +92,7 @@ Optional function to call when the rate-limited function is executed optional onReject: (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 @@ -102,7 +100,7 @@ Optional callback function that is called when an execution is rejected due to r ##### rateLimiter -[`AsyncRateLimiter`](../classes/asyncratelimiter.md)\<`TFn`, `TArgs`\> +[`AsyncRateLimiter`](../classes/asyncratelimiter.md)\<`TFn`\> #### Returns @@ -116,6 +114,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:31](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L31) 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..0d22a4035 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,7 +23,7 @@ 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. @@ -38,7 +36,7 @@ Defaults to true. optional onError: (error) => 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:15](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L15) Optional error handler for when the throttled function throws @@ -60,7 +58,7 @@ Optional error handler for when the throttled function throws optional onExecute: (throttler) => 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:19](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L19) Optional function to call when the throttled function is executed @@ -68,7 +66,7 @@ Optional function to call when the throttled function is executed ##### throttler -[`AsyncThrottler`](../classes/asyncthrottler.md)\<`TFn`, `TArgs`\> +[`AsyncThrottler`](../classes/asyncthrottler.md)\<`TFn`\> #### Returns @@ -82,7 +80,7 @@ Optional function to call when the throttled function is executed 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:24](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L24) Time window in milliseconds during which the function can only be executed once Defaults to 0ms 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/examples/react/react-query-queued-prefetch/src/index.tsx b/examples/react/react-query-queued-prefetch/src/index.tsx index f4e220aff..36fea82da 100644 --- a/examples/react/react-query-queued-prefetch/src/index.tsx +++ b/examples/react/react-query-queued-prefetch/src/index.tsx @@ -56,7 +56,7 @@ function PostList({ // queue up the hovered post id to be processed in order queryClient.prefetchQuery({ queryKey: ['post', queuedHoveredPostId], - queryFn: () => fetchPost(queuedHoveredPostId as number), + queryFn: () => fetchPost(queuedHoveredPostId), }) } }, [queuedHoveredPostId]) diff --git a/examples/react/react-query-throttled-prefetch/src/index.tsx b/examples/react/react-query-throttled-prefetch/src/index.tsx index 1ae9a5b67..17ae49ad6 100644 --- a/examples/react/react-query-throttled-prefetch/src/index.tsx +++ b/examples/react/react-query-throttled-prefetch/src/index.tsx @@ -41,7 +41,7 @@ function PostList({ >(null) // throttle the hovered post id to avoid excessive prefetches - const [throttledHoveredPostId] = useThrottledValue(currentHoveredPostId, { + const throttledHoveredPostId = useThrottledValue(currentHoveredPostId, { wait: 100, // adjust this value to see the difference }) diff --git a/examples/react/useRateLimitedValue/src/index.tsx b/examples/react/useRateLimitedValue/src/index.tsx index d3aa51d11..b6e589b0b 100644 --- a/examples/react/useRateLimitedValue/src/index.tsx +++ b/examples/react/useRateLimitedValue/src/index.tsx @@ -6,7 +6,7 @@ function App1() { const [instantCount, setInstantCount] = useState(0) // Using useRateLimitedValue with a rate limit of 5 executions per 5 seconds - const [limitedCount, rateLimiter] = useRateLimitedValue(instantCount, { + const limitedCount = useRateLimitedValue(instantCount, { // enabled: instantCount > 2, // optional, defaults to true limit: 5, window: 5000, @@ -26,14 +26,6 @@ function App1() {

      TanStack Pacer useRateLimitedValue Example 1

Execution Count:{debouncer.executionCount()}
Instant Search: {instantSearch()}Execution Count: {setCountDebouncer.executionCount()}
+
+
Instant Count: {instantCount()}Execution Count: {setSearchDebouncer.executionCount()}
+
+
Instant Search: {searchText()}
- - - - - - - - @@ -46,10 +38,6 @@ function App1() {
Execution Count:{rateLimiter.getExecutionCount()}
Rejection Count:{rateLimiter.getRejectionCount()}
Instant Count: {instantCount}
- -
) @@ -59,7 +47,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, @@ -89,14 +77,6 @@ function App2() {
- - - - - - - - @@ -107,12 +87,6 @@ function App2() {
Execution Count:{rateLimiter.getExecutionCount()}
Rejection Count:{rateLimiter.getRejectionCount()}
Instant Search: {instantSearch}
-
- - -
) } diff --git a/examples/react/useThrottledValue/src/index.tsx b/examples/react/useThrottledValue/src/index.tsx index 67acc013c..892722f53 100644 --- a/examples/react/useThrottledValue/src/index.tsx +++ b/examples/react/useThrottledValue/src/index.tsx @@ -11,7 +11,7 @@ function App1() { // highest-level hook that watches an instant local state value and returns a throttled value // optionally, grab the throttler from the last index of the returned array - const [throttledCount, throttler] = useThrottledValue(instantCount, { + const throttledCount = useThrottledValue(instantCount, { wait: 1000, // enabled: instantCount > 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 }) @@ -70,10 +66,6 @@ function App2() {
Execution Count:{throttler.getExecutionCount()}
Instant Count: {instantCount}
- - - - diff --git a/examples/react/useThrottler/src/index.tsx b/examples/react/useThrottler/src/index.tsx index d8d4a0d1c..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() { diff --git a/examples/solid/createRateLimitedValue/src/index.tsx b/examples/solid/createRateLimitedValue/src/index.tsx index 9fe8016c9..332a129c4 100644 --- a/examples/solid/createRateLimitedValue/src/index.tsx +++ b/examples/solid/createRateLimitedValue/src/index.tsx @@ -6,7 +6,7 @@ function App1() { const [instantCount, setInstantCount] = createSignal(0) // Using createRateLimitedValue with a rate limit of 5 executions per 5 seconds - const [limitedCount, rateLimiter] = createRateLimitedValue(instantCount, { + const limitedCount = createRateLimitedValue(instantCount, { limit: 5, window: 5000, }) @@ -20,14 +20,6 @@ function App1() {

TanStack Pacer createRateLimitedValue Example 1

Execution Count:{throttler.getExecutionCount()}
Instant Search: {instantSearch}
- - - - - - - - @@ -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, }) @@ -77,14 +65,6 @@ function App2() { - - - - - - - - @@ -95,12 +75,6 @@ function App2() {
Execution Count:{rateLimiter.executionCount()}
Rejection Count:{rateLimiter.rejectionCount()}
Instant Search: {instantSearch()}
-
- - -
) } diff --git a/examples/solid/createThrottledValue/src/index.tsx b/examples/solid/createThrottledValue/src/index.tsx index 7d2a9af19..7f80c2eb7 100644 --- a/examples/solid/createThrottledValue/src/index.tsx +++ b/examples/solid/createThrottledValue/src/index.tsx @@ -11,7 +11,7 @@ function App1() { // highest-level hook that watches an instant local state value and returns a throttled value // optionally, grab the throttler from the last index of the returned array - const [throttledCount, throttler] = createThrottledValue(instantCount, { + const throttledCount = createThrottledValue(instantCount, { wait: 1000, }) @@ -20,10 +20,6 @@ function App1() {

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, }) @@ -68,10 +64,6 @@ function App2() {
Execution Count:{throttler.executionCount()}
Instant Count: {instantCount()}
- - - - diff --git a/packages/pacer/src/async-debouncer.ts b/packages/pacer/src/async-debouncer.ts index 9f217eed0..94b6c9a00 100644 --- a/packages/pacer/src/async-debouncer.ts +++ b/packages/pacer/src/async-debouncer.ts @@ -97,14 +97,13 @@ 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 } /** diff --git a/packages/pacer/src/async-queuer.ts b/packages/pacer/src/async-queuer.ts index 53a24a796..643b12a1d 100644 --- a/packages/pacer/src/async-queuer.ts +++ b/packages/pacer/src/async-queuer.ts @@ -149,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 } /** diff --git a/packages/pacer/src/async-rate-limiter.ts b/packages/pacer/src/async-rate-limiter.ts index f9445680f..e92d5dab2 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. @@ -23,11 +20,11 @@ export interface AsyncRateLimiterOptions< /** * Optional function to call when the rate-limited function is executed */ - onExecute?: (rateLimiter: AsyncRateLimiter) => void + onExecute?: (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,7 +32,7 @@ export interface AsyncRateLimiterOptions< } const defaultOptions: Required< - Omit, 'limit' | 'window'> + Omit, 'limit' | 'window'> > = { enabled: true, onReject: () => {}, @@ -68,18 +65,15 @@ const defaultOptions: Required< * await rateLimiter.maybeExecute('123'); * ``` */ -export class AsyncRateLimiter< - TFn extends AnyAsyncFunction, - TArgs extends Parameters, -> { +export class AsyncRateLimiter { private _executionCount = 0 private _executionTimes: Array = [] - private _options: AsyncRateLimiterOptions + private _options: AsyncRateLimiterOptions private _rejectionCount = 0 constructor( private fn: TFn, - initialOptions: AsyncRateLimiterOptions, + initialOptions: AsyncRateLimiterOptions, ) { this._options = { ...defaultOptions, @@ -91,21 +85,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,7 +112,7 @@ export class AsyncRateLimiter< * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected * ``` */ - async maybeExecute(...args: TArgs): Promise { + async maybeExecute(...args: Parameters): Promise { this.cleanupOldExecutions() if (this._executionTimes.length < this._options.limit) { @@ -136,7 +124,7 @@ export class AsyncRateLimiter< return false } - private async executeFunction(...args: TArgs): Promise { + private async executeFunction(...args: Parameters): Promise { if (!this._options.enabled) return const now = Date.now() this._executionCount++ @@ -242,12 +230,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..4bfeea053 100644 --- a/packages/pacer/src/async-throttler.ts +++ b/packages/pacer/src/async-throttler.ts @@ -3,10 +3,7 @@ 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. @@ -19,7 +16,7 @@ export interface AsyncThrottlerOptions< /** * Optional function to call when the throttled function is executed */ - onExecute?: (throttler: AsyncThrottler) => void + onExecute?: (throttler: AsyncThrottler) => void /** * Time window in milliseconds during which the function can only be executed once * Defaults to 0ms @@ -27,7 +24,7 @@ export interface AsyncThrottlerOptions< wait: number } -const defaultOptions: Required> = { +const defaultOptions: Required> = { enabled: true, onError: () => {}, onExecute: () => {}, @@ -56,22 +53,19 @@ 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 _isExecuting = false private _isPending = false - private _lastArgs: TArgs | undefined + private _lastArgs: Parameters | undefined private _lastExecutionTime = 0 private _nextExecutionTime = 0 constructor( private fn: TFn, - initialOptions: AsyncThrottlerOptions, + initialOptions: AsyncThrottlerOptions, ) { this._options = { ...defaultOptions, @@ -83,20 +77,14 @@ export class AsyncThrottler< * Updates the throttler options * Returns the new options state */ - setOptions( - newOptions: Partial>, - ): Required> { - this._options = { - ...this._options, - ...newOptions, - } - return this._options + setOptions(newOptions: Partial>): void { + this._options = { ...this._options, ...newOptions } } /** * Returns the current options */ - getOptions(): Required> { + getOptions(): Required> { return this._options } @@ -104,7 +92,7 @@ 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 { + async maybeExecute(...args: Parameters): Promise { this._lastArgs = args if (this._isPending) return this._isPending = true @@ -156,7 +144,7 @@ export class AsyncThrottler< }) } - private async executeFunction(...args: TArgs): Promise { + private async executeFunction(...args: Parameters): Promise { if (!this._options.enabled) return this._executionCount++ await this.fn(...args) @@ -220,10 +208,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 b47919634..158dd74d5 100644 --- a/packages/pacer/src/debouncer.ts +++ b/packages/pacer/src/debouncer.ts @@ -83,20 +83,13 @@ 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 } /** diff --git a/packages/pacer/src/queuer.ts b/packages/pacer/src/queuer.ts index ee3c7f833..aa3424764 100644 --- a/packages/pacer/src/queuer.ts +++ b/packages/pacer/src/queuer.ts @@ -167,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 } /** 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..9b7462486 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,14 @@ export class Throttler> { * Updates the throttler options * Returns the new options state */ - setOptions( - newOptions: Partial>, - ): Required> { - this._options = { - ...this._options, - ...newOptions, - } - return this._options + setOptions(newOptions: Partial>): void { + this._options = { ...this._options, ...newOptions } } /** * Returns the current throttler options */ - getOptions(): Required> { + getOptions(): Required> { return this._options } @@ -128,42 +118,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,7 +168,6 @@ export class Throttler> { clearTimeout(this._timeoutId) this._timeoutId = undefined this._lastArgs = undefined - this._isPending = false } } @@ -195,7 +182,7 @@ export class Throttler> { * Returns `true` if there is a pending execution */ getIsPending(): boolean { - return this._options.enabled && this._isPending + return this._options.enabled && !!this._timeoutId } /** @@ -239,10 +226,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/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/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/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..09f4c79dd 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts @@ -1,9 +1,6 @@ import { useEffect } from 'react' import { useRateLimitedState } from './useRateLimitedState' -import type { - RateLimiter, - RateLimiterOptions, -} from '@tanstack/pacer/rate-limiter' +import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' /** * 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. @@ -19,7 +16,7 @@ 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 the rate-limited value that updates according to the configured rate limit. * * For more direct control over rate limiting behavior without React state management, * consider using the lower-level useRateLimiter hook instead. @@ -27,47 +24,33 @@ import type { * @example * ```tsx * // Basic rate limiting - update at most 5 times per minute - * const [rateLimitedValue] = useRateLimitedValue(rawValue, { + * const rateLimitedValue = useRateLimitedValue(rawValue, { * limit: 5, * window: 60000 * }); * * // With rejection callback - * const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { + * const rateLimitedValue = useRateLimitedValue(rawValue, { * limit: 3, * window: 5000, * onReject: (rateLimiter) => { * 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]>, -] { - const [rateLimitedValue, setRateLimitedValue, rateLimiter] = - useRateLimitedState(value, options) + options: RateLimiterOptions>>, +): TValue { + const [rateLimitedValue, setRateLimitedValue] = useRateLimitedState( + value, + options, + ) useEffect(() => { setRateLimitedValue(value) }, [value, setRateLimitedValue]) - return [rateLimitedValue, rateLimiter] + return rateLimitedValue } 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..6dcae9b39 100644 --- a/packages/react-pacer/src/throttler/useThrottledValue.ts +++ b/packages/react-pacer/src/throttler/useThrottledValue.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { useThrottledState } from './useThrottledState' -import type { Throttler, ThrottlerOptions } from '@tanstack/pacer/throttler' +import type { ThrottlerOptions } from '@tanstack/pacer/throttler' /** * A high-level React hook that creates a throttled version of a value that updates at most once within a specified time window. @@ -9,8 +9,7 @@ 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 the throttled value that updates according to the leading/trailing edge behavior specified in the options. * * For more direct control over throttling behavior without React state management, * consider using the lower-level useThrottler hook instead. @@ -18,39 +17,25 @@ 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 = useThrottledValue(rawValue, { wait: 1000 }); * * // With custom leading/trailing behavior - * const [throttledValue, throttler] = useThrottledValue(rawValue, { + * const throttledValue = useThrottledValue(rawValue, { * wait: 1000, * 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]>] { - const [throttledValue, setThrottledValue, throttler] = useThrottledState( - value, - options, - ) + options: ThrottlerOptions>>, +): TValue { + const [throttledValue, setThrottledValue] = useThrottledState(value, options) useEffect(() => { setThrottledValue(value) - return () => { - throttler.cancel() - } - }, [value, setThrottledValue, throttler]) + }, [value, setThrottledValue]) - return [throttledValue, throttler] + return throttledValue } 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 874e0b4dd..5ff229d5a 100644 --- a/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts +++ b/packages/solid-pacer/src/async-debouncer/createAsyncDebouncer.ts @@ -101,5 +101,5 @@ export function createAsyncDebouncer( 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..b45c5c655 100644 --- a/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts +++ b/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts @@ -5,11 +5,9 @@ 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, +export interface SolidAsyncRateLimiter + extends Omit< + AsyncRateLimiter, | 'getExecutionCount' | 'getRejectionCount' | 'getRemainingInWindow' @@ -57,14 +55,11 @@ 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(), @@ -79,9 +74,7 @@ export function createAsyncRateLimiter< asyncRateLimiter.getMsUntilNextWindow(), ) - function setOptions( - newOptions: Partial>, - ) { + function setOptions(newOptions: Partial>) { asyncRateLimiter.setOptions({ ...newOptions, onExecute: (rateLimiter) => { @@ -112,5 +105,5 @@ export function createAsyncRateLimiter< remainingInWindow, msUntilNextWindow, setOptions, - } + } as SolidAsyncRateLimiter } diff --git a/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts b/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts index 92d89f11c..d32f806da 100644 --- a/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts +++ b/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts @@ -5,11 +5,9 @@ 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, +export interface SolidAsyncThrottler + extends Omit< + AsyncThrottler, | 'getExecutionCount' | 'getIsPending' | 'getLastExecutionTime' @@ -58,14 +56,13 @@ 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(), @@ -78,7 +75,7 @@ export function createAsyncThrottler< asyncThrottler.getNextExecutionTime(), ) - function setOptions(newOptions: Partial>) { + function setOptions(newOptions: Partial>) { asyncThrottler.setOptions({ ...newOptions, onExecute: (throttler) => { @@ -102,5 +99,5 @@ export function createAsyncThrottler< lastExecutionTime, nextExecutionTime, setOptions, - } + } as SolidAsyncThrottler } diff --git a/packages/solid-pacer/src/debouncer/createDebouncer.ts b/packages/solid-pacer/src/debouncer/createDebouncer.ts index 099cd1823..6ad07ffe2 100644 --- a/packages/solid-pacer/src/debouncer/createDebouncer.ts +++ b/packages/solid-pacer/src/debouncer/createDebouncer.ts @@ -54,7 +54,7 @@ export function createDebouncer( fn: TFn, initialOptions: DebouncerOptions, ): SolidDebouncer { - const debouncer = new Debouncer(fn, initialOptions) + const debouncer = bindInstanceMethods(new Debouncer(fn, initialOptions)) const [executionCount, setExecutionCount] = createSignal( debouncer.getExecutionCount(), @@ -83,9 +83,9 @@ export function createDebouncer( }) 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..baba717c5 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts @@ -1,6 +1,5 @@ import { createEffect } from 'solid-js' import { createRateLimitedSignal } from './createRateLimitedSignal' -import type { SolidRateLimiter } from './createRateLimiter' import type { Accessor, Setter } from 'solid-js' import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' @@ -18,7 +17,7 @@ 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 an accessor function that provides the rate-limited value. * * For more direct control over rate limiting behavior without Solid state management, * consider using the lower-level createRateLimiter hook instead. @@ -26,41 +25,27 @@ 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 = 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`); - * } - * }); - * - * // 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'); - * } - * }; + * // Use the rate-limited value + * console.log(rateLimitedValue()); // Access the current rate-limited value * ``` */ export function createRateLimitedValue( value: Accessor, - initialOptions: RateLimiterOptions, [Accessor]>, -): [Accessor, SolidRateLimiter, [Accessor]>] { - const [rateLimitedValue, setRateLimitedValue, rateLimiter] = - createRateLimitedSignal(value(), initialOptions) + initialOptions: RateLimiterOptions>, +): Accessor { + const [rateLimitedValue, setRateLimitedValue] = createRateLimitedSignal( + value(), + initialOptions, + ) createEffect(() => { setRateLimitedValue(value() as any) }) - return [rateLimitedValue, rateLimiter] + return rateLimitedValue } 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..bc798ac10 100644 --- a/packages/solid-pacer/src/throttler/createThrottledValue.ts +++ b/packages/solid-pacer/src/throttler/createThrottledValue.ts @@ -1,6 +1,5 @@ -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' import type { ThrottlerOptions } from '@tanstack/pacer/throttler' @@ -11,7 +10,7 @@ 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 an accessor function that provides the throttled value. * 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,37 +19,24 @@ 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 = 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()); + * // Use the throttled value + * console.log(throttledValue()); // Access the current throttled value * ``` */ export function createThrottledValue( value: Accessor, - initialOptions: ThrottlerOptions, [Accessor]>, -): [Accessor, SolidThrottler, [Accessor]>] { - const [throttledValue, setThrottledValue, throttler] = createThrottledSignal( + initialOptions: ThrottlerOptions>, +): Accessor { + const [throttledValue, setThrottledValue] = createThrottledSignal( value(), initialOptions, ) createEffect(() => { setThrottledValue(value() as any) - onCleanup(() => { - throttler.cancel() - }) }) - return [throttledValue, throttler] + return throttledValue } 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 } From 6f045cc13283f4257af7fc91e860d1d3a147a45b Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Mon, 5 May 2025 00:10:49 -0500 Subject: [PATCH 07/10] rewrite async throttler to match feature parity of sync throttler --- .../functions/createasyncthrottler.md | 2 +- .../interfaces/solidasyncthrottler.md | 58 +++++- docs/reference/classes/asyncdebouncer.md | 2 +- docs/reference/classes/asyncthrottler.md | 96 +++++++-- docs/reference/classes/throttler.md | 14 +- docs/reference/functions/asyncthrottle.md | 6 +- docs/reference/functions/throttle.md | 2 +- .../interfaces/asyncthrottleroptions.md | 70 ++++++- .../react/useAsyncThrottler/src/index.tsx | 31 ++- .../solid/createAsyncThrottler/src/index.tsx | 31 ++- packages/pacer/src/async-debouncer.ts | 2 +- packages/pacer/src/async-throttler.ts | 185 ++++++++++++------ packages/pacer/src/throttler.ts | 29 +-- packages/pacer/tests/async-throttler.test.ts | 91 ++------- .../async-throttler/createAsyncThrottler.ts | 48 ++++- 15 files changed, 422 insertions(+), 245 deletions(-) diff --git a/docs/framework/solid/reference/functions/createasyncthrottler.md b/docs/framework/solid/reference/functions/createasyncthrottler.md index 6b30c79b1..1de9036fe 100644 --- a/docs/framework/solid/reference/functions/createasyncthrottler.md +++ b/docs/framework/solid/reference/functions/createasyncthrottler.md @@ -11,7 +11,7 @@ title: createAsyncThrottler function createAsyncThrottler(fn, initialOptions): SolidAsyncThrottler ``` -Defined in: [async-throttler/createAsyncThrottler.ts:59](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L59) +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. diff --git a/docs/framework/solid/reference/interfaces/solidasyncthrottler.md b/docs/framework/solid/reference/interfaces/solidasyncthrottler.md index d1fe3abae..2e8462ca9 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncthrottler.md +++ b/docs/framework/solid/reference/interfaces/solidasyncthrottler.md @@ -12,8 +12,12 @@ Defined in: [async-throttler/createAsyncThrottler.ts:8](https://github.com/TanSt ## Extends - `Omit`\<`AsyncThrottler`\<`TFn`\>, - \| `"getExecutionCount"` + \| `"getSuccessCount"` + \| `"getSettleCount"` + \| `"getErrorCount"` \| `"getIsPending"` + \| `"getIsExecuting"` + \| `"getLastResult"` \| `"getLastExecutionTime"` \| `"getNextExecutionTime"`\> @@ -23,13 +27,23 @@ Defined in: [async-throttler/createAsyncThrottler.ts:8](https://github.com/TanSt ## Properties -### executionCount +### errorCount ```ts -executionCount: Accessor; +errorCount: Accessor; ``` -Defined in: [async-throttler/createAsyncThrottler.ts:16](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L16) +Defined in: [async-throttler/createAsyncThrottler.ts:22](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L22) + +*** + +### isExecuting + +```ts +isExecuting: Accessor; +``` + +Defined in: [async-throttler/createAsyncThrottler.ts:24](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L24) *** @@ -39,7 +53,7 @@ Defined in: [async-throttler/createAsyncThrottler.ts:16](https://github.com/TanS isPending: Accessor; ``` -Defined in: [async-throttler/createAsyncThrottler.ts:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L17) +Defined in: [async-throttler/createAsyncThrottler.ts:23](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts#L23) *** @@ -49,7 +63,17 @@ Defined in: [async-throttler/createAsyncThrottler.ts:17](https://github.com/TanS lastExecutionTime: 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: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) *** @@ -59,4 +83,24 @@ Defined in: [async-throttler/createAsyncThrottler.ts:18](https://github.com/TanS nextExecutionTime: 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: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/reference/classes/asyncdebouncer.md b/docs/reference/classes/asyncdebouncer.md index c9afc84ec..f94b7331b 100644 --- a/docs/reference/classes/asyncdebouncer.md +++ b/docs/reference/classes/asyncdebouncer.md @@ -69,7 +69,7 @@ cancel(): void 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 diff --git a/docs/reference/classes/asyncthrottler.md b/docs/reference/classes/asyncthrottler.md index 20405a77a..b3edb067b 100644 --- a/docs/reference/classes/asyncthrottler.md +++ b/docs/reference/classes/asyncthrottler.md @@ -7,7 +7,7 @@ title: AsyncThrottler # Class: AsyncThrottler\ -Defined in: [async-throttler.ts:56](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L56) +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. @@ -43,7 +43,7 @@ inputElement.addEventListener('input', () => { new AsyncThrottler(fn, initialOptions): AsyncThrottler ``` -Defined in: [async-throttler.ts:66](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L66) +Defined in: [async-throttler.ts:89](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L89) #### Parameters @@ -67,9 +67,9 @@ Defined in: [async-throttler.ts:66](https://github.com/TanStack/pacer/blob/main/ cancel(): void ``` -Defined in: [async-throttler.ts:157](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L157) +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 @@ -77,15 +77,15 @@ Cancels any pending execution *** -### getExecutionCount() +### getErrorCount() ```ts -getExecutionCount(): number +getErrorCount(): number ``` -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: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 @@ -93,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:190](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L190) +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 @@ -115,7 +131,7 @@ Returns the current pending state getLastExecutionTime(): number ``` -Defined in: [async-throttler.ts:176](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L176) +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 @@ -125,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:183](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L183) +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 @@ -147,7 +179,7 @@ Returns the next execution time getOptions(): Required> ``` -Defined in: [async-throttler.ts:87](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L87) +Defined in: [async-throttler.ts:115](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L115) Returns the current options @@ -157,13 +189,45 @@ Returns the current options *** +### 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:95](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L95) +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 @@ -176,7 +240,7 @@ If a call is already in progress, it may be blocked or queued depending on the ` #### Returns -`Promise`\<`void`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> *** @@ -186,7 +250,7 @@ If a call is already in progress, it may be blocked or queued depending on the ` setOptions(newOptions): void ``` -Defined in: [async-throttler.ts:80](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L80) +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 diff --git a/docs/reference/classes/throttler.md b/docs/reference/classes/throttler.md index db99e3a98..e807f2571 100644 --- a/docs/reference/classes/throttler.md +++ b/docs/reference/classes/throttler.md @@ -72,7 +72,7 @@ Defined in: [throttler.ts:74](https://github.com/TanStack/pacer/blob/main/packag cancel(): void ``` -Defined in: [throttler.ts:166](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L166) +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. @@ -94,7 +94,7 @@ Has no effect if there is no pending execution. getExecutionCount(): number ``` -Defined in: [throttler.ts:177](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L177) +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 @@ -110,7 +110,7 @@ Returns the number of times the function has been executed getIsPending(): boolean ``` -Defined in: [throttler.ts:184](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L184) +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 @@ -126,7 +126,7 @@ Returns `true` if there is a pending execution getLastExecutionTime(): number ``` -Defined in: [throttler.ts:191](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L191) +Defined in: [throttler.ts:182](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L182) Returns the last execution time @@ -142,7 +142,7 @@ Returns the last execution time getNextExecutionTime(): number ``` -Defined in: [throttler.ts:198](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L198) +Defined in: [throttler.ts:189](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L189) Returns the next execution time @@ -158,7 +158,7 @@ Returns the next execution time getOptions(): Required> ``` -Defined in: [throttler.ts:95](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L95) +Defined in: [throttler.ts:100](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L100) Returns the current throttler options @@ -174,7 +174,7 @@ Returns the current throttler options maybeExecute(...args): void ``` -Defined in: [throttler.ts:121](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L121) +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: diff --git a/docs/reference/functions/asyncthrottle.md b/docs/reference/functions/asyncthrottle.md index 81d1f1e75..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:211](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L211) +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. @@ -46,7 +46,7 @@ If a call is already in progress, it may be blocked or queued depending on the ` ### Returns -`Promise`\<`void`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> ## Example diff --git a/docs/reference/functions/throttle.md b/docs/reference/functions/throttle.md index 3cc6a1acf..d529b83e6 100644 --- a/docs/reference/functions/throttle.md +++ b/docs/reference/functions/throttle.md @@ -11,7 +11,7 @@ title: throttle function throttle(fn, initialOptions): (...args) => void ``` -Defined in: [throttler.ts:229](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/throttler.ts#L229) +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. diff --git a/docs/reference/interfaces/asyncthrottleroptions.md b/docs/reference/interfaces/asyncthrottleroptions.md index 0d22a4035..dc1d6106e 100644 --- a/docs/reference/interfaces/asyncthrottleroptions.md +++ b/docs/reference/interfaces/asyncthrottleroptions.md @@ -30,13 +30,26 @@ 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:15](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L15) +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 @@ -46,25 +59,29 @@ Optional error handler for when the throttled function throws `unknown` +##### asyncThrottler + +[`AsyncThrottler`](../classes/asyncthrottler.md)\<`TFn`\> + #### Returns `void` *** -### onExecute()? +### onSettled()? ```ts -optional onExecute: (throttler) => void; +optional onSettled: (asyncThrottler) => void; ``` -Defined in: [async-throttler.ts:19](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L19) +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 -##### throttler +##### asyncThrottler [`AsyncThrottler`](../classes/asyncthrottler.md)\<`TFn`\> @@ -74,13 +91,52 @@ Optional function to call when the throttled function is executed *** +### onSuccess()? + +```ts +optional onSuccess: (result, asyncThrottler) => void; +``` + +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 + +##### result + +`ReturnType`\<`TFn`\> + +##### asyncThrottler + +[`AsyncThrottler`](../classes/asyncthrottler.md)\<`TFn`\> + +#### Returns + +`void` + +*** + +### 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:24](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L24) +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/examples/react/useAsyncThrottler/src/index.tsx b/examples/react/useAsyncThrottler/src/index.tsx index 7385a2533..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 ( @@ -89,7 +78,7 @@ function App() { {error &&
Error: {error.message}
}
-

API calls made: {setSearchAsyncThrottler.getExecutionCount()}

+

API calls made: {setSearchAsyncThrottler.getSuccessCount()}

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

Loading...

} + {setSearchAsyncThrottler.getIsPending() ? ( +

Pending...

+ ) : setSearchAsyncThrottler.getIsExecuting() ? ( +

Executing...

+ ) : null}
) diff --git a/examples/solid/createAsyncThrottler/src/index.tsx b/examples/solid/createAsyncThrottler/src/index.tsx index 319ca64ef..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 ( @@ -81,15 +76,13 @@ function App() { {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/packages/pacer/src/async-debouncer.ts b/packages/pacer/src/async-debouncer.ts index 94b6c9a00..7dd10af50 100644 --- a/packages/pacer/src/async-debouncer.ts +++ b/packages/pacer/src/async-debouncer.ts @@ -190,7 +190,7 @@ export class AsyncDebouncer { } /** - * Cancels any pending execution + * Cancels any pending execution or aborts any execution in progress */ cancel(): void { this._canLeadingExecute = true diff --git a/packages/pacer/src/async-throttler.ts b/packages/pacer/src/async-throttler.ts index 4bfeea053..880b3b13c 100644 --- a/packages/pacer/src/async-throttler.ts +++ b/packages/pacer/src/async-throttler.ts @@ -9,14 +9,31 @@ export interface AsyncThrottlerOptions { * 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 @@ -26,8 +43,11 @@ export interface AsyncThrottlerOptions { const defaultOptions: Required> = { enabled: true, + leading: true, onError: () => {}, - onExecute: () => {}, + onSettled: () => {}, + onSuccess: () => {}, + trailing: true, wait: 0, } @@ -56,12 +76,15 @@ const defaultOptions: 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: 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, @@ -79,6 +102,11 @@ export class AsyncThrottler { */ setOptions(newOptions: Partial>): void { this._options = { ...this._options, ...newOptions } + + // End the pending state if the debouncer is disabled + if (!this._options.enabled) { + this.cancel() + } } /** @@ -92,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: Parameters): 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: Parameters): 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 */ @@ -184,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 } } diff --git a/packages/pacer/src/throttler.ts b/packages/pacer/src/throttler.ts index 9b7462486..33209fde1 100644 --- a/packages/pacer/src/throttler.ts +++ b/packages/pacer/src/throttler.ts @@ -87,6 +87,11 @@ export class Throttler { */ setOptions(newOptions: Partial>): void { this._options = { ...this._options, ...newOptions } + + // End the pending state if the debouncer is disabled + if (!this._options.enabled) { + this.cancel() + } } /** @@ -172,31 +177,31 @@ export class Throttler { } /** - * 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._timeoutId + 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 } } 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/solid-pacer/src/async-throttler/createAsyncThrottler.ts b/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts index d32f806da..7121d896a 100644 --- a/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts +++ b/packages/solid-pacer/src/async-throttler/createAsyncThrottler.ts @@ -8,13 +8,21 @@ import type { AsyncThrottlerOptions } from '@tanstack/pacer/async-throttler' export interface SolidAsyncThrottler extends Omit< AsyncThrottler, - | 'getExecutionCount' + | '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 } @@ -64,10 +72,22 @@ export function createAsyncThrottler( 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,14 +98,18 @@ export function createAsyncThrottler( 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) }, }) } @@ -93,11 +117,15 @@ export function createAsyncThrottler( setOptions(initialOptions) return { - ...bindInstanceMethods(asyncThrottler), - executionCount, + ...asyncThrottler, + errorCount, + isExecuting, isPending, lastExecutionTime, + lastResult, nextExecutionTime, setOptions, + settleCount, + successCount, } as SolidAsyncThrottler } From 5fab3b20fc21718ede4d8d55efa0f25e8dd655a4 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Mon, 5 May 2025 00:45:24 -0500 Subject: [PATCH 08/10] add return types to async rate limiter --- .changeset/tricky-pants-hear.md | 2 + .../functions/createasyncratelimiter.md | 2 +- .../interfaces/solidasyncratelimiter.md | 34 +++++-- docs/reference/classes/asyncratelimiter.md | 62 +++++++++--- docs/reference/functions/asyncratelimit.md | 6 +- .../interfaces/asyncratelimiteroptions.md | 44 +++++++-- .../react/useAsyncRateLimiter/src/index.tsx | 4 +- examples/solid/asyncDebounce/src/index.tsx | 8 +- examples/solid/asyncRateLimit/src/index.tsx | 6 +- examples/solid/asyncThrottle/src/index.tsx | 6 +- .../solid/createAsyncDebouncer/src/index.tsx | 6 +- .../solid/createAsyncQueuer/src/index.tsx | 15 +-- .../createAsyncRateLimiter/src/index.tsx | 10 +- packages/pacer/src/async-rate-limiter.ts | 97 ++++++++++++------- .../createAsyncRateLimiter.ts | 35 +++++-- 15 files changed, 229 insertions(+), 108 deletions(-) diff --git a/.changeset/tricky-pants-hear.md b/.changeset/tricky-pants-hear.md index 2d615d229..87bd52616 100644 --- a/.changeset/tricky-pants-hear.md +++ b/.changeset/tricky-pants-hear.md @@ -6,6 +6,8 @@ - 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 diff --git a/docs/framework/solid/reference/functions/createasyncratelimiter.md b/docs/framework/solid/reference/functions/createasyncratelimiter.md index ddf232b03..05d8e82b5 100644 --- a/docs/framework/solid/reference/functions/createasyncratelimiter.md +++ b/docs/framework/solid/reference/functions/createasyncratelimiter.md @@ -11,7 +11,7 @@ title: createAsyncRateLimiter function createAsyncRateLimiter(fn, initialOptions): SolidAsyncRateLimiter ``` -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:58](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L58) +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. diff --git a/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md b/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md index 5d5c04df2..3cfca7680 100644 --- a/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md +++ b/docs/framework/solid/reference/interfaces/solidasyncratelimiter.md @@ -12,7 +12,9 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:8](https://github.com/ ## Extends - `Omit`\<`AsyncRateLimiter`\<`TFn`\>, - \| `"getExecutionCount"` + \| `"getSuccessCount"` + \| `"getSettleCount"` + \| `"getErrorCount"` \| `"getRejectionCount"` \| `"getRemainingInWindow"` \| `"getMsUntilNextWindow"`\> @@ -23,13 +25,13 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:8](https://github.com/ ## Properties -### executionCount +### errorCount ```ts -executionCount: Accessor; +errorCount: Accessor; ``` -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:16](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L16) +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) *** @@ -39,7 +41,7 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:16](https://github.com msUntilNextWindow: 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:23](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L23) *** @@ -49,7 +51,7 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:19](https://github.com rejectionCount: Accessor; ``` -Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:17](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L17) +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) *** @@ -59,4 +61,24 @@ Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:17](https://github.com remainingInWindow: Accessor; ``` +Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:22](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L22) + +*** + +### settleCount + +```ts +settleCount: Accessor; +``` + +Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:19](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L19) + +*** + +### 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/reference/classes/asyncratelimiter.md b/docs/reference/classes/asyncratelimiter.md index a3b5b728f..4c551ff76 100644 --- a/docs/reference/classes/asyncratelimiter.md +++ b/docs/reference/classes/asyncratelimiter.md @@ -7,7 +7,7 @@ title: AsyncRateLimiter # Class: AsyncRateLimiter\ -Defined in: [async-rate-limiter.ts:68](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L68) +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. @@ -46,7 +46,7 @@ await rateLimiter.maybeExecute('123'); new AsyncRateLimiter(fn, initialOptions): AsyncRateLimiter ``` -Defined in: [async-rate-limiter.ts:74](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L74) +Defined in: [async-rate-limiter.ts:85](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L85) #### Parameters @@ -64,15 +64,15 @@ Defined in: [async-rate-limiter.ts:74](https://github.com/TanStack/pacer/blob/ma ## Methods -### getExecutionCount() +### getErrorCount() ```ts -getExecutionCount(): number +getErrorCount(): number ``` -Defined in: [async-rate-limiter.ts:167](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L167) +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 @@ -86,7 +86,7 @@ Returns the number of times the function has been executed getMsUntilNextWindow(): number ``` -Defined in: [async-rate-limiter.ts:189](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L189) +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 @@ -102,7 +102,7 @@ Returns the number of milliseconds until the next execution will be possible getOptions(): Required> ``` -Defined in: [async-rate-limiter.ts:95](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L95) +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 @@ -118,7 +118,7 @@ Returns the current rate limiter options getRejectionCount(): number ``` -Defined in: [async-rate-limiter.ts:174](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L174) +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 @@ -134,7 +134,7 @@ Returns the number of times the function has been rejected getRemainingInWindow(): number ``` -Defined in: [async-rate-limiter.ts:181](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L181) +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 @@ -144,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:115](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L115) +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. @@ -164,7 +196,7 @@ If execution is allowed, waits for any previous execution to complete before pro #### Returns -`Promise`\<`boolean`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> #### Example @@ -186,7 +218,7 @@ await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected reset(): void ``` -Defined in: [async-rate-limiter.ts:196](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L196) +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 @@ -202,7 +234,7 @@ Resets the rate limiter state setOptions(newOptions): void ``` -Defined in: [async-rate-limiter.ts:88](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L88) +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 diff --git a/docs/reference/functions/asyncratelimit.md b/docs/reference/functions/asyncratelimit.md index b157e5bcd..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:233](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L233) +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. @@ -53,7 +53,7 @@ If execution is allowed, waits for any previous execution to complete before pro ### Returns -`Promise`\<`boolean`\> +`Promise`\<`undefined` \| `ReturnType`\<`TFn`\>\> ### Example diff --git a/docs/reference/interfaces/asyncratelimiteroptions.md b/docs/reference/interfaces/asyncratelimiteroptions.md index 8cf19fd3a..0319b4d6c 100644 --- a/docs/reference/interfaces/asyncratelimiteroptions.md +++ b/docs/reference/interfaces/asyncratelimiteroptions.md @@ -45,7 +45,7 @@ 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:19](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L19) @@ -58,16 +58,42 @@ Optional error handler for when the rate-limited function throws `unknown` +##### rateLimiter + +[`AsyncRateLimiter`](../classes/asyncratelimiter.md)\<`TFn`\> + #### Returns `void` *** -### onExecute()? +### onReject()? ```ts -optional onExecute: (rateLimiter) => void; +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` + +*** + +### onSettled()? + +```ts +optional onSettled: (rateLimiter) => void; ``` Defined in: [async-rate-limiter.ts:23](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L23) @@ -86,18 +112,22 @@ 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: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`\> @@ -114,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:31](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L31) +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/examples/react/useAsyncRateLimiter/src/index.tsx b/examples/react/useAsyncRateLimiter/src/index.tsx index 64221b3dd..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 @@ -90,7 +90,7 @@ function App() { {error &&
    Error: {error.message}
    }
    -

    API calls made: {setSearchAsyncRateLimiter.getExecutionCount()}

    +

    API calls made: {setSearchAsyncRateLimiter.getSuccessCount()}

    {results.length > 0 && (
      {results.map((item) => ( diff --git a/examples/solid/asyncDebounce/src/index.tsx b/examples/solid/asyncDebounce/src/index.tsx index 87c701de0..b8c414a84 100644 --- a/examples/solid/asyncDebounce/src/index.tsx +++ b/examples/solid/asyncDebounce/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 { asyncDebounce } from '@tanstack/solid-pacer/async-debouncer' @@ -69,11 +69,7 @@ function SearchApp() {
    Execution Count:{throttler.executionCount()}
    Instant Search: {instantSearch()}

    Search Results:

    -
      - {searchResults().map((result) => ( -
    • {result}
    • - ))} -
    + {(result) =>
  • {result}
  • }
    ) diff --git a/examples/solid/asyncRateLimit/src/index.tsx b/examples/solid/asyncRateLimit/src/index.tsx index 3ebddf42d..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' @@ -75,9 +75,7 @@ function SearchApp() {

    Search Results:

      - {searchResults().map((result) => ( -
    • {result}
    • - ))} + {(result) =>
    • {result}
    • }
    diff --git a/examples/solid/asyncThrottle/src/index.tsx b/examples/solid/asyncThrottle/src/index.tsx index ad7efad00..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' @@ -70,9 +70,7 @@ function SearchApp() {

    Search Results:

      - {searchResults().map((result) => ( -
    • {result}
    • - ))} + {(result) =>
    • {result}
    • }
    diff --git a/examples/solid/createAsyncDebouncer/src/index.tsx b/examples/solid/createAsyncDebouncer/src/index.tsx index 539312b18..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' @@ -86,9 +86,7 @@ function App() {

    API calls made: {setSearchAsyncDebouncer.successCount()}

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

    Loading...

    } 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()} +
    + )} +
    {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/packages/pacer/src/async-rate-limiter.ts b/packages/pacer/src/async-rate-limiter.ts index e92d5dab2..c87575dd2 100644 --- a/packages/pacer/src/async-rate-limiter.ts +++ b/packages/pacer/src/async-rate-limiter.ts @@ -16,11 +16,18 @@ 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 */ @@ -35,9 +42,10 @@ const defaultOptions: Required< Omit, 'limit' | 'window'> > = { enabled: true, - onReject: () => {}, onError: () => {}, - onExecute: () => {}, + onReject: () => {}, + onSettled: () => {}, + onSuccess: () => {}, } /** @@ -66,10 +74,13 @@ const defaultOptions: Required< * ``` */ export class AsyncRateLimiter { - private _executionCount = 0 - private _executionTimes: Array = [] private _options: AsyncRateLimiterOptions + private _errorCount = 0 + private _executionTimes: Array = [] + private _lastResult: ReturnType | undefined private _rejectionCount = 0 + private _settleCount = 0 + private _successCount = 0 constructor( private fn: TFn, @@ -112,38 +123,40 @@ export class AsyncRateLimiter { * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected * ``` */ - async maybeExecute(...args: Parameters): 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: Parameters): 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 { @@ -161,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 } /** @@ -195,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 } } diff --git a/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts b/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts index b45c5c655..be2d442fe 100644 --- a/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts +++ b/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts @@ -8,12 +8,16 @@ import type { AsyncRateLimiterOptions } from '@tanstack/pacer/async-rate-limiter export interface SolidAsyncRateLimiter extends Omit< AsyncRateLimiter, - | 'getExecutionCount' + | 'getSuccessCount' + | 'getSettleCount' + | 'getErrorCount' | 'getRejectionCount' | 'getRemainingInWindow' | 'getMsUntilNextWindow' > { - executionCount: Accessor + successCount: Accessor + settleCount: Accessor + errorCount: Accessor rejectionCount: Accessor remainingInWindow: Accessor msUntilNextWindow: Accessor @@ -61,12 +65,18 @@ export function createAsyncRateLimiter( ): 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(), ) @@ -77,13 +87,16 @@ export function createAsyncRateLimiter( 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()) @@ -100,10 +113,12 @@ export function createAsyncRateLimiter( return { ...bindInstanceMethods(asyncRateLimiter), - executionCount, - rejectionCount, + errorCount, remainingInWindow, msUntilNextWindow, + rejectionCount, setOptions, + settleCount, + successCount, } as SolidAsyncRateLimiter } From 8e71b5dbb331eefd11325747e07950ce0e7834fc Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Mon, 5 May 2025 01:42:58 -0500 Subject: [PATCH 09/10] update documentation --- docs/config.json | 23 +++++++++++ docs/guides/debouncing.md | 72 +++++++++++++++++++++++++++-------- docs/guides/queueing.md | 74 ++++++++++++++++++++++++++++-------- docs/guides/rate-limiting.md | 61 +++++++++++++++++++++++------ docs/guides/throttling.md | 66 +++++++++++++++++++++++++------- 5 files changed, 239 insertions(+), 57 deletions(-) diff --git a/docs/config.json b/docs/config.json index ce8a83cb7..170505dcd 100644 --- a/docs/config.json +++ b/docs/config.json @@ -648,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/guides/debouncing.md b/docs/guides/debouncing.md index 1af64dabf..4e05b927f 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..89a455e80 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 diff --git a/docs/guides/throttling.md b/docs/guides/throttling.md index 817f318f0..8eb65ac29 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 } ) From 228348e4939425a4aca95c949b7fce5882a4183f Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Mon, 5 May 2025 02:27:49 -0500 Subject: [PATCH 10/10] change value hooks back to tuples. not a breaking change anymore --- .changeset/tricky-pants-hear-react.md | 1 - .changeset/tricky-pants-hear-solid.md | 1 - .../reference/functions/usedebouncedvalue.md | 12 ++++++---- .../functions/useratelimitedvalue.md | 14 ++++++----- .../reference/functions/usethrottledvalue.md | 14 ++++++----- .../functions/createdebouncedvalue.md | 18 +++++++-------- .../functions/createratelimitedvalue.md | 15 ++++++++---- .../functions/createthrottledvalue.md | 16 +++++++++---- docs/guides/debouncing.md | 2 +- docs/guides/rate-limiting.md | 2 +- docs/guides/throttling.md | 2 +- .../src/index.tsx | 17 ++++++++------ .../react-query-queued-prefetch/src/index.tsx | 7 ++---- .../src/index.tsx | 17 ++++++++------ .../react/useDebouncedValue/src/index.tsx | 5 ++-- .../react/useRateLimitedValue/src/index.tsx | 5 ++-- .../react/useThrottledValue/src/index.tsx | 4 ++-- .../solid/createDebouncedValue/src/index.tsx | 4 ++-- .../createRateLimitedValue/src/index.tsx | 4 ++-- .../solid/createThrottledValue/src/index.tsx | 4 ++-- .../src/debouncer/useDebouncedValue.ts | 14 ++++++----- .../src/rate-limiter/useRateLimitedValue.ts | 23 +++++++++++-------- .../src/throttler/useThrottledValue.ts | 19 +++++++++------ .../src/debouncer/createDebouncedValue.ts | 19 +++++++-------- .../rate-limiter/createRateLimitedValue.ts | 20 +++++++++------- .../src/throttler/createThrottledValue.ts | 17 ++++++++++---- 26 files changed, 159 insertions(+), 117 deletions(-) diff --git a/.changeset/tricky-pants-hear-react.md b/.changeset/tricky-pants-hear-react.md index eca323743..33e9654d4 100644 --- a/.changeset/tricky-pants-hear-react.md +++ b/.changeset/tricky-pants-hear-react.md @@ -4,5 +4,4 @@ - breaking: renamed `useQueuerState` hook to `useQueuedState` - breaking: changed return signature of `useQueuedState` to include the `addItem` function -- breaking: changed return signature of `useDebouncedValue` to return a single value and not a tuple - feat: add `useQueuedValue` hook diff --git a/.changeset/tricky-pants-hear-solid.md b/.changeset/tricky-pants-hear-solid.md index 44281c9bb..172cc4b82 100644 --- a/.changeset/tricky-pants-hear-solid.md +++ b/.changeset/tricky-pants-hear-solid.md @@ -4,5 +4,4 @@ - breaking: renamed `createQueuerState` hook to `createQueuedState` - breaking: changed return signature of `createQueuedState` to include the `addItem` function -- breaking: changed return signature of `createDebouncedValue` to return a single value and not a tuple - feat: add `createQueuedValue` hook diff --git a/docs/framework/react/reference/functions/usedebouncedvalue.md b/docs/framework/react/reference/functions/usedebouncedvalue.md index d3869f36a..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 +function useDebouncedValue(value, options): [TValue, Debouncer>>] ``` -Defined in: [react-pacer/src/debouncer/useDebouncedValue.ts:39](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/debouncer/useDebouncedValue.ts#L39) +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,7 +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 the current debounced value. +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 @@ -43,14 +45,14 @@ The hook returns the current debounced value. ## Returns -`TValue` +\[`TValue`, `Debouncer`\<`Dispatch`\<`SetStateAction`\<`TValue`\>\>\>\] ## Example ```tsx // Debounce a search query const [searchQuery, setSearchQuery] = useState(''); -const debouncedQuery = useDebouncedValue(searchQuery, { +const [debouncedQuery, debouncer] = useDebouncedValue(searchQuery, { wait: 500 // Wait 500ms after last change }); diff --git a/docs/framework/react/reference/functions/useratelimitedvalue.md b/docs/framework/react/reference/functions/useratelimitedvalue.md index c6ebd5fd7..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 +function useRateLimitedValue(value, options): [TValue, RateLimiter>>] ``` -Defined in: [react-pacer/src/rate-limiter/useRateLimitedValue.ts:42](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts#L42) +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 the rate-limited value that updates according to the configured rate limit. +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. @@ -47,19 +49,19 @@ consider using the lower-level useRateLimiter hook instead. ## Returns -`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 }); // With rejection callback -const rateLimitedValue = useRateLimitedValue(rawValue, { +const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { limit: 3, window: 5000, onReject: (rateLimiter) => { diff --git a/docs/framework/react/reference/functions/usethrottledvalue.md b/docs/framework/react/reference/functions/usethrottledvalue.md index 8456383d7..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 +function useThrottledValue(value, options): [TValue, Throttler>>] ``` -Defined in: [react-pacer/src/throttler/useThrottledValue.ts:30](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/throttler/useThrottledValue.ts#L30) +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,7 +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 the throttled value that updates 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. @@ -40,16 +42,16 @@ consider using the lower-level useThrottler hook instead. ## Returns -`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 = useThrottledValue(rawValue, { +const [throttledValue, throttler] = useThrottledValue(rawValue, { wait: 1000, leading: true, // Update immediately on first change trailing: false // Skip trailing edge updates diff --git a/docs/framework/solid/reference/functions/createdebouncedvalue.md b/docs/framework/solid/reference/functions/createdebouncedvalue.md index 69d633d44..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 +function createDebouncedValue(value, initialOptions): [Accessor, SolidDebouncer>] ``` -Defined in: [debouncer/createDebouncedValue.ts:40](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/debouncer/createDebouncedValue.ts#L40) +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 @@ -25,7 +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 an Accessor that provides the current debounced value. +The hook returns a tuple containing: +- An Accessor that provides the current debounced value +- The debouncer instance with control methods ## Type Parameters @@ -43,14 +45,14 @@ The hook returns an Accessor that provides the current debounced value. ## Returns -`Accessor`\<`TValue`\> +\[`Accessor`\<`TValue`\>, [`SolidDebouncer`](../interfaces/soliddebouncer.md)\<`Setter`\<`TValue`\>\>\] ## Example ```tsx // Debounce a search query const [searchQuery, setSearchQuery] = createSignal(''); -const debouncedQuery = createDebouncedValue(searchQuery, { +const [debouncedQuery, debouncer] = createDebouncedValue(searchQuery, { wait: 500 // Wait 500ms after last change }); @@ -59,8 +61,6 @@ createEffect(() => { fetchSearchResults(debouncedQuery()); }); -// 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/createratelimitedvalue.md b/docs/framework/solid/reference/functions/createratelimitedvalue.md index ba13c790c..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 +function createRateLimitedValue(value, initialOptions): [Accessor, SolidRateLimiter>] ``` -Defined in: [rate-limiter/createRateLimitedValue.ts:37](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts#L37) +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 an accessor function that provides the rate-limited value. +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. @@ -47,17 +49,20 @@ consider using the lower-level createRateLimiter hook instead. ## Returns -`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 }); // Use the rate-limited value console.log(rateLimitedValue()); // Access the current rate-limited value + +// Control the rate limiter +rateLimiter.reset(); // Reset the rate limit window ``` diff --git a/docs/framework/solid/reference/functions/createthrottledvalue.md b/docs/framework/solid/reference/functions/createthrottledvalue.md index 6097f541f..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 +function createThrottledValue(value, initialOptions): [Accessor, SolidThrottler>] ``` -Defined in: [throttler/createThrottledValue.ts:28](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/throttler/createThrottledValue.ts#L28) +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 an accessor function that provides the throttled value. +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, @@ -41,14 +44,17 @@ consider using the lower-level createThrottler hook instead. ## Returns -`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 }); +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/guides/debouncing.md b/docs/guides/debouncing.md index 4e05b927f..c316f36f1 100644 --- a/docs/guides/debouncing.md +++ b/docs/guides/debouncing.md @@ -279,7 +279,7 @@ const handleSearch = useDebouncedCallback( // State-based hook for reactive state management const [instantState, setInstantState] = useState('') -const debouncedValue = useDebouncedValue( +const [debouncedValue] = useDebouncedValue( instantState, // Value to debounce { wait: 500 } ) diff --git a/docs/guides/rate-limiting.md b/docs/guides/rate-limiting.md index 89a455e80..3fbc417d1 100644 --- a/docs/guides/rate-limiting.md +++ b/docs/guides/rate-limiting.md @@ -271,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 8eb65ac29..0f0827bd3 100644 --- a/docs/guides/throttling.md +++ b/docs/guides/throttling.md @@ -255,7 +255,7 @@ const handleUpdate = useThrottledCallback( // State-based hook for reactive state management const [instantState, setInstantState] = useState(0) -const throttledValue = useThrottledValue( +const [throttledValue] = useThrottledValue( instantState, // Value to throttle { wait: 200 } ) diff --git a/examples/react/react-query-debounced-prefetch/src/index.tsx b/examples/react/react-query-debounced-prefetch/src/index.tsx index 7541f18d7..ec9e8299c 100644 --- a/examples/react/react-query-debounced-prefetch/src/index.tsx +++ b/examples/react/react-query-debounced-prefetch/src/index.tsx @@ -1,9 +1,8 @@ -import React from 'react' +import React, { useEffect } from 'react' import ReactDOM from 'react-dom/client' import { QueryClient, QueryClientProvider, - usePrefetchQuery, useQuery, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' @@ -41,15 +40,19 @@ function PostList({ >(null) // debounce the hovered post id to avoid excessive prefetches - const debouncedHoveredPostId = useDebouncedValue(currentHoveredPostId, { + const [debouncedHoveredPostId] = useDebouncedValue(currentHoveredPostId, { wait: 100, // adjust this value to see the difference }) // perform the prefetch when the debounced hovered post id changes - usePrefetchQuery({ - queryKey: ['post', debouncedHoveredPostId], - queryFn: () => fetchPost(debouncedHoveredPostId!), - }) + useEffect(() => { + if (debouncedHoveredPostId) { + queryClient.ensureQueryData({ + queryKey: ['post', debouncedHoveredPostId], + queryFn: () => fetchPost(debouncedHoveredPostId), + }) + } + }, [debouncedHoveredPostId]) const handleMouseEnter = (postId: number) => { setCurrentHoveredPostId(postId) // update the hovered post id diff --git a/examples/react/react-query-queued-prefetch/src/index.tsx b/examples/react/react-query-queued-prefetch/src/index.tsx index 36fea82da..865ea21cf 100644 --- a/examples/react/react-query-queued-prefetch/src/index.tsx +++ b/examples/react/react-query-queued-prefetch/src/index.tsx @@ -49,12 +49,9 @@ function PostList({ }) useEffect(() => { - if ( - queuedHoveredPostId && - !queryClient.getQueryData(['post', queuedHoveredPostId]) - ) { + if (queuedHoveredPostId) { // queue up the hovered post id to be processed in order - queryClient.prefetchQuery({ + queryClient.ensureQueryData({ queryKey: ['post', queuedHoveredPostId], queryFn: () => fetchPost(queuedHoveredPostId), }) diff --git a/examples/react/react-query-throttled-prefetch/src/index.tsx b/examples/react/react-query-throttled-prefetch/src/index.tsx index 17ae49ad6..4d9b11118 100644 --- a/examples/react/react-query-throttled-prefetch/src/index.tsx +++ b/examples/react/react-query-throttled-prefetch/src/index.tsx @@ -1,9 +1,8 @@ -import React from 'react' +import React, { useEffect } from 'react' import ReactDOM from 'react-dom/client' import { QueryClient, QueryClientProvider, - usePrefetchQuery, useQuery, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' @@ -41,15 +40,19 @@ function PostList({ >(null) // throttle the hovered post id to avoid excessive prefetches - const throttledHoveredPostId = useThrottledValue(currentHoveredPostId, { + const [throttledHoveredPostId] = useThrottledValue(currentHoveredPostId, { wait: 100, // adjust this value to see the difference }) // perform the prefetch when the throttled hovered post id changes - usePrefetchQuery({ - queryKey: ['post', throttledHoveredPostId], - queryFn: () => fetchPost(throttledHoveredPostId!), - }) + useEffect(() => { + if (throttledHoveredPostId) { + queryClient.ensureQueryData({ + queryKey: ['post', throttledHoveredPostId], + queryFn: () => fetchPost(throttledHoveredPostId), + }) + } + }, [throttledHoveredPostId]) const handleMouseEnter = (postId: number) => { setCurrentHoveredPostId(postId) // update the hovered post id diff --git a/examples/react/useDebouncedValue/src/index.tsx b/examples/react/useDebouncedValue/src/index.tsx index 496836cca..67806e735 100644 --- a/examples/react/useDebouncedValue/src/index.tsx +++ b/examples/react/useDebouncedValue/src/index.tsx @@ -10,7 +10,7 @@ function App1() { } // highest-level hook that watches an instant local state value and returns a debounced value - const debouncedCount = useDebouncedValue(instantCount, { + const [debouncedCount] = useDebouncedValue(instantCount, { wait: 500, // enabled: instantCount > 2, // optional, defaults to true // leading: true, // optional, defaults to false @@ -42,7 +42,8 @@ function App2() { const [instantSearch, setInstantSearch] = useState('') // highest-level hook that watches an instant local state value and returns a debounced value - const debouncedSearch = 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 }) diff --git a/examples/react/useRateLimitedValue/src/index.tsx b/examples/react/useRateLimitedValue/src/index.tsx index b6e589b0b..e1a662f14 100644 --- a/examples/react/useRateLimitedValue/src/index.tsx +++ b/examples/react/useRateLimitedValue/src/index.tsx @@ -6,7 +6,8 @@ function App1() { const [instantCount, setInstantCount] = useState(0) // Using useRateLimitedValue with a rate limit of 5 executions per 5 seconds - const limitedCount = useRateLimitedValue(instantCount, { + // optionally, grab the rate limiter from the last index of the returned array + const [limitedCount] = useRateLimitedValue(instantCount, { // enabled: instantCount > 2, // optional, defaults to true limit: 5, window: 5000, @@ -47,7 +48,7 @@ function App2() { const [instantSearch, setInstantSearch] = useState('') // Using useRateLimitedValue with a rate limit of 5 executions per 5 seconds - const limitedSearch = useRateLimitedValue(instantSearch, { + const [limitedSearch] = useRateLimitedValue(instantSearch, { // enabled: instantSearch.length > 2, // optional, defaults to true limit: 5, window: 5000, diff --git a/examples/react/useThrottledValue/src/index.tsx b/examples/react/useThrottledValue/src/index.tsx index 892722f53..2f07712d7 100644 --- a/examples/react/useThrottledValue/src/index.tsx +++ b/examples/react/useThrottledValue/src/index.tsx @@ -11,7 +11,7 @@ function App1() { // highest-level hook that watches an instant local state value and returns a throttled value // optionally, grab the throttler from the last index of the returned array - const throttledCount = useThrottledValue(instantCount, { + const [throttledCount] = useThrottledValue(instantCount, { wait: 1000, // enabled: instantCount > 2, // optional, defaults to true }) @@ -42,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 = useThrottledValue(instantSearch, { + const [throttledSearch] = useThrottledValue(instantSearch, { wait: 1000, // enabled: instantSearch.length > 2, // optional, defaults to true }) diff --git a/examples/solid/createDebouncedValue/src/index.tsx b/examples/solid/createDebouncedValue/src/index.tsx index 1ddd6cf49..906a6bde8 100644 --- a/examples/solid/createDebouncedValue/src/index.tsx +++ b/examples/solid/createDebouncedValue/src/index.tsx @@ -10,7 +10,7 @@ function App1() { } // highest-level hook that watches an instant local state value and returns a debounced value - const debouncedCount = createDebouncedValue(instantCount, { + const [debouncedCount] = createDebouncedValue(instantCount, { wait: 500, // leading: true, // optional, defaults to false }) @@ -41,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 = createDebouncedValue(instantSearch, { + const [debouncedSearch] = createDebouncedValue(instantSearch, { wait: 500, }) diff --git a/examples/solid/createRateLimitedValue/src/index.tsx b/examples/solid/createRateLimitedValue/src/index.tsx index 332a129c4..be1d74edc 100644 --- a/examples/solid/createRateLimitedValue/src/index.tsx +++ b/examples/solid/createRateLimitedValue/src/index.tsx @@ -6,7 +6,7 @@ function App1() { const [instantCount, setInstantCount] = createSignal(0) // Using createRateLimitedValue with a rate limit of 5 executions per 5 seconds - const limitedCount = createRateLimitedValue(instantCount, { + const [limitedCount] = createRateLimitedValue(instantCount, { limit: 5, window: 5000, }) @@ -41,7 +41,7 @@ function App2() { const [instantSearch, setInstantSearch] = createSignal('') // Using createRateLimitedValue with a rate limit of 5 executions per 5 seconds - const limitedSearch = createRateLimitedValue(instantSearch, { + const [limitedSearch] = createRateLimitedValue(instantSearch, { limit: 5, window: 5000, }) diff --git a/examples/solid/createThrottledValue/src/index.tsx b/examples/solid/createThrottledValue/src/index.tsx index 7f80c2eb7..7e236dab1 100644 --- a/examples/solid/createThrottledValue/src/index.tsx +++ b/examples/solid/createThrottledValue/src/index.tsx @@ -11,7 +11,7 @@ function App1() { // highest-level hook that watches an instant local state value and returns a throttled value // optionally, grab the throttler from the last index of the returned array - const throttledCount = createThrottledValue(instantCount, { + const [throttledCount] = createThrottledValue(instantCount, { wait: 1000, }) @@ -41,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 = createThrottledValue(instantSearch, { + const [throttledSearch] = createThrottledValue(instantSearch, { wait: 1000, }) diff --git a/packages/react-pacer/src/debouncer/useDebouncedValue.ts b/packages/react-pacer/src/debouncer/useDebouncedValue.ts index 932bf8cc8..8c37cface 100644 --- a/packages/react-pacer/src/debouncer/useDebouncedValue.ts +++ b/packages/react-pacer/src/debouncer/useDebouncedValue.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { useDebouncedState } from './useDebouncedState' -import type { DebouncerOptions } from '@tanstack/pacer/debouncer' +import type { Debouncer, DebouncerOptions } from '@tanstack/pacer/debouncer' /** * A React hook that creates a debounced value that updates only after a specified delay. @@ -15,13 +15,15 @@ import type { 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 the current debounced value. + * 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 * // Debounce a search query * const [searchQuery, setSearchQuery] = useState(''); - * const debouncedQuery = useDebouncedValue(searchQuery, { + * const [debouncedQuery, debouncer] = useDebouncedValue(searchQuery, { * wait: 500 // Wait 500ms after last change * }); * @@ -39,7 +41,7 @@ import type { DebouncerOptions } from '@tanstack/pacer/debouncer' export function useDebouncedValue( value: TValue, options: DebouncerOptions>>, -): TValue { +): [TValue, Debouncer>>] { const [debouncedValue, setDebouncedValue, debouncer] = useDebouncedState( value, options, @@ -47,7 +49,7 @@ export function useDebouncedValue( useEffect(() => { setDebouncedValue(value) - }, [value, setDebouncedValue, debouncer]) + }, [value, setDebouncedValue]) - return debouncedValue + return [debouncedValue, debouncer] } diff --git a/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts b/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts index 09f4c79dd..ffdc3ef3f 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts @@ -1,6 +1,9 @@ import { useEffect } from 'react' import { useRateLimitedState } from './useRateLimitedState' -import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' +import type { + RateLimiter, + RateLimiterOptions, +} from '@tanstack/pacer/rate-limiter' /** * 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. @@ -16,7 +19,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 the rate-limited value that updates according to the configured rate limit. + * 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. @@ -24,13 +29,13 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * @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 * }); * * // With rejection callback - * const rateLimitedValue = useRateLimitedValue(rawValue, { + * const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { * limit: 3, * window: 5000, * onReject: (rateLimiter) => { @@ -42,15 +47,13 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' export function useRateLimitedValue( value: TValue, options: RateLimiterOptions>>, -): TValue { - const [rateLimitedValue, setRateLimitedValue] = useRateLimitedState( - value, - options, - ) +): [TValue, RateLimiter>>] { + const [rateLimitedValue, setRateLimitedValue, rateLimiter] = + useRateLimitedState(value, options) useEffect(() => { setRateLimitedValue(value) }, [value, setRateLimitedValue]) - return rateLimitedValue + return [rateLimitedValue, rateLimiter] } diff --git a/packages/react-pacer/src/throttler/useThrottledValue.ts b/packages/react-pacer/src/throttler/useThrottledValue.ts index 6dcae9b39..30f6be8ab 100644 --- a/packages/react-pacer/src/throttler/useThrottledValue.ts +++ b/packages/react-pacer/src/throttler/useThrottledValue.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { useThrottledState } from './useThrottledState' -import type { ThrottlerOptions } from '@tanstack/pacer/throttler' +import type { Throttler, ThrottlerOptions } from '@tanstack/pacer/throttler' /** * A high-level React hook that creates a throttled version of a value that updates at most once within a specified time window. @@ -9,7 +9,9 @@ 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 the throttled value that updates 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. @@ -17,10 +19,10 @@ import type { 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 = useThrottledValue(rawValue, { + * const [throttledValue, throttler] = useThrottledValue(rawValue, { * wait: 1000, * leading: true, // Update immediately on first change * trailing: false // Skip trailing edge updates @@ -30,12 +32,15 @@ import type { ThrottlerOptions } from '@tanstack/pacer/throttler' export function useThrottledValue( value: TValue, options: ThrottlerOptions>>, -): TValue { - const [throttledValue, setThrottledValue] = useThrottledState(value, options) +): [TValue, Throttler>>] { + const [throttledValue, setThrottledValue, throttler] = useThrottledState( + value, + options, + ) useEffect(() => { setThrottledValue(value) }, [value, setThrottledValue]) - return throttledValue + return [throttledValue, throttler] } diff --git a/packages/solid-pacer/src/debouncer/createDebouncedValue.ts b/packages/solid-pacer/src/debouncer/createDebouncedValue.ts index 93a6a4eca..e610d6a26 100644 --- a/packages/solid-pacer/src/debouncer/createDebouncedValue.ts +++ b/packages/solid-pacer/src/debouncer/createDebouncedValue.ts @@ -1,5 +1,6 @@ import { createEffect } from 'solid-js' import { createDebouncedSignal } from './createDebouncedSignal' +import type { SolidDebouncer } from './createDebouncer' import type { Accessor, Setter } from 'solid-js' import type { DebouncerOptions } from '@tanstack/pacer/debouncer' @@ -16,13 +17,15 @@ import type { 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 an Accessor that provides the current debounced value. + * The hook returns a tuple containing: + * - An Accessor that provides the current debounced value + * - The debouncer instance with control methods * * @example * ```tsx * // Debounce a search query * const [searchQuery, setSearchQuery] = createSignal(''); - * const debouncedQuery = createDebouncedValue(searchQuery, { + * const [debouncedQuery, debouncer] = createDebouncedValue(searchQuery, { * wait: 500 // Wait 500ms after last change * }); * @@ -31,17 +34,15 @@ import type { DebouncerOptions } from '@tanstack/pacer/debouncer' * fetchSearchResults(debouncedQuery()); * }); * - * // 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 { - const [debouncedValue, setDebouncedValue] = createDebouncedSignal( +): [Accessor, SolidDebouncer>] { + const [debouncedValue, setDebouncedValue, debouncer] = createDebouncedSignal( value(), initialOptions, ) @@ -50,5 +51,5 @@ export function createDebouncedValue( setDebouncedValue(value() as any) }) - return debouncedValue + return [debouncedValue, debouncer] } diff --git a/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts b/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts index baba717c5..2d895f870 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts @@ -1,5 +1,6 @@ import { createEffect } from 'solid-js' import { createRateLimitedSignal } from './createRateLimitedSignal' +import type { SolidRateLimiter } from './createRateLimiter' import type { Accessor, Setter } from 'solid-js' import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' @@ -17,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 an accessor function that provides the rate-limited value. + * 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. @@ -25,27 +28,28 @@ 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 * }); * * // Use the rate-limited value * console.log(rateLimitedValue()); // Access the current rate-limited value + * + * // Control the rate limiter + * rateLimiter.reset(); // Reset the rate limit window * ``` */ export function createRateLimitedValue( value: Accessor, initialOptions: RateLimiterOptions>, -): Accessor { - const [rateLimitedValue, setRateLimitedValue] = createRateLimitedSignal( - value(), - initialOptions, - ) +): [Accessor, SolidRateLimiter>] { + const [rateLimitedValue, setRateLimitedValue, rateLimiter] = + createRateLimitedSignal(value(), initialOptions) createEffect(() => { setRateLimitedValue(value() as any) }) - return rateLimitedValue + return [rateLimitedValue, rateLimiter] } diff --git a/packages/solid-pacer/src/throttler/createThrottledValue.ts b/packages/solid-pacer/src/throttler/createThrottledValue.ts index bc798ac10..eb17e01cd 100644 --- a/packages/solid-pacer/src/throttler/createThrottledValue.ts +++ b/packages/solid-pacer/src/throttler/createThrottledValue.ts @@ -1,5 +1,6 @@ import { createEffect } from 'solid-js' import { createThrottledSignal } from './createThrottledSignal' +import type { SolidThrottler } from './createThrottler' import type { Accessor, Setter } from 'solid-js' import type { ThrottlerOptions } from '@tanstack/pacer/throttler' @@ -10,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 an accessor function that provides the throttled value. + * 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, @@ -19,17 +23,20 @@ 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 }); * * // Use the throttled value * console.log(throttledValue()); // Access the current throttled value + * + * // Control the throttler + * throttler.cancel(); // Cancel any pending updates * ``` */ export function createThrottledValue( value: Accessor, initialOptions: ThrottlerOptions>, -): Accessor { - const [throttledValue, setThrottledValue] = createThrottledSignal( +): [Accessor, SolidThrottler>] { + const [throttledValue, setThrottledValue, throttler] = createThrottledSignal( value(), initialOptions, ) @@ -38,5 +45,5 @@ export function createThrottledValue( setThrottledValue(value() as any) }) - return throttledValue + return [throttledValue, throttler] }