diff --git a/README.md b/README.md index 922b903f6..b923efe56 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,11 @@ Try other [TanStack](https://tanstack.com) libraries: You may know **TanSack Pacer** by our adapter names, too! - [**React Pacer**](https://tanstack.com/pacer/latest/docs/framework/react/react-pacer) +- [**Solid Pacer**](https://tanstack.com/pacer/latest/docs/framework/solid/solid-pacer) +- Angular Pacer - needs a contributor! +- Preact Pacer - Coming soon! (After React Pacer is more fleshed out) +- Svelte Pacer - needs a contributor! +- Vue Pacer - needs a contributor! ## Summary @@ -64,6 +69,7 @@ Take control of your application's timing with TanStack Pacer's rate limiting, t - **Rate Limiting** - Limit the rate at which a function can fire - Synchronous or Asynchronous Rate Limiting utilities with promise support and error handling + - Fixed or Sliding window types - **Queuing** - Queue functions to be executed in a specific order - Choose from FIFO, LIFO, and Priority queue implementations @@ -90,6 +96,7 @@ Install one of the following packages based on your framework of choice: ```bash # Npm npm install @tanstack/react-pacer +npm install @tanstack/solid-pacer npm install @tanstack/pacer # no framework, just vanilla js ``` diff --git a/docs/framework/react/reference/functions/useasyncratelimiter.md b/docs/framework/react/reference/functions/useasyncratelimiter.md index 7be183836..f0589d8cf 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: [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) +Defined in: [react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts:54](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts#L54) A low-level React hook that creates an `AsyncRateLimiter` instance to limit how many times an async function can execute within a time window. @@ -22,6 +22,16 @@ Rate limiting allows an async function to execute up to a specified limit within then blocks subsequent calls until the window passes. This is useful for respecting API rate limits, managing resource constraints, or controlling bursts of async operations. +Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function, +making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call +instead of setting the result on a state variable from within the rate-limited function. + +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All executions within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows executions as old ones expire. This provides a more + consistent rate of execution over time. + ## Type Parameters • **TFn** *extends* `AnyAsyncFunction` @@ -43,21 +53,22 @@ managing resource constraints, or controlling bursts of async operations. ## Example ```tsx -// Basic API call rate limiting +// Basic API call rate limiting with return value const { maybeExecute } = useAsyncRateLimiter( async (id: string) => { const data = await api.fetchData(id); - return data; + return data; // Return value is preserved }, { limit: 5, window: 1000 } // 5 calls per second ); -// With state management +// With state management and return value const [data, setData] = useState(null); const { maybeExecute } = useAsyncRateLimiter( async (query) => { const result = await searchAPI(query); setData(result); + return result; // Return value can be used by the caller }, { limit: 10, diff --git a/docs/framework/react/reference/functions/useratelimitedcallback.md b/docs/framework/react/reference/functions/useratelimitedcallback.md index 32f179b97..7b1287a3f 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: [react-pacer/src/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:59](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts#L59) A React hook that creates a rate-limited version of a callback function. This hook is essentially a wrapper around the basic `rateLimiter` function @@ -24,6 +24,12 @@ or debouncing, it does not attempt to space out or intelligently collapse calls. This can lead to bursts of rapid executions followed by periods where all calls are blocked. +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All executions within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows executions as old ones expire. This provides a more + consistent rate of execution over time. + For smoother execution patterns, consider: - useThrottledCallback: When you want consistent spacing between executions (e.g. UI updates) - useDebouncedCallback: When you want to collapse rapid calls into a single execution (e.g. search input) @@ -74,7 +80,7 @@ Consider using the `useRateLimiter` hook instead. ## Example ```tsx -// Rate limit API calls to maximum 5 calls per minute +// Rate limit API calls to maximum 5 calls per minute with a sliding window const makeApiCall = useRateLimitedCallback( (data: ApiData) => { return fetch('/api/endpoint', { method: 'POST', body: JSON.stringify(data) }); @@ -82,6 +88,7 @@ const makeApiCall = useRateLimitedCallback( { limit: 5, window: 60000, // 1 minute + windowType: 'sliding', onReject: () => { console.warn('API rate limit reached. Please wait before trying again.'); } diff --git a/docs/framework/react/reference/functions/useratelimitedstate.md b/docs/framework/react/reference/functions/useratelimitedstate.md index 4686d112a..655e09aa6 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>>] ``` -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) +Defined in: [react-pacer/src/rate-limiter/useRateLimitedState.ts:66](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts#L66) 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. @@ -20,6 +20,12 @@ Rate limiting is a simple "hard limit" approach - it allows all updates until th subsequent updates until the window resets. Unlike throttling or debouncing, it does not attempt to space out or intelligently collapse updates. This can lead to bursts of rapid updates followed by periods of no updates. +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All updates within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows updates as old ones expire. This provides a more + consistent rate of updates over time. + For smoother update patterns, consider: - useThrottledState: When you want consistent spacing between updates (e.g. UI changes) - useDebouncedState: When you want to collapse rapid updates into a single update (e.g. search input) @@ -55,16 +61,18 @@ consider using the lower-level useRateLimiter hook instead. ## Example ```tsx -// Basic rate limiting - update state at most 5 times per minute +// Basic rate limiting - update state at most 5 times per minute with a sliding window const [value, setValue, rateLimiter] = useRateLimitedState(0, { limit: 5, - window: 60000 + window: 60000, + windowType: 'sliding' }); -// With rejection callback +// With rejection callback and fixed window const [value, setValue] = useRateLimitedState(0, { limit: 3, window: 5000, + windowType: 'fixed', onReject: (rateLimiter) => { alert(`Rate limit reached. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); } diff --git a/docs/framework/react/reference/functions/useratelimitedvalue.md b/docs/framework/react/reference/functions/useratelimitedvalue.md index 103c9ef91..2194cd3f1 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>>] ``` -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) +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. @@ -20,6 +20,12 @@ Rate limiting is a simple "hard limit" approach - it allows all updates until th subsequent updates until the window resets. Unlike throttling or debouncing, it does not attempt to space out or intelligently collapse updates. This can lead to bursts of rapid updates followed by periods of no updates. +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All updates within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows updates as old ones expire. This provides a more + consistent rate of updates over time. + For smoother update patterns, consider: - useThrottledValue: When you want consistent spacing between updates (e.g. UI changes) - useDebouncedValue: When you want to collapse rapid updates into a single update (e.g. search input) @@ -54,16 +60,18 @@ consider using the lower-level useRateLimiter hook instead. ## Example ```tsx -// Basic rate limiting - update at most 5 times per minute +// Basic rate limiting - update at most 5 times per minute with a sliding window const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { limit: 5, - window: 60000 + window: 60000, + windowType: 'sliding' }); -// With rejection callback +// With rejection callback and fixed window const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { limit: 3, window: 5000, + windowType: 'fixed', onReject: (rateLimiter) => { console.log(`Update rejected. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); } diff --git a/docs/framework/react/reference/functions/useratelimiter.md b/docs/framework/react/reference/functions/useratelimiter.md index 59953e338..1ca948af9 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: [react-pacer/src/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:55](https://github.com/TanStack/pacer/blob/main/packages/react-pacer/src/rate-limiter/useRateLimiter.ts#L55) A low-level React hook that creates a `RateLimiter` instance to enforce rate limits on function execution. @@ -22,6 +22,12 @@ Rate limiting is a simple "hard limit" approach that allows executions until a m a time window, then blocks all subsequent calls until the window resets. Unlike throttling or debouncing, it does not attempt to space out or collapse executions intelligently. +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All executions within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows executions as old ones expire. This provides a more + consistent rate of execution over time. + For smoother execution patterns: - Use throttling when you want consistent spacing between executions (e.g. UI updates) - Use debouncing when you want to collapse rapid-fire events (e.g. search input) @@ -55,10 +61,11 @@ The hook returns an object containing: ## Example ```tsx -// Basic rate limiting - max 5 calls per minute +// Basic rate limiting - max 5 calls per minute with a sliding window const { maybeExecute } = useRateLimiter(apiCall, { limit: 5, window: 60000, + windowType: 'sliding', }); // Monitor rate limit status diff --git a/docs/framework/solid/reference/functions/createasyncratelimiter.md b/docs/framework/solid/reference/functions/createasyncratelimiter.md index 05d8e82b5..ae8a0eec5 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:62](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L62) +Defined in: [async-rate-limiter/createAsyncRateLimiter.ts:73](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts#L73) A low-level Solid hook that creates an `AsyncRateLimiter` instance to limit how many times an async function can execute within a time window. @@ -22,6 +22,16 @@ Rate limiting allows an async function to execute up to a specified limit within then blocks subsequent calls until the window passes. This is useful for respecting API rate limits, managing resource constraints, or controlling bursts of async operations. +Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function, +making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call +instead of setting the result on a state variable from within the rate-limited function. + +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All executions within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows executions as old ones expire. This provides a more + consistent rate of execution over time. + ## Type Parameters • **TFn** *extends* `AnyAsyncFunction` @@ -43,21 +53,22 @@ managing resource constraints, or controlling bursts of async operations. ## Example ```tsx -// Basic API call rate limiting +// Basic API call rate limiting with return value const { maybeExecute } = createAsyncRateLimiter( async (id: string) => { const data = await api.fetchData(id); - return data; + return data; // Return value is preserved }, { limit: 5, window: 1000 } // 5 calls per second ); -// With state management +// With state management and return value const [data, setData] = createSignal(null); const { maybeExecute } = createAsyncRateLimiter( async (query) => { const result = await searchAPI(query); setData(result); + return result; // Return value can be used by the caller }, { limit: 10, diff --git a/docs/framework/solid/reference/functions/createratelimitedsignal.md b/docs/framework/solid/reference/functions/createratelimitedsignal.md index 2984da2bd..08c6ac2b5 100644 --- a/docs/framework/solid/reference/functions/createratelimitedsignal.md +++ b/docs/framework/solid/reference/functions/createratelimitedsignal.md @@ -11,7 +11,7 @@ title: createRateLimitedSignal 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) +Defined in: [rate-limiter/createRateLimitedSignal.ts:65](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts#L65) A Solid hook that creates a rate-limited state value that enforces a hard limit on state updates within a time window. This hook combines Solid's createSignal with rate limiting functionality to provide controlled state updates. @@ -20,6 +20,12 @@ Rate limiting is a simple "hard limit" approach - it allows all updates until th subsequent updates until the window resets. Unlike throttling or debouncing, it does not attempt to space out or intelligently collapse updates. This can lead to bursts of rapid updates followed by periods of no updates. +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All updates within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows updates as old ones expire. This provides a more + consistent rate of updates over time. + For smoother update patterns, consider: - createThrottledSignal: When you want consistent spacing between updates (e.g. UI changes) - createDebouncedSignal: When you want to collapse rapid updates into a single update (e.g. search input) @@ -55,16 +61,18 @@ consider using the lower-level createRateLimiter hook instead. ## Example ```tsx -// Basic rate limiting - update state at most 5 times per minute +// Basic rate limiting - update state at most 5 times per minute with a sliding window const [value, setValue, rateLimiter] = createRateLimitedSignal(0, { limit: 5, - window: 60000 + window: 60000, + windowType: 'sliding' }); -// With rejection callback +// With rejection callback and fixed window const [value, setValue] = createRateLimitedSignal(0, { limit: 3, window: 5000, + windowType: 'fixed', onReject: (rateLimiter) => { alert(`Rate limit reached. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); } diff --git a/docs/framework/solid/reference/functions/createratelimitedvalue.md b/docs/framework/solid/reference/functions/createratelimitedvalue.md index 3923e208c..071b0c849 100644 --- a/docs/framework/solid/reference/functions/createratelimitedvalue.md +++ b/docs/framework/solid/reference/functions/createratelimitedvalue.md @@ -11,7 +11,7 @@ title: createRateLimitedValue function createRateLimitedValue(value, initialOptions): [Accessor, SolidRateLimiter>] ``` -Defined in: [rate-limiter/createRateLimitedValue.ts:43](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts#L43) +Defined in: [rate-limiter/createRateLimitedValue.ts:50](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts#L50) 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. @@ -20,6 +20,12 @@ Rate limiting is a simple "hard limit" approach - it allows all updates until th subsequent updates until the window resets. Unlike throttling or debouncing, it does not attempt to space out or intelligently collapse updates. This can lead to bursts of rapid updates followed by periods of no updates. +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All updates within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows updates as old ones expire. This provides a more + consistent rate of updates over time. + For smoother update patterns, consider: - createThrottledValue: When you want consistent spacing between updates (e.g. UI changes) - createDebouncedValue: When you want to collapse rapid updates into a single update (e.g. search input) @@ -54,10 +60,11 @@ consider using the lower-level createRateLimiter hook instead. ## Example ```tsx -// Basic rate limiting - update at most 5 times per minute +// Basic rate limiting - update at most 5 times per minute with a sliding window const [rateLimitedValue, rateLimiter] = createRateLimitedValue(rawValue, { limit: 5, - window: 60000 + window: 60000, + windowType: 'sliding' }); // Use the rate-limited value diff --git a/docs/framework/solid/reference/functions/createratelimiter.md b/docs/framework/solid/reference/functions/createratelimiter.md index 65a2faa12..8f65bfa4c 100644 --- a/docs/framework/solid/reference/functions/createratelimiter.md +++ b/docs/framework/solid/reference/functions/createratelimiter.md @@ -11,7 +11,7 @@ title: createRateLimiter function createRateLimiter(fn, initialOptions): SolidRateLimiter ``` -Defined in: [rate-limiter/createRateLimiter.ts:61](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L61) +Defined in: [rate-limiter/createRateLimiter.ts:68](https://github.com/TanStack/pacer/blob/main/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts#L68) A low-level Solid hook that creates a `RateLimiter` instance to enforce rate limits on function execution. @@ -22,6 +22,12 @@ Rate limiting is a simple "hard limit" approach that allows executions until a m a time window, then blocks all subsequent calls until the window resets. Unlike throttling or debouncing, it does not attempt to space out or collapse executions intelligently. +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All executions within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows executions as old ones expire. This provides a more + consistent rate of execution over time. + For smoother execution patterns: - Use throttling when you want consistent spacing between executions (e.g. UI updates) - Use debouncing when you want to collapse rapid-fire events (e.g. search input) @@ -48,10 +54,11 @@ For smoother execution patterns: ## Example ```tsx -// Basic rate limiting - max 5 calls per minute +// Basic rate limiting - max 5 calls per minute with a sliding window const rateLimiter = createRateLimiter(apiCall, { limit: 5, window: 60000, + windowType: 'sliding' }); // Monitor rate limit status diff --git a/docs/guides/debouncing.md b/docs/guides/debouncing.md index c316f36f1..296e34f7c 100644 --- a/docs/guides/debouncing.md +++ b/docs/guides/debouncing.md @@ -199,22 +199,16 @@ The async debouncer provides a powerful way to handle asynchronous operations wi 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 +2. **Different Callbacks** +The `AsyncDebouncer` supports the following callbacks instead of just `onExecute` in the synchronous version: +- `onSuccess`: Called after each successful execution, providing the debouncer instance +- `onSettled`: Called after each execution, providing the debouncer instance +- `onError`: Called if the async function throws an error, providing both the error and the debouncer instance -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) +3. **Sequential Execution** +Since the debouncer's `maybeExecute` method returns a Promise, you can choose to await each execution before starting the next one. This gives you control over the execution order and ensures each call processes the most up-to-date data. This is particularly useful when dealing with operations that depend on the results of previous calls or when maintaining data consistency is critical. -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. +For example, if you're updating a user's profile and then immediately fetching their updated data, you can await the update operation before starting the fetch: #### Basic Usage Example @@ -241,19 +235,6 @@ const debouncedSearch = asyncDebounce( const results = await debouncedSearch('query') ``` -#### 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 Each framework adapter provides hooks that build on top of the core debouncing functionality to integrate with the framework's state management system. Hooks like `createDebouncer`, `useDebouncedCallback`, `useDebouncedState`, or `useDebouncedValue` are available for each framework. diff --git a/docs/guides/rate-limiting.md b/docs/guides/rate-limiting.md index 3fbc417d1..09428a92d 100644 --- a/docs/guides/rate-limiting.md +++ b/docs/guides/rate-limiting.md @@ -23,6 +23,35 @@ Executed: ✅ ✅ ✅ ❌ ❌ [=== 3 allowed ===][=== blocked until window ends ===][=== new window =======] ``` +### Window Types + +TanStack Pacer supports two types of rate limiting windows: + +1. **Fixed Window** (default) + - A strict window that resets after the window period + - All executions within the window count towards the limit + - The window resets completely after the period + - Can lead to bursty behavior at window boundaries + +2. **Sliding Window** + - A rolling window that allows executions as old ones expire + - Provides a more consistent rate of execution over time + - Better for maintaining a steady flow of executions + - Prevents bursty behavior at window boundaries + +Here's a visualization of sliding window rate limiting: + +```text +Sliding Window Rate Limiting (limit: 3 calls per window) +Timeline: [1 second per tick] + Window 1 | Window 2 +Calls: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ +Executed: ✅ ✅ ✅ ❌ ✅ ✅ ✅ + [=== 3 allowed ===][=== oldest expires, new allowed ===][=== continues sliding =======] +``` + +The key difference is that with a sliding window, as soon as the oldest execution expires, a new execution is allowed. This creates a more consistent flow of executions compared to the fixed window approach. + ### When to Use Rate Limiting Rate Limiting is particularly important when dealing with front-end operations that could accidentally overwhelm your back-end services or cause performance issues in the browser. @@ -58,6 +87,7 @@ const rateLimitedApi = rateLimit( { limit: 5, window: 60 * 1000, // 1 minute in milliseconds + windowType: 'fixed', // default onReject: (rateLimiter) => { console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`) } @@ -114,6 +144,9 @@ limiter.reset() The `RateLimiter` class supports enabling/disabling via the `enabled` option. Using the `setOptions` method, you can enable/disable the rate limiter at any time: +> [!NOTE] +> The `enabled` option enables/disables the actual function execution. Disabling the rate limiter does not turn off rate limiting, it just prevents the function from being executed at all. + ```ts const limiter = new RateLimiter(fn, { limit: 5, @@ -186,23 +219,18 @@ The async rate limiter provides a powerful way to handle asynchronous operations 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 +2. **Different Callbacks** +The `AsyncRateLimiter` supports the following callbacks instead of just `onExecute` in the synchronous version: +- `onSuccess`: Called after each successful execution, providing the rate limiter instance +- `onSettled`: Called after each execution, 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 +Both the Async and Synchronous rate limiters support the `onReject` callback for handling blocked executions. -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. +3. **Sequential Execution** +Since the rate limiter's `maybeExecute` method returns a Promise, you can choose to await each execution before starting the next one. This gives you control over the execution order and ensures each call processes the most up-to-date data. This is particularly useful 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. +For example, if you're updating a user's profile and then immediately fetching their updated data, you can await the update operation before starting the fetch: #### Basic Usage Example @@ -233,19 +261,6 @@ const rateLimitedApi = asyncRateLimit( const result = await rateLimitedApi('123') ``` -#### 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 Each framework adapter provides hooks that build on top of the core rate limiting functionality to integrate with the framework's state management system. Hooks like `createRateLimiter`, `useRateLimitedCallback`, `useRateLimitedState`, or `useRateLimitedValue` are available for each framework. diff --git a/docs/guides/throttling.md b/docs/guides/throttling.md index 0f0827bd3..feb76deff 100644 --- a/docs/guides/throttling.md +++ b/docs/guides/throttling.md @@ -175,22 +175,18 @@ The async throttler provides a powerful way to handle asynchronous operations wi 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 +2. **Different Callbacks** +The `AsyncThrottler` supports the following callbacks instead of just `onExecute` in the synchronous version: +- `onSuccess`: Called after each successful execution, providing the throttler instance +- `onSettled`: Called after each execution, providing the throttler instance +- `onError`: Called if the async function throws an error, providing both the error and the throttler instance -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) +Both the Async and Synchronous throttlers support the `onExecute` callback for handling successful executions. -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. +3. **Sequential Execution** +Since the throttler's `maybeExecute` method returns a Promise, you can choose to await each execution before starting the next one. This gives you control over the execution order and ensures each call processes the most up-to-date data. This is particularly useful 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. +For example, if you're updating a user's profile and then immediately fetching their updated data, you can await the update operation before starting the fetch: #### Basic Usage Example @@ -217,19 +213,6 @@ const throttledSearch = asyncThrottle( const results = await throttledSearch('query') ``` -#### 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 Each framework adapter provides hooks that build on top of the core throttling functionality to integrate with the framework's state management system. Hooks like `createThrottler`, `useThrottledCallback`, `useThrottledState`, or `useThrottledValue` are available for each framework. diff --git a/docs/reference/classes/asyncdebouncer.md b/docs/reference/classes/asyncdebouncer.md index f94b7331b..d1e03c5b5 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:73](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L73) +Defined in: [async-debouncer.ts:77](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L77) A class that creates an async debounced function. @@ -18,17 +18,21 @@ or input changes where you only want to execute the handler after the events hav Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until the function stops being called for the specified delay period. +Unlike the non-async Debouncer, this async version supports returning values from the debounced function, +making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call +instead of setting the result on a state variable from within the debounced function. + ## Example ```ts const asyncDebouncer = new AsyncDebouncer(async (value: string) => { - await searchAPI(value); + const results = await searchAPI(value); + return results; // Return value is preserved }, { wait: 500 }); // Called on each keystroke but only executes after 500ms of no typing -inputElement.addEventListener('input', () => { - asyncDebouncer.maybeExecute(inputElement.value); -}); +// Returns the API response directly +const results = await asyncDebouncer.maybeExecute(inputElement.value); ``` ## Type Parameters @@ -43,7 +47,7 @@ inputElement.addEventListener('input', () => { new AsyncDebouncer(fn, initialOptions): AsyncDebouncer ``` -Defined in: [async-debouncer.ts:86](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L86) +Defined in: [async-debouncer.ts:90](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L90) #### Parameters @@ -67,7 +71,7 @@ Defined in: [async-debouncer.ts:86](https://github.com/TanStack/pacer/blob/main/ cancel(): void ``` -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:199](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L199) Cancels any pending execution or aborts any execution in progress @@ -83,7 +87,7 @@ Cancels any pending execution or aborts any execution in progress getErrorCount(): number ``` -Defined in: [async-debouncer.ts:224](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L224) +Defined in: [async-debouncer.ts:228](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L228) Returns the number of times the function has errored @@ -99,7 +103,7 @@ Returns the number of times the function has errored getIsExecuting(): boolean ``` -Defined in: [async-debouncer.ts:238](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L238) +Defined in: [async-debouncer.ts:242](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L242) Returns `true` if there is currently an execution in progress @@ -115,7 +119,7 @@ Returns `true` if there is currently an execution in progress getIsPending(): boolean ``` -Defined in: [async-debouncer.ts:231](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L231) +Defined in: [async-debouncer.ts:235](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L235) Returns `true` if there is a pending execution queued up for trailing execution @@ -131,7 +135,7 @@ Returns `true` if there is a pending execution queued up for trailing execution getLastResult(): undefined | ReturnType ``` -Defined in: [async-debouncer.ts:203](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L203) +Defined in: [async-debouncer.ts:207](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L207) Returns the last result of the debounced function @@ -147,7 +151,7 @@ Returns the last result of the debounced function getOptions(): Required> ``` -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:116](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L116) Returns the current debouncer options @@ -163,7 +167,7 @@ Returns the current debouncer options getSettleCount(): number ``` -Defined in: [async-debouncer.ts:217](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L217) +Defined in: [async-debouncer.ts:221](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L221) Returns the number of times the function has settled (completed or errored) @@ -179,7 +183,7 @@ Returns the number of times the function has settled (completed or errored) getSuccessCount(): number ``` -Defined in: [async-debouncer.ts:210](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L210) +Defined in: [async-debouncer.ts:214](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L214) Returns the number of times the function has been executed successfully @@ -195,7 +199,7 @@ Returns the number of times the function has been executed successfully maybeExecute(...args): Promise> ``` -Defined in: [async-debouncer.ts:120](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L120) +Defined in: [async-debouncer.ts:124](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L124) Attempts to execute the debounced function If a call is already in progress, it will be queued @@ -218,7 +222,7 @@ If a call is already in progress, it will be queued setOptions(newOptions): void ``` -Defined in: [async-debouncer.ts:100](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L100) +Defined in: [async-debouncer.ts:104](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L104) Updates the debouncer options Returns the new options state diff --git a/docs/reference/classes/asyncratelimiter.md b/docs/reference/classes/asyncratelimiter.md index 4c551ff76..e187e04eb 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:76](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L76) +Defined in: [async-rate-limiter.ts:95](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L95) A class that creates an async rate-limited function. @@ -15,6 +15,16 @@ Rate limiting is a simple approach that allows a function to execute up to a lim then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where all executions happen immediately, followed by a complete block. +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All executions within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows executions as old ones expire. This provides a more + consistent rate of execution over time. + +Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function, +making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call +instead of setting the result on a state variable from within the rate-limited function. + For smoother execution patterns, consider using: - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms) - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls) @@ -27,11 +37,12 @@ smoothing out frequent events, throttling or debouncing usually provide better u ```ts const rateLimiter = new AsyncRateLimiter( async (id: string) => await api.getData(id), - { limit: 5, window: 1000 } // 5 calls per second + { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window ); // Will execute immediately until limit reached, then block -await rateLimiter.maybeExecute('123'); +// Returns the API response directly +const data = await rateLimiter.maybeExecute('123'); ``` ## Type Parameters @@ -46,7 +57,7 @@ await rateLimiter.maybeExecute('123'); new AsyncRateLimiter(fn, initialOptions): AsyncRateLimiter ``` -Defined in: [async-rate-limiter.ts:85](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L85) +Defined in: [async-rate-limiter.ts:105](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L105) #### Parameters @@ -70,7 +81,7 @@ Defined in: [async-rate-limiter.ts:85](https://github.com/TanStack/pacer/blob/ma getErrorCount(): number ``` -Defined in: [async-rate-limiter.ts:209](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L209) +Defined in: [async-rate-limiter.ts:250](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L250) Returns the number of times the function has errored @@ -80,15 +91,33 @@ Returns the number of times the function has errored *** +### getIsExecuting() + +```ts +getIsExecuting(): boolean +``` + +Defined in: [async-rate-limiter.ts:264](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L264) + +Returns whether the function is currently executing + +#### Returns + +`boolean` + +*** + ### getMsUntilNextWindow() ```ts getMsUntilNextWindow(): number ``` -Defined in: [async-rate-limiter.ts:188](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L188) +Defined in: [async-rate-limiter.ts:225](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L225) Returns the number of milliseconds until the next execution will be possible +For fixed windows, this is the time until the current window resets +For sliding windows, this is the time until the oldest execution expires #### Returns @@ -102,7 +131,7 @@ Returns the number of milliseconds until the next execution will be possible getOptions(): Required> ``` -Defined in: [async-rate-limiter.ts:106](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L106) +Defined in: [async-rate-limiter.ts:126](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L126) Returns the current rate limiter options @@ -118,7 +147,7 @@ Returns the current rate limiter options getRejectionCount(): number ``` -Defined in: [async-rate-limiter.ts:216](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L216) +Defined in: [async-rate-limiter.ts:257](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L257) Returns the number of times the function has been rejected @@ -134,7 +163,7 @@ Returns the number of times the function has been rejected getRemainingInWindow(): number ``` -Defined in: [async-rate-limiter.ts:180](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L180) +Defined in: [async-rate-limiter.ts:215](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L215) Returns the number of remaining executions allowed in the current window @@ -150,7 +179,7 @@ Returns the number of remaining executions allowed in the current window getSettleCount(): number ``` -Defined in: [async-rate-limiter.ts:202](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L202) +Defined in: [async-rate-limiter.ts:243](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L243) Returns the number of times the function has been settled @@ -166,7 +195,7 @@ Returns the number of times the function has been settled getSuccessCount(): number ``` -Defined in: [async-rate-limiter.ts:195](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L195) +Defined in: [async-rate-limiter.ts:236](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L236) Returns the number of times the function has been executed @@ -182,7 +211,7 @@ Returns the number of times the function has been executed maybeExecute(...args): Promise> ``` -Defined in: [async-rate-limiter.ts:126](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L126) +Defined in: [async-rate-limiter.ts:146](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L146) 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. @@ -218,7 +247,7 @@ await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected reset(): void ``` -Defined in: [async-rate-limiter.ts:223](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L223) +Defined in: [async-rate-limiter.ts:271](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L271) Resets the rate limiter state @@ -234,7 +263,7 @@ Resets the rate limiter state setOptions(newOptions): void ``` -Defined in: [async-rate-limiter.ts:99](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L99) +Defined in: [async-rate-limiter.ts:119](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L119) Updates the rate limiter options Returns the new options state diff --git a/docs/reference/classes/asyncthrottler.md b/docs/reference/classes/asyncthrottler.md index b3edb067b..c98c307f8 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:76](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L76) +Defined in: [async-throttler.ts:80](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L80) A class that creates an async throttled function. @@ -15,6 +15,10 @@ Throttling limits how often a function can be executed, allowing only one execut Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a regular interval regardless of how often it's called. +Unlike the non-async Throttler, this async version supports returning values from the throttled function, +making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call +instead of setting the result on a state variable from within the throttled function. + This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to ensure a maximum execution frequency. @@ -22,13 +26,13 @@ ensure a maximum execution frequency. ```ts const throttler = new AsyncThrottler(async (value: string) => { - await saveToAPI(value); + const result = await saveToAPI(value); + return result; // Return value is preserved }, { wait: 1000 }); // Will only execute once per second no matter how often called -inputElement.addEventListener('input', () => { - throttler.maybeExecute(inputElement.value); -}); +// Returns the API response directly +const result = await throttler.maybeExecute(inputElement.value); ``` ## Type Parameters @@ -43,7 +47,7 @@ inputElement.addEventListener('input', () => { new AsyncThrottler(fn, initialOptions): AsyncThrottler ``` -Defined in: [async-throttler.ts:89](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L89) +Defined in: [async-throttler.ts:93](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L93) #### Parameters @@ -67,7 +71,7 @@ Defined in: [async-throttler.ts:89](https://github.com/TanStack/pacer/blob/main/ cancel(): void ``` -Defined in: [async-throttler.ts:187](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L187) +Defined in: [async-throttler.ts:191](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L191) Cancels any pending execution or aborts any execution in progress @@ -83,7 +87,7 @@ Cancels any pending execution or aborts any execution in progress getErrorCount(): number ``` -Defined in: [async-throttler.ts:237](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L237) +Defined in: [async-throttler.ts:241](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L241) Returns the number of times the function has errored @@ -99,7 +103,7 @@ Returns the number of times the function has errored getIsExecuting(): boolean ``` -Defined in: [async-throttler.ts:251](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L251) +Defined in: [async-throttler.ts:255](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L255) Returns the current executing state @@ -115,7 +119,7 @@ Returns the current executing state getIsPending(): boolean ``` -Defined in: [async-throttler.ts:244](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L244) +Defined in: [async-throttler.ts:248](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L248) Returns the current pending state @@ -131,7 +135,7 @@ Returns the current pending state getLastExecutionTime(): number ``` -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:206](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L206) Returns the last execution time @@ -147,7 +151,7 @@ Returns the last execution time getLastResult(): undefined | ReturnType ``` -Defined in: [async-throttler.ts:216](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L216) +Defined in: [async-throttler.ts:220](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L220) Returns the last result of the debounced function @@ -163,7 +167,7 @@ Returns the last result of the debounced function getNextExecutionTime(): number ``` -Defined in: [async-throttler.ts:209](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L209) +Defined in: [async-throttler.ts:213](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L213) Returns the next execution time @@ -179,7 +183,7 @@ Returns the next execution time getOptions(): Required> ``` -Defined in: [async-throttler.ts:115](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L115) +Defined in: [async-throttler.ts:119](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L119) Returns the current options @@ -195,7 +199,7 @@ Returns the current options getSettleCount(): number ``` -Defined in: [async-throttler.ts:230](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L230) +Defined in: [async-throttler.ts:234](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L234) Returns the number of times the function has settled (completed or errored) @@ -211,7 +215,7 @@ Returns the number of times the function has settled (completed or errored) getSuccessCount(): number ``` -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:227](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L227) Returns the number of times the function has been executed successfully @@ -227,7 +231,7 @@ Returns the number of times the function has been executed successfully maybeExecute(...args): Promise> ``` -Defined in: [async-throttler.ts:123](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L123) +Defined in: [async-throttler.ts:127](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L127) Attempts to execute the throttled function If a call is already in progress, it may be blocked or queued depending on the `wait` option @@ -250,7 +254,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:103](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L103) +Defined in: [async-throttler.ts:107](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L107) Updates the throttler options Returns the new options state diff --git a/docs/reference/classes/ratelimiter.md b/docs/reference/classes/ratelimiter.md index d690bae43..02c26f09c 100644 --- a/docs/reference/classes/ratelimiter.md +++ b/docs/reference/classes/ratelimiter.md @@ -7,7 +7,7 @@ title: RateLimiter # Class: RateLimiter\ -Defined in: [rate-limiter.ts:63](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L63) +Defined in: [rate-limiter.ts:77](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L77) A class that creates a rate-limited function. @@ -15,6 +15,12 @@ Rate limiting is a simple approach that allows a function to execute up to a lim then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where all executions happen immediately, followed by a complete block. +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All executions within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows executions as old ones expire. This provides a more + consistent rate of execution over time. + For smoother execution patterns, consider using: - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms) - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls) @@ -27,7 +33,7 @@ smoothing out frequent events, throttling or debouncing usually provide better u ```ts const rateLimiter = new RateLimiter( (id: string) => api.getData(id), - { limit: 5, window: 1000 } // 5 calls per second + { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window ); // Will execute immediately until limit reached, then block @@ -46,7 +52,7 @@ rateLimiter.maybeExecute('123'); new RateLimiter(fn, initialOptions): RateLimiter ``` -Defined in: [rate-limiter.ts:69](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L69) +Defined in: [rate-limiter.ts:83](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L83) #### Parameters @@ -70,7 +76,7 @@ Defined in: [rate-limiter.ts:69](https://github.com/TanStack/pacer/blob/main/pac getExecutionCount(): number ``` -Defined in: [rate-limiter.ts:149](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L149) +Defined in: [rate-limiter.ts:175](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L175) Returns the number of times the function has been executed @@ -86,7 +92,7 @@ Returns the number of times the function has been executed getMsUntilNextWindow(): number ``` -Defined in: [rate-limiter.ts:171](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L171) +Defined in: [rate-limiter.ts:197](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L197) Returns the number of milliseconds until the next execution will be possible @@ -102,7 +108,7 @@ Returns the number of milliseconds until the next execution will be possible getOptions(): Required> ``` -Defined in: [rate-limiter.ts:90](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L90) +Defined in: [rate-limiter.ts:104](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L104) Returns the current rate limiter options @@ -118,7 +124,7 @@ Returns the current rate limiter options getRejectionCount(): number ``` -Defined in: [rate-limiter.ts:156](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L156) +Defined in: [rate-limiter.ts:182](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L182) Returns the number of times the function has been rejected @@ -134,7 +140,7 @@ Returns the number of times the function has been rejected getRemainingInWindow(): number ``` -Defined in: [rate-limiter.ts:163](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L163) +Defined in: [rate-limiter.ts:189](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L189) Returns the number of remaining executions allowed in the current window @@ -150,7 +156,7 @@ Returns the number of remaining executions allowed in the current window maybeExecute(...args): boolean ``` -Defined in: [rate-limiter.ts:109](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L109) +Defined in: [rate-limiter.ts:123](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L123) 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. @@ -185,7 +191,7 @@ rateLimiter.maybeExecute('arg1', 'arg2'); // false reset(): void ``` -Defined in: [rate-limiter.ts:179](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L179) +Defined in: [rate-limiter.ts:208](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L208) Resets the rate limiter state @@ -201,7 +207,7 @@ Resets the rate limiter state setOptions(newOptions): void ``` -Defined in: [rate-limiter.ts:83](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L83) +Defined in: [rate-limiter.ts:97](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L97) Updates the rate limiter options Returns the new options state diff --git a/docs/reference/functions/asyncdebounce.md b/docs/reference/functions/asyncdebounce.md index 9c42ae372..5ca82bc30 100644 --- a/docs/reference/functions/asyncdebounce.md +++ b/docs/reference/functions/asyncdebounce.md @@ -11,12 +11,16 @@ title: asyncDebounce function asyncDebounce(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-debouncer.ts:260](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L260) +Defined in: [async-debouncer.ts:268](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-debouncer.ts#L268) 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. If called again during the wait period, the timer resets and a new wait period begins. +Unlike the non-async Debouncer, this async version supports returning values from the debounced function, +making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call +instead of setting the result on a state variable from within the debounced function. + ## Type Parameters • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) @@ -52,11 +56,11 @@ If a call is already in progress, it will be queued ```ts const debounced = asyncDebounce(async (value: string) => { - await saveToAPI(value); + const result = await saveToAPI(value); + return result; // Return value is preserved }, { wait: 1000 }); // Will only execute once, 1 second after the last call -await debounced("first"); // Cancelled -await debounced("second"); // Cancelled -await debounced("third"); // Executes after 1s +// Returns the API response directly +const result = await debounced("third"); ``` diff --git a/docs/reference/functions/asyncratelimit.md b/docs/reference/functions/asyncratelimit.md index 1b58d0bc7..484866310 100644 --- a/docs/reference/functions/asyncratelimit.md +++ b/docs/reference/functions/asyncratelimit.md @@ -11,10 +11,20 @@ title: asyncRateLimit function asyncRateLimit(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-rate-limiter.ts:262](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L262) +Defined in: [async-rate-limiter.ts:322](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L322) Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window. +Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function, +making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call +instead of setting the result on a state variable from within the rate-limited function. + +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All executions within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows executions as old ones expire. This provides a more + consistent rate of execution over time. + Note that rate limiting is a simpler form of execution control compared to throttling or debouncing: - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets - A throttler ensures even spacing between executions, which can be better for consistent performance @@ -70,10 +80,11 @@ await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected ## Example ```ts -// Rate limit to 5 calls per minute +// Rate limit to 5 calls per minute with a sliding window const rateLimited = asyncRateLimit(makeApiCall, { limit: 5, window: 60000, + windowType: 'sliding', onReject: (rateLimiter) => { console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); } @@ -81,7 +92,8 @@ const rateLimited = asyncRateLimit(makeApiCall, { // First 5 calls will execute immediately // Additional calls will be rejected until the minute window resets -await rateLimited(); +// Returns the API response directly +const result = await rateLimited(); // For more even execution, consider using throttle instead: const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds diff --git a/docs/reference/functions/asyncthrottle.md b/docs/reference/functions/asyncthrottle.md index 465db7b44..6203fb2e3 100644 --- a/docs/reference/functions/asyncthrottle.md +++ b/docs/reference/functions/asyncthrottle.md @@ -11,12 +11,16 @@ title: asyncThrottle function asyncThrottle(fn, initialOptions): (...args) => Promise> ``` -Defined in: [async-throttler.ts:272](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L272) +Defined in: [async-throttler.ts:281](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-throttler.ts#L281) 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. If called while executing, it will wait until execution completes before scheduling the next call. +Unlike the non-async Throttler, this async version supports returning values from the throttled function, +making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call +instead of setting the result on a state variable from within the throttled function. + ## Type Parameters • **TFn** *extends* [`AnyAsyncFunction`](../type-aliases/anyasyncfunction.md) @@ -51,11 +55,12 @@ If a call is already in progress, it may be blocked or queued depending on the ` ## Example ```ts -const throttled = asyncThrottle(async () => { - await someAsyncOperation(); +const throttled = asyncThrottle(async (value: string) => { + const result = await saveToAPI(value); + return result; // Return value is preserved }, { wait: 1000 }); // This will execute at most once per second -await throttled(); -await throttled(); // Waits 1 second before executing +// Returns the API response directly +const result = await throttled(inputElement.value); ``` diff --git a/docs/reference/functions/ratelimit.md b/docs/reference/functions/ratelimit.md index 0c6836671..861744ad1 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:216](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L216) +Defined in: [rate-limiter.ts:252](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L252) Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window. @@ -20,6 +20,12 @@ Note that rate limiting is a simpler form of execution control compared to throt - A throttler ensures even spacing between executions, which can be better for consistent performance - A debouncer collapses multiple calls into one, which is better for handling bursts of events +The rate limiter supports two types of windows: +- 'fixed': A strict window that resets after the window period. All executions within the window count + towards the limit, and the window resets completely after the period. +- 'sliding': A rolling window that allows executions as old ones expire. This provides a more + consistent rate of execution over time. + Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically need to enforce a hard limit on the number of executions within a time period. @@ -69,10 +75,11 @@ rateLimiter.maybeExecute('arg1', 'arg2'); // false ## Example ```ts -// Rate limit to 5 calls per minute +// Rate limit to 5 calls per minute with a sliding window const rateLimited = rateLimit(makeApiCall, { limit: 5, window: 60000, + windowType: 'sliding', onReject: (rateLimiter) => { console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); } diff --git a/docs/reference/interfaces/asyncratelimiteroptions.md b/docs/reference/interfaces/asyncratelimiteroptions.md index 0319b4d6c..4ee2c027f 100644 --- a/docs/reference/interfaces/asyncratelimiteroptions.md +++ b/docs/reference/interfaces/asyncratelimiteroptions.md @@ -147,3 +147,18 @@ window: number; 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 + +*** + +### windowType? + +```ts +optional windowType: "fixed" | "sliding"; +``` + +Defined in: [async-rate-limiter.ts:45](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/async-rate-limiter.ts#L45) + +Type of window to use for rate limiting +- 'fixed': Uses a fixed window that resets after the window period +- 'sliding': Uses a sliding window that allows executions as old ones expire +Defaults to 'fixed' diff --git a/docs/reference/interfaces/ratelimiteroptions.md b/docs/reference/interfaces/ratelimiteroptions.md index 8958fffd6..c1bc83408 100644 --- a/docs/reference/interfaces/ratelimiteroptions.md +++ b/docs/reference/interfaces/ratelimiteroptions.md @@ -95,3 +95,18 @@ window: number; 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 + +*** + +### windowType? + +```ts +optional windowType: "fixed" | "sliding"; +``` + +Defined in: [rate-limiter.ts:34](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/rate-limiter.ts#L34) + +Type of window to use for rate limiting +- 'fixed': Uses a fixed window that resets after the window period +- 'sliding': Uses a sliding window that allows executions as old ones expire +Defaults to 'fixed' diff --git a/examples/react/asyncDebounce/package.json b/examples/react/asyncDebounce/package.json index 29d90ad12..115a244d3 100644 --- a/examples/react/asyncDebounce/package.json +++ b/examples/react/asyncDebounce/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/asyncRateLimit/package.json b/examples/react/asyncRateLimit/package.json index e9c033b88..873a3198d 100644 --- a/examples/react/asyncRateLimit/package.json +++ b/examples/react/asyncRateLimit/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/asyncRateLimit/src/index.tsx b/examples/react/asyncRateLimit/src/index.tsx index a845428e5..d4f0590f4 100644 --- a/examples/react/asyncRateLimit/src/index.tsx +++ b/examples/react/asyncRateLimit/src/index.tsx @@ -35,6 +35,7 @@ function SearchApp() { { limit: 5, window: 5000, + // windowType: 'sliding', // default is 'fixed' onReject: (rateLimiter) => { console.log( `Rate limit reached. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`, diff --git a/examples/react/asyncThrottle/package.json b/examples/react/asyncThrottle/package.json index 93d7b37d6..f98835829 100644 --- a/examples/react/asyncThrottle/package.json +++ b/examples/react/asyncThrottle/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/debounce/package.json b/examples/react/debounce/package.json index 4749b3404..3e109a8d9 100644 --- a/examples/react/debounce/package.json +++ b/examples/react/debounce/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/queue/package.json b/examples/react/queue/package.json index 6aaabcb8a..5bf0a5926 100644 --- a/examples/react/queue/package.json +++ b/examples/react/queue/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/rateLimit/package.json b/examples/react/rateLimit/package.json index f4097ec53..bf7a1e168 100644 --- a/examples/react/rateLimit/package.json +++ b/examples/react/rateLimit/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/rateLimit/src/index.tsx b/examples/react/rateLimit/src/index.tsx index dc67fd4d4..79157ec6f 100644 --- a/examples/react/rateLimit/src/index.tsx +++ b/examples/react/rateLimit/src/index.tsx @@ -12,6 +12,7 @@ function App1() { rateLimit(setRateLimitedCount, { limit: 5, window: 5000, + // windowType: 'sliding', // default is 'fixed' onReject: (rateLimiter) => console.log( 'Rejected by rate limiter', diff --git a/examples/react/react-query-debounced-prefetch/package.json b/examples/react/react-query-debounced-prefetch/package.json index a0b945437..12fd660f2 100644 --- a/examples/react/react-query-debounced-prefetch/package.json +++ b/examples/react/react-query-debounced-prefetch/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "@tanstack/react-query": "^5.75.2", "@tanstack/react-query-devtools": "^5.75.2", "react": "^19.1.0", diff --git a/examples/react/react-query-queued-prefetch/package.json b/examples/react/react-query-queued-prefetch/package.json index 6ce1f3513..2b77e6a31 100644 --- a/examples/react/react-query-queued-prefetch/package.json +++ b/examples/react/react-query-queued-prefetch/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "@tanstack/react-query": "^5.75.2", "@tanstack/react-query-devtools": "^5.75.2", "react": "^19.1.0", diff --git a/examples/react/react-query-throttled-prefetch/package.json b/examples/react/react-query-throttled-prefetch/package.json index 02eb2f4a4..6113d8c85 100644 --- a/examples/react/react-query-throttled-prefetch/package.json +++ b/examples/react/react-query-throttled-prefetch/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "@tanstack/react-query": "^5.75.2", "@tanstack/react-query-devtools": "^5.75.2", "react": "^19.1.0", diff --git a/examples/react/throttle/package.json b/examples/react/throttle/package.json index f80f16bac..ada409a01 100644 --- a/examples/react/throttle/package.json +++ b/examples/react/throttle/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useAsyncDebouncer/package.json b/examples/react/useAsyncDebouncer/package.json index 1fc2be346..bd8c7bdcc 100644 --- a/examples/react/useAsyncDebouncer/package.json +++ b/examples/react/useAsyncDebouncer/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useAsyncQueuedState/package.json b/examples/react/useAsyncQueuedState/package.json index 918d8915f..014a4f88c 100644 --- a/examples/react/useAsyncQueuedState/package.json +++ b/examples/react/useAsyncQueuedState/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useAsyncQueuer/package.json b/examples/react/useAsyncQueuer/package.json index 0f05f25a2..58af88c6e 100644 --- a/examples/react/useAsyncQueuer/package.json +++ b/examples/react/useAsyncQueuer/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useAsyncRateLimiter/package.json b/examples/react/useAsyncRateLimiter/package.json index 78a1dfab8..0e9e629de 100644 --- a/examples/react/useAsyncRateLimiter/package.json +++ b/examples/react/useAsyncRateLimiter/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useAsyncRateLimiter/src/index.tsx b/examples/react/useAsyncRateLimiter/src/index.tsx index f1e49a94a..391ad2b02 100644 --- a/examples/react/useAsyncRateLimiter/src/index.tsx +++ b/examples/react/useAsyncRateLimiter/src/index.tsx @@ -9,7 +9,7 @@ interface SearchResult { // Simulate API call with fake data const fakeApi = async (term: string): Promise> => { - await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 300)) // Simulate network delay return [ { id: 1, title: `${term} result ${Math.floor(Math.random() * 100)}` }, { id: 2, title: `${term} result ${Math.floor(Math.random() * 100)}` }, @@ -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 rate limited @@ -32,13 +31,8 @@ function App() { // throw new Error('Test error') // you don't have to catch errors here (though you still can). The onError optional handler will catch it - if (!results.length) { - setIsLoading(true) - } - const data = await fakeApi(term) setResults(data) - setIsLoading(false) setError(null) console.log(setSearchAsyncRateLimiter.getSuccessCount()) @@ -46,8 +40,14 @@ function App() { // hook that gives you an async rate limiter instance const setSearchAsyncRateLimiter = useAsyncRateLimiter(handleSearch, { - limit: 2, // Maximum 2 requests - window: 1000, // per 1 second + // windowType: 'sliding', // default is 'fixed' + limit: 3, // Maximum 2 requests + window: 3000, // per 1 second + onReject: (rateLimiter) => { + console.log( + `Rate limit reached. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`, + ) + }, onError: (error) => { // optional error handler console.error('Search failed:', error) @@ -90,15 +90,38 @@ function App() { {error &&
Error: {error.message}
}
-

API calls made: {setSearchAsyncRateLimiter.getSuccessCount()}

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

Loading...

} + + + + + + + + + + + + + + + + + + + +
API calls made:{setSearchAsyncRateLimiter.getSuccessCount()}
Rejected calls:{setSearchAsyncRateLimiter.getRejectionCount()}
Is executing: + {setSearchAsyncRateLimiter.getIsExecuting() ? 'Yes' : 'No'} +
Results: + {results.length > 0 ? ( +
    + {results.map((item) => ( +
  • {item.title}
  • + ))} +
+ ) : ( + 'No results' + )} +
) diff --git a/examples/react/useAsyncThrottler/package.json b/examples/react/useAsyncThrottler/package.json index dddf45c42..90dfd58a8 100644 --- a/examples/react/useAsyncThrottler/package.json +++ b/examples/react/useAsyncThrottler/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useDebouncedCallback/package.json b/examples/react/useDebouncedCallback/package.json index 81bbae6df..eed0a4ba1 100644 --- a/examples/react/useDebouncedCallback/package.json +++ b/examples/react/useDebouncedCallback/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useDebouncedState/package.json b/examples/react/useDebouncedState/package.json index da24b6808..64e39fa98 100644 --- a/examples/react/useDebouncedState/package.json +++ b/examples/react/useDebouncedState/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useDebouncedValue/package.json b/examples/react/useDebouncedValue/package.json index 11b2626df..3ca616cf0 100644 --- a/examples/react/useDebouncedValue/package.json +++ b/examples/react/useDebouncedValue/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useDebouncer/package.json b/examples/react/useDebouncer/package.json index 90fc6abe0..ce8db7979 100644 --- a/examples/react/useDebouncer/package.json +++ b/examples/react/useDebouncer/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useQueuedState/package.json b/examples/react/useQueuedState/package.json index 92ec26925..93ba7228b 100644 --- a/examples/react/useQueuedState/package.json +++ b/examples/react/useQueuedState/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useQueuedValue/package.json b/examples/react/useQueuedValue/package.json index a4fb172f7..82409aef2 100644 --- a/examples/react/useQueuedValue/package.json +++ b/examples/react/useQueuedValue/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useQueuer/package.json b/examples/react/useQueuer/package.json index 824bb7105..32ccd2332 100644 --- a/examples/react/useQueuer/package.json +++ b/examples/react/useQueuer/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useRateLimitedCallback/package.json b/examples/react/useRateLimitedCallback/package.json index eacfd7061..309cbc4f2 100644 --- a/examples/react/useRateLimitedCallback/package.json +++ b/examples/react/useRateLimitedCallback/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useRateLimitedState/package.json b/examples/react/useRateLimitedState/package.json index 7df1acfdc..49ce08909 100644 --- a/examples/react/useRateLimitedState/package.json +++ b/examples/react/useRateLimitedState/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useRateLimitedValue/package.json b/examples/react/useRateLimitedValue/package.json index 5e069c98e..89aeebc8f 100644 --- a/examples/react/useRateLimitedValue/package.json +++ b/examples/react/useRateLimitedValue/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useRateLimiter/package.json b/examples/react/useRateLimiter/package.json index 877bb4c9d..2f951a8ea 100644 --- a/examples/react/useRateLimiter/package.json +++ b/examples/react/useRateLimiter/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useRateLimiter/src/index.tsx b/examples/react/useRateLimiter/src/index.tsx index 67661ef90..fa5138ab3 100644 --- a/examples/react/useRateLimiter/src/index.tsx +++ b/examples/react/useRateLimiter/src/index.tsx @@ -9,9 +9,10 @@ function App1() { // Using useRateLimiter with a rate limit of 5 executions per 5 seconds const rateLimiter = useRateLimiter(setLimitedCount, { - enabled: instantCount > 2, + // enabled: instantCount > 2, limit: 5, window: 5000, + // windowType: 'sliding', // default is 'fixed' onReject: (rateLimiter) => console.log( 'Rejected by rate limiter', @@ -41,6 +42,19 @@ function App1() { Rejection Count: {rateLimiter.getRejectionCount()} + + Remaining in Window: + {rateLimiter.getRemainingInWindow()} + + + Ms Until Next Window: + {rateLimiter.getMsUntilNextWindow()} + + + +
+ + Instant Count: {instantCount} @@ -53,10 +67,7 @@ function App1() {
- - +
) @@ -71,6 +82,7 @@ function App2() { enabled: instantSearch.length > 2, // optional, defaults to true limit: 5, window: 5000, + // windowType: 'sliding', // default is 'fixed' onReject: (rateLimiter) => console.log( 'Rejected by rate limiter', @@ -107,9 +119,21 @@ function App2() { Rejection Count: {rateLimiter.getRejectionCount()} + + Remaining in Window: + {rateLimiter.getRemainingInWindow()} + + + Ms Until Next Window: + {rateLimiter.getMsUntilNextWindow()} + + + +
+ + Instant Search: - {instantSearch} Rate Limited Search: @@ -118,10 +142,7 @@ function App2() {
- - +
) diff --git a/examples/react/useThrottledCallback/package.json b/examples/react/useThrottledCallback/package.json index 7dff8590d..b69c30eb5 100644 --- a/examples/react/useThrottledCallback/package.json +++ b/examples/react/useThrottledCallback/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useThrottledState/package.json b/examples/react/useThrottledState/package.json index 282b3ad72..9b4049966 100644 --- a/examples/react/useThrottledState/package.json +++ b/examples/react/useThrottledState/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useThrottledValue/package.json b/examples/react/useThrottledValue/package.json index fe3ce9b31..b4345c2ef 100644 --- a/examples/react/useThrottledValue/package.json +++ b/examples/react/useThrottledValue/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/react/useThrottler/package.json b/examples/react/useThrottler/package.json index ea1f140f2..6f31b2bea 100644 --- a/examples/react/useThrottler/package.json +++ b/examples/react/useThrottler/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-pacer": "^0.3.0", + "@tanstack/react-pacer": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/solid/asyncDebounce/package.json b/examples/solid/asyncDebounce/package.json index 80253679a..3395d7f73 100644 --- a/examples/solid/asyncDebounce/package.json +++ b/examples/solid/asyncDebounce/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/asyncRateLimit/package.json b/examples/solid/asyncRateLimit/package.json index 2f2390f10..65ca2d178 100644 --- a/examples/solid/asyncRateLimit/package.json +++ b/examples/solid/asyncRateLimit/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/asyncThrottle/package.json b/examples/solid/asyncThrottle/package.json index 9c1663ce0..130f937c4 100644 --- a/examples/solid/asyncThrottle/package.json +++ b/examples/solid/asyncThrottle/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createAsyncDebouncer/package.json b/examples/solid/createAsyncDebouncer/package.json index 3c6f870d9..c5eea7892 100644 --- a/examples/solid/createAsyncDebouncer/package.json +++ b/examples/solid/createAsyncDebouncer/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createAsyncQueuer/package.json b/examples/solid/createAsyncQueuer/package.json index a986fdb18..e0dabbd80 100644 --- a/examples/solid/createAsyncQueuer/package.json +++ b/examples/solid/createAsyncQueuer/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createAsyncRateLimiter/package.json b/examples/solid/createAsyncRateLimiter/package.json index 751816f34..49dee8b65 100644 --- a/examples/solid/createAsyncRateLimiter/package.json +++ b/examples/solid/createAsyncRateLimiter/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createAsyncThrottler/package.json b/examples/solid/createAsyncThrottler/package.json index a875ef046..c49b75d79 100644 --- a/examples/solid/createAsyncThrottler/package.json +++ b/examples/solid/createAsyncThrottler/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createDebouncedSignal/package.json b/examples/solid/createDebouncedSignal/package.json index 5d1f29118..55d4a55ac 100644 --- a/examples/solid/createDebouncedSignal/package.json +++ b/examples/solid/createDebouncedSignal/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createDebouncedValue/package.json b/examples/solid/createDebouncedValue/package.json index 0007f5b59..f36ac9aae 100644 --- a/examples/solid/createDebouncedValue/package.json +++ b/examples/solid/createDebouncedValue/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createDebouncer/package.json b/examples/solid/createDebouncer/package.json index a9f57a820..a24446300 100644 --- a/examples/solid/createDebouncer/package.json +++ b/examples/solid/createDebouncer/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createQueuer/package.json b/examples/solid/createQueuer/package.json index 0cab78453..44a437fec 100644 --- a/examples/solid/createQueuer/package.json +++ b/examples/solid/createQueuer/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createRateLimitedSignal/package.json b/examples/solid/createRateLimitedSignal/package.json index 8144b1022..fa1499445 100644 --- a/examples/solid/createRateLimitedSignal/package.json +++ b/examples/solid/createRateLimitedSignal/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createRateLimitedValue/package.json b/examples/solid/createRateLimitedValue/package.json index 4a446916f..8343313f1 100644 --- a/examples/solid/createRateLimitedValue/package.json +++ b/examples/solid/createRateLimitedValue/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createRateLimiter/package.json b/examples/solid/createRateLimiter/package.json index 062f3dcc9..b1f5c23f9 100644 --- a/examples/solid/createRateLimiter/package.json +++ b/examples/solid/createRateLimiter/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createThrottledSignal/package.json b/examples/solid/createThrottledSignal/package.json index da9ca06da..be4d87f43 100644 --- a/examples/solid/createThrottledSignal/package.json +++ b/examples/solid/createThrottledSignal/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createThrottledValue/package.json b/examples/solid/createThrottledValue/package.json index 3c2dfcf6c..44c45c1c8 100644 --- a/examples/solid/createThrottledValue/package.json +++ b/examples/solid/createThrottledValue/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/createThrottler/package.json b/examples/solid/createThrottler/package.json index ea80f43f6..5a640aee4 100644 --- a/examples/solid/createThrottler/package.json +++ b/examples/solid/createThrottler/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/debounce/package.json b/examples/solid/debounce/package.json index 1d84047d0..fa32ad9a6 100644 --- a/examples/solid/debounce/package.json +++ b/examples/solid/debounce/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/queue/package.json b/examples/solid/queue/package.json index 06c053981..7f49f5eca 100644 --- a/examples/solid/queue/package.json +++ b/examples/solid/queue/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/rateLimit/package.json b/examples/solid/rateLimit/package.json index 745138808..f6e94461d 100644 --- a/examples/solid/rateLimit/package.json +++ b/examples/solid/rateLimit/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/examples/solid/throttle/package.json b/examples/solid/throttle/package.json index 322f80e5b..c11d28e08 100644 --- a/examples/solid/throttle/package.json +++ b/examples/solid/throttle/package.json @@ -9,7 +9,7 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/solid-pacer": "^0.3.0", + "@tanstack/solid-pacer": "^0.4.0", "solid-js": "^1.9.6" }, "devDependencies": { diff --git a/packages/pacer/CHANGELOG.md b/packages/pacer/CHANGELOG.md index 78a5e508b..4a93c131a 100644 --- a/packages/pacer/CHANGELOG.md +++ b/packages/pacer/CHANGELOG.md @@ -1,32 +1,39 @@ # @tanstack/pacer +## 0.4.0 + +### Minor Changes + +- Added fixed and sliding windowTypes to rate limiters ([#17](https://github.com/TanStack/pacer/pull/17)) + Added `getIsExecuting` to `AsyncRateLimiter` + ## 0.3.0 ### Minor Changes -- - feat: add queuer expiration feature to `AsyncQueuer` and `Queuer` ([#12](https://github.com/TanStack/pacer/pull/12)) - - feat: add return values and types to `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` - - feat: standardize `onSuccess`, `onSettled`, and `onError` in `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` - - feat: replace `getExecutionCount` with `getSuccessCount`, `getErrorCount`, and `getSettleCount` in `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` - - feat: add `getIsPending`, `getIsExecuting`, and `getLastResult` to `AsyncThrottler` - - feat: add `leading` and `trailing` options to `AsyncThrottler` - - breaking: rename `onExecute` to `onSettled` in `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` - - breaking: Set `started` to `true` by default in `AsyncQueuer` and `Queuer` - - breaking: Simplified generics to just use `TFn` instead of `TFn, TArgs` in debouncers, throttlers, and rate limiters - - fix: fixed leading and trailing edge behavior of `Debouncer` and `AsyncDebouncer` - - fix: fixed `getIsPending` to return correct value in `AsyncDebouncer` +- feat: add queuer expiration feature to `AsyncQueuer` and `Queuer` ([#12](https://github.com/TanStack/pacer/pull/12)) +- feat: add return values and types to `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` +- feat: standardize `onSuccess`, `onSettled`, and `onError` in `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` +- feat: replace `getExecutionCount` with `getSuccessCount`, `getErrorCount`, and `getSettleCount` in `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` +- feat: add `getIsPending`, `getIsExecuting`, and `getLastResult` to `AsyncThrottler` +- feat: add `leading` and `trailing` options to `AsyncThrottler` +- breaking: rename `onExecute` to `onSettled` in `AsyncDebouncer`, `AsyncThrottler`, and `AsyncRateLimiter` +- breaking: Set `started` to `true` by default in `AsyncQueuer` and `Queuer` +- breaking: Simplified generics to just use `TFn` instead of `TFn, TArgs` in debouncers, throttlers, and rate limiters +- fix: fixed leading and trailing edge behavior of `Debouncer` and `AsyncDebouncer` +- fix: fixed `getIsPending` to return correct value in `AsyncDebouncer` ## 0.2.0 ### Minor Changes -- - feat: Add Solid JS Adapter ([`19cee99`](https://github.com/TanStack/pacer/commit/19cee995d79bc16077c9a28fc5f6ab251d626e16)) - - rewrote all instance methods on utilities for reading state to use `get*` naming convention - - added `getOptions` methods to all utilities for reading options - - changed rate limiter's `onReject` signature to match other utilities - - added an `onReject` option to the queuer utilities - - added `onExecute`, `onItemsChanged`, `onIsRunningChanged`, and other callbacks to all utilities - - added `getIsPending` methods to debouncer and throttler +- feat: Add Solid JS Adapter ([`19cee99`](https://github.com/TanStack/pacer/commit/19cee995d79bc16077c9a28fc5f6ab251d626e16)) +- rewrote all instance methods on utilities for reading state to use `get*` naming convention +- added `getOptions` methods to all utilities for reading options +- changed rate limiter's `onReject` signature to match other utilities +- added an `onReject` option to the queuer utilities +- added `onExecute`, `onItemsChanged`, `onIsRunningChanged`, and other callbacks to all utilities +- added `getIsPending` methods to debouncer and throttler ## 0.1.0 diff --git a/packages/pacer/package.json b/packages/pacer/package.json index a26ec3686..2ab79869f 100644 --- a/packages/pacer/package.json +++ b/packages/pacer/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/pacer", - "version": "0.3.0", + "version": "0.4.0", "description": "Utilities for debouncing, throttling, rate-limiting, queuing, and more.", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/pacer/src/async-debouncer.ts b/packages/pacer/src/async-debouncer.ts index 7dd10af50..b01938697 100644 --- a/packages/pacer/src/async-debouncer.ts +++ b/packages/pacer/src/async-debouncer.ts @@ -58,16 +58,20 @@ const defaultOptions: Required> = { * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until * the function stops being called for the specified delay period. * + * Unlike the non-async Debouncer, this async version supports returning values from the debounced function, + * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call + * instead of setting the result on a state variable from within the debounced function. + * * @example * ```ts * const asyncDebouncer = new AsyncDebouncer(async (value: string) => { - * await searchAPI(value); + * const results = await searchAPI(value); + * return results; // Return value is preserved * }, { wait: 500 }); * * // Called on each keystroke but only executes after 500ms of no typing - * inputElement.addEventListener('input', () => { - * asyncDebouncer.maybeExecute(inputElement.value); - * }); + * // Returns the API response directly + * const results = await asyncDebouncer.maybeExecute(inputElement.value); * ``` */ export class AsyncDebouncer { @@ -245,16 +249,20 @@ export class AsyncDebouncer { * The debounced function will only execute once the wait period has elapsed without any new calls. * If called again during the wait period, the timer resets and a new wait period begins. * + * Unlike the non-async Debouncer, this async version supports returning values from the debounced function, + * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call + * instead of setting the result on a state variable from within the debounced function. + * * @example * ```ts * const debounced = asyncDebounce(async (value: string) => { - * await saveToAPI(value); + * const result = await saveToAPI(value); + * return result; // Return value is preserved * }, { wait: 1000 }); * * // Will only execute once, 1 second after the last call - * await debounced("first"); // Cancelled - * await debounced("second"); // Cancelled - * await debounced("third"); // Executes after 1s + * // Returns the API response directly + * const result = await debounced("third"); * ``` */ export function asyncDebounce( diff --git a/packages/pacer/src/async-rate-limiter.ts b/packages/pacer/src/async-rate-limiter.ts index c87575dd2..ede00c23b 100644 --- a/packages/pacer/src/async-rate-limiter.ts +++ b/packages/pacer/src/async-rate-limiter.ts @@ -17,6 +17,10 @@ export interface AsyncRateLimiterOptions { * Optional error handler for when the rate-limited function throws */ onError?: (error: unknown, rateLimiter: AsyncRateLimiter) => void + /** + * Optional callback function that is called when an execution is rejected due to rate limiting + */ + onReject?: (rateLimiter: AsyncRateLimiter) => void /** * Optional function to call when the rate-limited function is executed */ @@ -28,14 +32,17 @@ export interface AsyncRateLimiterOptions { result: ReturnType, rateLimiter: AsyncRateLimiter, ) => void - /** - * Optional callback function that is called when an execution is rejected due to rate limiting - */ - onReject?: (rateLimiter: AsyncRateLimiter) => void /** * Time window in milliseconds within which the limit applies */ window: number + /** + * Type of window to use for rate limiting + * - 'fixed': Uses a fixed window that resets after the window period + * - 'sliding': Uses a sliding window that allows executions as old ones expire + * Defaults to 'fixed' + */ + windowType?: 'fixed' | 'sliding' } const defaultOptions: Required< @@ -46,6 +53,7 @@ const defaultOptions: Required< onReject: () => {}, onSettled: () => {}, onSuccess: () => {}, + windowType: 'fixed', } /** @@ -55,6 +63,16 @@ const defaultOptions: Required< * then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where * all executions happen immediately, followed by a complete block. * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All executions within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more + * consistent rate of execution over time. + * + * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function, + * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call + * instead of setting the result on a state variable from within the rate-limited function. + * * For smoother execution patterns, consider using: * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms) * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls) @@ -66,11 +84,12 @@ const defaultOptions: Required< * ```ts * const rateLimiter = new AsyncRateLimiter( * async (id: string) => await api.getData(id), - * { limit: 5, window: 1000 } // 5 calls per second + * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window * ); * * // Will execute immediately until limit reached, then block - * await rateLimiter.maybeExecute('123'); + * // Returns the API response directly + * const data = await rateLimiter.maybeExecute('123'); * ``` */ export class AsyncRateLimiter { @@ -81,6 +100,7 @@ export class AsyncRateLimiter { private _rejectionCount = 0 private _settleCount = 0 private _successCount = 0 + private _isExecuting = false constructor( private fn: TFn, @@ -128,9 +148,22 @@ export class AsyncRateLimiter { ): Promise | undefined> { this.cleanupOldExecutions() - if (this._executionTimes.length < this._options.limit) { - await this.executeFunction(...args) - return this._lastResult + if (this._options.windowType === 'sliding') { + // For sliding window, we can execute if we have capacity in the current window + if (this._executionTimes.length < this._options.limit) { + await this.executeFunction(...args) + return this._lastResult + } + } else { + // For fixed window, we need to check if we're in a new window + const now = Date.now() + const oldestExecution = Math.min(...this._executionTimes) + const isNewWindow = oldestExecution + this._options.window <= now + + if (isNewWindow || this._executionTimes.length < this._options.limit) { + await this.executeFunction(...args) + return this._lastResult + } } this.rejectFunction() @@ -141,6 +174,7 @@ export class AsyncRateLimiter { ...args: Parameters ): Promise | undefined> { if (!this._options.enabled) return + this._isExecuting = true const now = Date.now() this._executionTimes.push(now) @@ -152,6 +186,7 @@ export class AsyncRateLimiter { this._errorCount++ this._options.onError?.(error, this) } finally { + this._isExecuting = false this._settleCount++ this._options.onSettled?.(this) } @@ -184,9 +219,15 @@ export class AsyncRateLimiter { /** * Returns the number of milliseconds until the next execution will be possible + * For fixed windows, this is the time until the current window resets + * For sliding windows, this is the time until the oldest execution expires */ getMsUntilNextWindow(): number { - return this.getRemainingInWindow() * this._options.window + if (this.getRemainingInWindow() > 0) { + return 0 + } + const oldestExecution = Math.min(...this._executionTimes) + return oldestExecution + this._options.window - Date.now() } /** @@ -217,6 +258,13 @@ export class AsyncRateLimiter { return this._rejectionCount } + /** + * Returns whether the function is currently executing + */ + getIsExecuting(): boolean { + return this._isExecuting + } + /** * Resets the rate limiter state */ @@ -232,6 +280,16 @@ export class AsyncRateLimiter { /** * Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window. * + * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function, + * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call + * instead of setting the result on a state variable from within the rate-limited function. + * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All executions within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more + * consistent rate of execution over time. + * * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing: * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets * - A throttler ensures even spacing between executions, which can be better for consistent performance @@ -242,10 +300,11 @@ export class AsyncRateLimiter { * * @example * ```ts - * // Rate limit to 5 calls per minute + * // Rate limit to 5 calls per minute with a sliding window * const rateLimited = asyncRateLimit(makeApiCall, { * limit: 5, * window: 60000, + * windowType: 'sliding', * onReject: (rateLimiter) => { * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); * } @@ -253,7 +312,8 @@ export class AsyncRateLimiter { * * // First 5 calls will execute immediately * // Additional calls will be rejected until the minute window resets - * await rateLimited(); + * // Returns the API response directly + * const result = await rateLimited(); * * // For more even execution, consider using throttle instead: * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds diff --git a/packages/pacer/src/async-throttler.ts b/packages/pacer/src/async-throttler.ts index 880b3b13c..6f076da3d 100644 --- a/packages/pacer/src/async-throttler.ts +++ b/packages/pacer/src/async-throttler.ts @@ -58,19 +58,23 @@ const defaultOptions: Required> = { * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a * regular interval regardless of how often it's called. * + * Unlike the non-async Throttler, this async version supports returning values from the throttled function, + * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call + * instead of setting the result on a state variable from within the throttled function. + * * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to * ensure a maximum execution frequency. * * @example * ```ts * const throttler = new AsyncThrottler(async (value: string) => { - * await saveToAPI(value); + * const result = await saveToAPI(value); + * return result; // Return value is preserved * }, { wait: 1000 }); * * // Will only execute once per second no matter how often called - * inputElement.addEventListener('input', () => { - * throttler.maybeExecute(inputElement.value); - * }); + * // Returns the API response directly + * const result = await throttler.maybeExecute(inputElement.value); * ``` */ export class AsyncThrottler { @@ -258,15 +262,20 @@ export class AsyncThrottler { * The throttled function will execute at most once per wait period, even if called multiple times. * If called while executing, it will wait until execution completes before scheduling the next call. * + * Unlike the non-async Throttler, this async version supports returning values from the throttled function, + * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call + * instead of setting the result on a state variable from within the throttled function. + * * @example * ```ts - * const throttled = asyncThrottle(async () => { - * await someAsyncOperation(); + * const throttled = asyncThrottle(async (value: string) => { + * const result = await saveToAPI(value); + * return result; // Return value is preserved * }, { wait: 1000 }); * * // This will execute at most once per second - * await throttled(); - * await throttled(); // Waits 1 second before executing + * // Returns the API response directly + * const result = await throttled(inputElement.value); * ``` */ export function asyncThrottle( diff --git a/packages/pacer/src/rate-limiter.ts b/packages/pacer/src/rate-limiter.ts index 99e44be3a..6cac6d54b 100644 --- a/packages/pacer/src/rate-limiter.ts +++ b/packages/pacer/src/rate-limiter.ts @@ -25,6 +25,13 @@ export interface RateLimiterOptions { * Time window in milliseconds within which the limit applies */ window: number + /** + * Type of window to use for rate limiting + * - 'fixed': Uses a fixed window that resets after the window period + * - 'sliding': Uses a sliding window that allows executions as old ones expire + * Defaults to 'fixed' + */ + windowType?: 'fixed' | 'sliding' } const defaultOptions: Required> = { @@ -33,6 +40,7 @@ const defaultOptions: Required> = { onExecute: () => {}, onReject: () => {}, window: 0, + windowType: 'fixed', } /** @@ -42,6 +50,12 @@ const defaultOptions: Required> = { * then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where * all executions happen immediately, followed by a complete block. * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All executions within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more + * consistent rate of execution over time. + * * For smoother execution patterns, consider using: * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms) * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls) @@ -53,7 +67,7 @@ const defaultOptions: Required> = { * ```ts * const rateLimiter = new RateLimiter( * (id: string) => api.getData(id), - * { limit: 5, window: 1000 } // 5 calls per second + * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window * ); * * // Will execute immediately until limit reached, then block @@ -109,13 +123,25 @@ export class RateLimiter { maybeExecute(...args: Parameters): boolean { this.cleanupOldExecutions() - if (this._executionTimes.length < this._options.limit) { - this.executeFunction(...args) - return true + if (this._options.windowType === 'sliding') { + // For sliding window, we can execute if we have capacity in the current window + if (this._executionTimes.length < this._options.limit) { + this.executeFunction(...args) + return true + } + } else { + // For fixed window, we need to check if we're in a new window + const now = Date.now() + const oldestExecution = Math.min(...this._executionTimes) + const isNewWindow = oldestExecution + this._options.window <= now + + if (isNewWindow || this._executionTimes.length < this._options.limit) { + this.executeFunction(...args) + return true + } } this.rejectFunction() - return false } @@ -169,6 +195,9 @@ export class RateLimiter { * Returns the number of milliseconds until the next execution will be possible */ getMsUntilNextWindow(): number { + if (this.getRemainingInWindow() > 0) { + return 0 + } const oldestExecution = Math.min(...this._executionTimes) return oldestExecution + this._options.window - Date.now() } @@ -191,15 +220,22 @@ export class RateLimiter { * - A throttler ensures even spacing between executions, which can be better for consistent performance * - A debouncer collapses multiple calls into one, which is better for handling bursts of events * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All executions within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more + * consistent rate of execution over time. + * * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically * need to enforce a hard limit on the number of executions within a time period. * * @example * ```ts - * // Rate limit to 5 calls per minute + * // Rate limit to 5 calls per minute with a sliding window * const rateLimited = rateLimit(makeApiCall, { * limit: 5, * window: 60000, + * windowType: 'sliding', * onReject: (rateLimiter) => { * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); * } diff --git a/packages/pacer/tests/async-rate-limiter.test.ts b/packages/pacer/tests/async-rate-limiter.test.ts new file mode 100644 index 000000000..727a4a77e --- /dev/null +++ b/packages/pacer/tests/async-rate-limiter.test.ts @@ -0,0 +1,454 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { AsyncRateLimiter, asyncRateLimit } from '../src/async-rate-limiter' + +describe('AsyncRateLimiter', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + describe('basic rate limiting', () => { + it('should allow execution within limits', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 3, + window: 1000, + }) + + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + await expect(rateLimiter.maybeExecute()).resolves.toBeUndefined() + + expect(mockFn).toHaveBeenCalledTimes(3) + }) + + it('should reset after window expires', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 2, + window: 1000, + }) + + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + await expect(rateLimiter.maybeExecute()).resolves.toBeUndefined() + + // Advance time past the window + vi.advanceTimersByTime(1001) + + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + expect(mockFn).toHaveBeenCalledTimes(3) + }) + }) + + describe('execution tracking', () => { + it('should track success and error counts', async () => { + const mockFn = vi + .fn() + .mockResolvedValueOnce('success1') + .mockRejectedValueOnce(new Error('error')) + .mockResolvedValueOnce('success2') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 3, + window: 1000, + }) + + await rateLimiter.maybeExecute() + expect(rateLimiter.getSuccessCount()).toBe(1) + expect(rateLimiter.getErrorCount()).toBe(0) + + await rateLimiter.maybeExecute().catch(() => {}) + expect(rateLimiter.getSuccessCount()).toBe(1) + expect(rateLimiter.getErrorCount()).toBe(1) + + await rateLimiter.maybeExecute() + expect(rateLimiter.getSuccessCount()).toBe(2) + expect(rateLimiter.getErrorCount()).toBe(1) + }) + + it('should track remaining executions', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 3, + window: 1000, + }) + + expect(rateLimiter.getRemainingInWindow()).toBe(3) + + await rateLimiter.maybeExecute() + expect(rateLimiter.getRemainingInWindow()).toBe(2) + + await rateLimiter.maybeExecute() + expect(rateLimiter.getRemainingInWindow()).toBe(1) + + await rateLimiter.maybeExecute() + expect(rateLimiter.getRemainingInWindow()).toBe(0) + }) + }) + + describe('reset functionality', () => { + it('should reset execution state', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 2, + window: 1000, + }) + + await rateLimiter.maybeExecute() + await rateLimiter.maybeExecute() + expect(rateLimiter.getRemainingInWindow()).toBe(0) + + rateLimiter.reset() + expect(rateLimiter.getRemainingInWindow()).toBe(2) + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + }) + }) + + describe('asyncRateLimit helper function', () => { + it('should create a rate-limited function', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimitedFn = asyncRateLimit(mockFn, { limit: 2, window: 1000 }) + + await expect(rateLimitedFn()).resolves.toBe('result') + await expect(rateLimitedFn()).resolves.toBe('result') + await expect(rateLimitedFn()).resolves.toBeUndefined() + + expect(mockFn).toHaveBeenCalledTimes(2) + }) + + it('should pass arguments to the wrapped function', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimitedFn = asyncRateLimit(mockFn, { limit: 1, window: 1000 }) + + await rateLimitedFn(42, 'test') + + expect(mockFn).toHaveBeenCalledWith(42, 'test') + }) + + it('should handle multiple executions with proper timing', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimitedFn = asyncRateLimit(mockFn, { limit: 2, window: 1000 }) + + // First burst + await expect(rateLimitedFn('a')).resolves.toBe('result') + await expect(rateLimitedFn('b')).resolves.toBe('result') + await expect(rateLimitedFn('c')).resolves.toBeUndefined() + expect(mockFn).toHaveBeenCalledTimes(2) + + // Advance past window + vi.advanceTimersByTime(1001) + + // Should be able to execute again + await expect(rateLimitedFn('d')).resolves.toBe('result') + expect(mockFn).toHaveBeenCalledTimes(3) + expect(mockFn).toHaveBeenLastCalledWith('d') + }) + }) + + describe('sliding window functionality', () => { + it('should allow executions as old ones expire', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 3, + window: 1000, + windowType: 'sliding', + }) + + // Fill up the window + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + await expect(rateLimiter.maybeExecute()).resolves.toBeUndefined() + + // Advance time by 500ms - oldest execution should still be in window + vi.advanceTimersByTime(500) + await expect(rateLimiter.maybeExecute()).resolves.toBeUndefined() + + // Advance time by 600ms more - oldest execution should be expired + vi.advanceTimersByTime(600) + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + expect(mockFn).toHaveBeenCalledTimes(4) + }) + + it('should maintain consistent rate with sliding window', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 3, + window: 1000, + windowType: 'sliding', + }) + + // Execute 3 times + await rateLimiter.maybeExecute() + await rateLimiter.maybeExecute() + await rateLimiter.maybeExecute() + + // Advance time by 400ms + vi.advanceTimersByTime(400) + await expect(rateLimiter.maybeExecute()).resolves.toBeUndefined() + + // Advance time by 700ms - one execution should be expired + vi.advanceTimersByTime(700) + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + expect(mockFn).toHaveBeenCalledTimes(4) + }) + }) + + describe('enabled/disabled state', () => { + it('should not execute when disabled', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 3, + window: 1000, + enabled: false, + }) + + await expect(rateLimiter.maybeExecute()).resolves.toBeUndefined() + expect(mockFn).not.toHaveBeenCalled() + }) + + it('should update enabled state', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 3, + window: 1000, + enabled: false, + }) + + await rateLimiter.maybeExecute() + expect(mockFn).not.toHaveBeenCalled() + + rateLimiter.setOptions({ enabled: true }) + await expect(rateLimiter.maybeExecute()).resolves.toBe('result') + expect(mockFn).toHaveBeenCalledTimes(1) + }) + }) + + describe('callback functions', () => { + it('should call onSuccess callback after successful execution', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const onSuccess = vi.fn() + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 1, + window: 1000, + onSuccess, + }) + + await rateLimiter.maybeExecute() + expect(onSuccess).toHaveBeenCalledTimes(1) + expect(onSuccess).toHaveBeenCalledWith('result', rateLimiter) + }) + + it('should call onError callback when execution fails', async () => { + const error = new Error('test error') + const mockFn = vi.fn().mockRejectedValue(error) + const onError = vi.fn() + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 1, + window: 1000, + onError, + }) + + await rateLimiter.maybeExecute().catch(() => {}) + expect(onError).toHaveBeenCalledTimes(1) + expect(onError).toHaveBeenCalledWith(error, rateLimiter) + }) + + it('should call onSettled callback after execution completes', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const onSettled = vi.fn() + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 1, + window: 1000, + onSettled, + }) + + await rateLimiter.maybeExecute() + expect(onSettled).toHaveBeenCalledTimes(1) + expect(onSettled).toHaveBeenCalledWith(rateLimiter) + }) + + it('should call onReject callback when execution is rejected', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const onReject = vi.fn() + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 1, + window: 1000, + onReject, + }) + + await rateLimiter.maybeExecute() + await rateLimiter.maybeExecute() + expect(onReject).toHaveBeenCalledTimes(1) + expect(onReject).toHaveBeenCalledWith(rateLimiter) + }) + }) + + describe('time tracking', () => { + it('should correctly calculate time until next window', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 1, + window: 1000, + }) + + await rateLimiter.maybeExecute() + expect(rateLimiter.getMsUntilNextWindow()).toBe(1000) + + vi.advanceTimersByTime(500) + expect(rateLimiter.getMsUntilNextWindow()).toBe(500) + + vi.advanceTimersByTime(500) + expect(rateLimiter.getMsUntilNextWindow()).toBe(0) + }) + + it('should return 0 ms when executions are available', async () => { + const mockFn = vi.fn().mockResolvedValue('result') + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 2, + window: 1000, + }) + + expect(rateLimiter.getMsUntilNextWindow()).toBe(0) + await rateLimiter.maybeExecute() + expect(rateLimiter.getMsUntilNextWindow()).toBe(0) + }) + }) + + describe('async-specific behavior', () => { + it('should handle concurrent executions properly', async () => { + const mockFn = vi.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + return 'result' + }) + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 2, + window: 1000, + }) + + // Start multiple executions concurrently + const promises = [ + rateLimiter.maybeExecute(), + rateLimiter.maybeExecute(), + rateLimiter.maybeExecute(), + ] + + // Advance timers to resolve the promises + await vi.advanceTimersByTimeAsync(100) + + // All promises should resolve, but only 2 should execute + const results = await Promise.all(promises) + expect(results.filter((r) => r === 'result')).toHaveLength(2) + expect(results).toContain(undefined) + expect(mockFn).toHaveBeenCalledTimes(2) + }) + + it('should handle long-running executions correctly', async () => { + const mockFn = vi.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 500)) + return 'result' + }) + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 2, + window: 1000, + }) + + // Start a long-running execution + const promise1 = rateLimiter.maybeExecute() + + // Try to execute again while the first one is still running + const promise2 = rateLimiter.maybeExecute() + + // Advance timers to resolve the promises + await vi.advanceTimersByTimeAsync(500) + + // Both should execute since they're within the limit + const [result1, result2] = await Promise.all([promise1, promise2]) + expect(result1).toBe('result') + expect(result2).toBe('result') + expect(mockFn).toHaveBeenCalledTimes(2) + }) + + it('should handle cancellation of async operations', async () => { + const mockFn = vi.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + return 'result' + }) + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 1, + window: 1000, + }) + + // Start an execution + const promise = rateLimiter.maybeExecute() + + // Reset the rate limiter while the execution is pending + rateLimiter.reset() + + // Advance timers to resolve the promise + await vi.advanceTimersByTimeAsync(100) + + // The original promise should still resolve + await expect(promise).resolves.toBe('result') + expect(mockFn).toHaveBeenCalledTimes(1) + }) + + it('should handle rapid async executions with sliding window', async () => { + const mockFn = vi.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)) + return 'result' + }) + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 3, + window: 1000, + windowType: 'sliding', + }) + + // Start multiple executions rapidly + const promises = Array.from({ length: 5 }, () => + rateLimiter.maybeExecute(), + ) + + // Advance timers to resolve all promises + await vi.advanceTimersByTimeAsync(50) + + const results = await Promise.all(promises) + + // Should have 3 successes and 2 undefined (rejected) + expect(results.filter((r) => r === 'result')).toHaveLength(3) + expect(results.filter((r) => r === undefined)).toHaveLength(2) + expect(mockFn).toHaveBeenCalledTimes(3) + }) + + it('should maintain proper execution order with async operations', async () => { + const results: Array = [] + const mockFn = vi.fn().mockImplementation(async (id: string) => { + await new Promise((resolve) => setTimeout(resolve, 100)) + results.push(id) + return id + }) + const rateLimiter = new AsyncRateLimiter(mockFn, { + limit: 2, + window: 1000, + }) + + // Start executions in sequence + const promise1 = rateLimiter.maybeExecute('first') + const promise2 = rateLimiter.maybeExecute('second') + const promise3 = rateLimiter.maybeExecute('third') + + // Advance timers to resolve all promises + await vi.advanceTimersByTimeAsync(100) + + await Promise.all([promise1, promise2, promise3]) + + // Results should maintain the order of execution + expect(results).toEqual(['first', 'second']) + expect(mockFn).toHaveBeenCalledTimes(2) + }) + }) +}) diff --git a/packages/pacer/tests/rate-limiter.test.ts b/packages/pacer/tests/rate-limiter.test.ts index bd455e29e..55ac29900 100644 --- a/packages/pacer/tests/rate-limiter.test.ts +++ b/packages/pacer/tests/rate-limiter.test.ts @@ -127,4 +127,145 @@ describe('RateLimiter', () => { expect(mockFn).toHaveBeenLastCalledWith('d') }) }) + + describe('sliding window functionality', () => { + it('should allow executions as old ones expire', () => { + const mockFn = vi.fn() + const rateLimiter = new RateLimiter(mockFn, { + limit: 3, + window: 1000, + windowType: 'sliding', + }) + + // Fill up the window + expect(rateLimiter.maybeExecute()).toBe(true) + expect(rateLimiter.maybeExecute()).toBe(true) + expect(rateLimiter.maybeExecute()).toBe(true) + expect(rateLimiter.maybeExecute()).toBe(false) + + // Advance time by 500ms - oldest execution should still be in window + vi.advanceTimersByTime(500) + expect(rateLimiter.maybeExecute()).toBe(false) + + // Advance time by 600ms more - oldest execution should be expired + vi.advanceTimersByTime(600) + expect(rateLimiter.maybeExecute()).toBe(true) + expect(mockFn).toHaveBeenCalledTimes(4) + }) + + it('should maintain consistent rate with sliding window', () => { + const mockFn = vi.fn() + const rateLimiter = new RateLimiter(mockFn, { + limit: 3, + window: 1000, + windowType: 'sliding', + }) + + // Execute 3 times + rateLimiter.maybeExecute() + rateLimiter.maybeExecute() + rateLimiter.maybeExecute() + + // Advance time by 400ms + vi.advanceTimersByTime(400) + expect(rateLimiter.maybeExecute()).toBe(false) + + // Advance time by 700ms - one execution should be expired + vi.advanceTimersByTime(700) + expect(rateLimiter.maybeExecute()).toBe(true) + expect(mockFn).toHaveBeenCalledTimes(4) + }) + }) + + describe('enabled/disabled state', () => { + it('should not execute when disabled', () => { + const mockFn = vi.fn() + const rateLimiter = new RateLimiter(mockFn, { + limit: 3, + window: 1000, + enabled: false, + }) + + expect(rateLimiter.maybeExecute()).toBe(true) + expect(mockFn).not.toHaveBeenCalled() + }) + + it('should update enabled state', () => { + const mockFn = vi.fn() + const rateLimiter = new RateLimiter(mockFn, { + limit: 3, + window: 1000, + enabled: false, + }) + + rateLimiter.maybeExecute() + expect(mockFn).not.toHaveBeenCalled() + + rateLimiter.setOptions({ enabled: true }) + rateLimiter.maybeExecute() + expect(mockFn).toHaveBeenCalledTimes(1) + }) + }) + + describe('callback functions', () => { + it('should call onExecute callback after successful execution', () => { + const mockFn = vi.fn() + const onExecute = vi.fn() + const rateLimiter = new RateLimiter(mockFn, { + limit: 1, + window: 1000, + onExecute, + }) + + rateLimiter.maybeExecute() + expect(onExecute).toHaveBeenCalledTimes(1) + expect(onExecute).toHaveBeenCalledWith(rateLimiter) + }) + + it('should call onReject callback when execution is rejected', () => { + const mockFn = vi.fn() + const onReject = vi.fn() + const rateLimiter = new RateLimiter(mockFn, { + limit: 1, + window: 1000, + onReject, + }) + + rateLimiter.maybeExecute() + rateLimiter.maybeExecute() + expect(onReject).toHaveBeenCalledTimes(1) + expect(onReject).toHaveBeenCalledWith(rateLimiter) + }) + }) + + describe('time tracking', () => { + it('should correctly calculate time until next window', () => { + const mockFn = vi.fn() + const rateLimiter = new RateLimiter(mockFn, { + limit: 1, + window: 1000, + }) + + rateLimiter.maybeExecute() + expect(rateLimiter.getMsUntilNextWindow()).toBe(1000) + + vi.advanceTimersByTime(500) + expect(rateLimiter.getMsUntilNextWindow()).toBe(500) + + vi.advanceTimersByTime(500) + expect(rateLimiter.getMsUntilNextWindow()).toBe(0) + }) + + it('should return 0 ms when executions are available', () => { + const mockFn = vi.fn() + const rateLimiter = new RateLimiter(mockFn, { + limit: 2, + window: 1000, + }) + + expect(rateLimiter.getMsUntilNextWindow()).toBe(0) + rateLimiter.maybeExecute() + expect(rateLimiter.getMsUntilNextWindow()).toBe(0) + }) + }) }) diff --git a/packages/react-pacer/CHANGELOG.md b/packages/react-pacer/CHANGELOG.md index fb531cb3a..a00ed381d 100644 --- a/packages/react-pacer/CHANGELOG.md +++ b/packages/react-pacer/CHANGELOG.md @@ -1,5 +1,17 @@ # @tanstack/react-pacer +## 0.4.0 + +### Minor Changes + +- Added fixed and sliding windowTypes to rate limiters ([#17](https://github.com/TanStack/pacer/pull/17)) + Added `getIsExecuting` to `AsyncRateLimiter` + +### Patch Changes + +- Updated dependencies [[`f12ba56`](https://github.com/TanStack/pacer/commit/f12ba561d9eafb6a19a16514f8db1a2f5f6fda82)]: + - @tanstack/pacer@0.4.0 + ## 0.3.0 ### Minor Changes diff --git a/packages/react-pacer/package.json b/packages/react-pacer/package.json index 9032a991c..29bf10a07 100644 --- a/packages/react-pacer/package.json +++ b/packages/react-pacer/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-pacer", - "version": "0.3.0", + "version": "0.4.0", "description": "Utilities for debouncing and throttling functions in React.", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts b/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts index b26629874..68501be3c 100644 --- a/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts +++ b/packages/react-pacer/src/async-rate-limiter/useAsyncRateLimiter.ts @@ -14,23 +14,34 @@ import type { AsyncRateLimiterOptions } from '@tanstack/pacer/async-rate-limiter * then blocks subsequent calls until the window passes. This is useful for respecting API rate limits, * managing resource constraints, or controlling bursts of async operations. * + * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function, + * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call + * instead of setting the result on a state variable from within the rate-limited function. + * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All executions within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more + * consistent rate of execution over time. + * * @example * ```tsx - * // Basic API call rate limiting + * // Basic API call rate limiting with return value * const { maybeExecute } = useAsyncRateLimiter( * async (id: string) => { * const data = await api.fetchData(id); - * return data; + * return data; // Return value is preserved * }, * { limit: 5, window: 1000 } // 5 calls per second * ); * - * // With state management + * // With state management and return value * const [data, setData] = useState(null); * const { maybeExecute } = useAsyncRateLimiter( * async (query) => { * const result = await searchAPI(query); * setData(result); + * return result; // Return value can be used by the caller * }, * { * limit: 10, diff --git a/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts b/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts index 3697906a4..c5514cdb6 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedCallback.ts @@ -15,6 +15,12 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * This can lead to bursts of rapid executions followed by periods where all calls * are blocked. * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All executions within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more + * consistent rate of execution over time. + * * For smoother execution patterns, consider: * - useThrottledCallback: When you want consistent spacing between executions (e.g. UI updates) * - useDebouncedCallback: When you want to collapse rapid calls into a single execution (e.g. search input) @@ -34,7 +40,7 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * * @example * ```tsx - * // Rate limit API calls to maximum 5 calls per minute + * // Rate limit API calls to maximum 5 calls per minute with a sliding window * const makeApiCall = useRateLimitedCallback( * (data: ApiData) => { * return fetch('/api/endpoint', { method: 'POST', body: JSON.stringify(data) }); @@ -42,6 +48,7 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * { * limit: 5, * window: 60000, // 1 minute + * windowType: 'sliding', * onReject: () => { * console.warn('API rate limit reached. Please wait before trying again.'); * } diff --git a/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts b/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts index d0b39e118..f59058734 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedState.ts @@ -13,6 +13,12 @@ import type { * subsequent updates until the window resets. Unlike throttling or debouncing, it does not attempt to space out * or intelligently collapse updates. This can lead to bursts of rapid updates followed by periods of no updates. * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All updates within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows updates as old ones expire. This provides a more + * consistent rate of updates over time. + * * For smoother update patterns, consider: * - useThrottledState: When you want consistent spacing between updates (e.g. UI changes) * - useDebouncedState: When you want to collapse rapid updates into a single update (e.g. search input) @@ -29,16 +35,18 @@ import type { * * @example * ```tsx - * // Basic rate limiting - update state at most 5 times per minute + * // Basic rate limiting - update state at most 5 times per minute with a sliding window * const [value, setValue, rateLimiter] = useRateLimitedState(0, { * limit: 5, - * window: 60000 + * window: 60000, + * windowType: 'sliding' * }); * - * // With rejection callback + * // With rejection callback and fixed window * const [value, setValue] = useRateLimitedState(0, { * limit: 3, * window: 5000, + * windowType: 'fixed', * onReject: (rateLimiter) => { * alert(`Rate limit reached. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); * } @@ -55,7 +63,6 @@ import type { * }; * ``` */ - export function useRateLimitedState( value: TValue, options: RateLimiterOptions>>, diff --git a/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts b/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts index ffdc3ef3f..4cc9d50a9 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimitedValue.ts @@ -13,6 +13,12 @@ import type { * subsequent updates until the window resets. Unlike throttling or debouncing, it does not attempt to space out * or intelligently collapse updates. This can lead to bursts of rapid updates followed by periods of no updates. * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All updates within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows updates as old ones expire. This provides a more + * consistent rate of updates over time. + * * For smoother update patterns, consider: * - useThrottledValue: When you want consistent spacing between updates (e.g. UI changes) * - useDebouncedValue: When you want to collapse rapid updates into a single update (e.g. search input) @@ -28,16 +34,18 @@ import type { * * @example * ```tsx - * // Basic rate limiting - update at most 5 times per minute + * // Basic rate limiting - update at most 5 times per minute with a sliding window * const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { * limit: 5, - * window: 60000 + * window: 60000, + * windowType: 'sliding' * }); * - * // With rejection callback + * // With rejection callback and fixed window * const [rateLimitedValue, rateLimiter] = useRateLimitedValue(rawValue, { * limit: 3, * window: 5000, + * windowType: 'fixed', * onReject: (rateLimiter) => { * console.log(`Update rejected. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); * } diff --git a/packages/react-pacer/src/rate-limiter/useRateLimiter.ts b/packages/react-pacer/src/rate-limiter/useRateLimiter.ts index 7ee2a58de..488f012cd 100644 --- a/packages/react-pacer/src/rate-limiter/useRateLimiter.ts +++ b/packages/react-pacer/src/rate-limiter/useRateLimiter.ts @@ -14,6 +14,12 @@ import type { AnyFunction } from '@tanstack/pacer/types' * a time window, then blocks all subsequent calls until the window resets. Unlike throttling or debouncing, * it does not attempt to space out or collapse executions intelligently. * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All executions within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more + * consistent rate of execution over time. + * * For smoother execution patterns: * - Use throttling when you want consistent spacing between executions (e.g. UI updates) * - Use debouncing when you want to collapse rapid-fire events (e.g. search input) @@ -28,10 +34,11 @@ import type { AnyFunction } from '@tanstack/pacer/types' * * @example * ```tsx - * // Basic rate limiting - max 5 calls per minute + * // Basic rate limiting - max 5 calls per minute with a sliding window * const { maybeExecute } = useRateLimiter(apiCall, { * limit: 5, * window: 60000, + * windowType: 'sliding', * }); * * // Monitor rate limit status diff --git a/packages/solid-pacer/CHANGELOG.md b/packages/solid-pacer/CHANGELOG.md index dd98771bf..e7409a2dc 100644 --- a/packages/solid-pacer/CHANGELOG.md +++ b/packages/solid-pacer/CHANGELOG.md @@ -1,5 +1,17 @@ # @tanstack/solid-pacer +## 0.4.0 + +### Minor Changes + +- Added fixed and sliding windowTypes to rate limiters ([#17](https://github.com/TanStack/pacer/pull/17)) + Added `getIsExecuting` to `AsyncRateLimiter` + +### Patch Changes + +- Updated dependencies [[`f12ba56`](https://github.com/TanStack/pacer/commit/f12ba561d9eafb6a19a16514f8db1a2f5f6fda82)]: + - @tanstack/pacer@0.4.0 + ## 0.3.0 ### Minor Changes diff --git a/packages/solid-pacer/package.json b/packages/solid-pacer/package.json index b6f66760e..e25a34eb9 100644 --- a/packages/solid-pacer/package.json +++ b/packages/solid-pacer/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/solid-pacer", - "version": "0.3.0", + "version": "0.4.0", "description": "Utilities for debouncing and throttling functions in Solid.", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts b/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts index be2d442fe..823286494 100644 --- a/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts +++ b/packages/solid-pacer/src/async-rate-limiter/createAsyncRateLimiter.ts @@ -33,23 +33,34 @@ export interface SolidAsyncRateLimiter * then blocks subsequent calls until the window passes. This is useful for respecting API rate limits, * managing resource constraints, or controlling bursts of async operations. * + * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function, + * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call + * instead of setting the result on a state variable from within the rate-limited function. + * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All executions within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more + * consistent rate of execution over time. + * * @example * ```tsx - * // Basic API call rate limiting + * // Basic API call rate limiting with return value * const { maybeExecute } = createAsyncRateLimiter( * async (id: string) => { * const data = await api.fetchData(id); - * return data; + * return data; // Return value is preserved * }, * { limit: 5, window: 1000 } // 5 calls per second * ); * - * // With state management + * // With state management and return value * const [data, setData] = createSignal(null); * const { maybeExecute } = createAsyncRateLimiter( * async (query) => { * const result = await searchAPI(query); * setData(result); + * return result; // Return value can be used by the caller * }, * { * limit: 10, diff --git a/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts b/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts index bb8d1e3e6..87651d5a9 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimitedSignal.ts @@ -12,6 +12,12 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * subsequent updates until the window resets. Unlike throttling or debouncing, it does not attempt to space out * or intelligently collapse updates. This can lead to bursts of rapid updates followed by periods of no updates. * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All updates within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows updates as old ones expire. This provides a more + * consistent rate of updates over time. + * * For smoother update patterns, consider: * - createThrottledSignal: When you want consistent spacing between updates (e.g. UI changes) * - createDebouncedSignal: When you want to collapse rapid updates into a single update (e.g. search input) @@ -28,16 +34,18 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * * @example * ```tsx - * // Basic rate limiting - update state at most 5 times per minute + * // Basic rate limiting - update state at most 5 times per minute with a sliding window * const [value, setValue, rateLimiter] = createRateLimitedSignal(0, { * limit: 5, - * window: 60000 + * window: 60000, + * windowType: 'sliding' * }); * - * // With rejection callback + * // With rejection callback and fixed window * const [value, setValue] = createRateLimitedSignal(0, { * limit: 3, * window: 5000, + * windowType: 'fixed', * onReject: (rateLimiter) => { * alert(`Rate limit reached. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`); * } diff --git a/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts b/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts index 2d895f870..f4512e817 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimitedValue.ts @@ -12,6 +12,12 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * subsequent updates until the window resets. Unlike throttling or debouncing, it does not attempt to space out * or intelligently collapse updates. This can lead to bursts of rapid updates followed by periods of no updates. * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All updates within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows updates as old ones expire. This provides a more + * consistent rate of updates over time. + * * For smoother update patterns, consider: * - createThrottledValue: When you want consistent spacing between updates (e.g. UI changes) * - createDebouncedValue: When you want to collapse rapid updates into a single update (e.g. search input) @@ -27,10 +33,11 @@ import type { RateLimiterOptions } from '@tanstack/pacer/rate-limiter' * * @example * ```tsx - * // Basic rate limiting - update at most 5 times per minute + * // Basic rate limiting - update at most 5 times per minute with a sliding window * const [rateLimitedValue, rateLimiter] = createRateLimitedValue(rawValue, { * limit: 5, - * window: 60000 + * window: 60000, + * windowType: 'sliding' * }); * * // Use the rate-limited value diff --git a/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts b/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts index be73b8828..0df3f724f 100644 --- a/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts +++ b/packages/solid-pacer/src/rate-limiter/createRateLimiter.ts @@ -29,6 +29,12 @@ export interface SolidRateLimiter * a time window, then blocks all subsequent calls until the window resets. Unlike throttling or debouncing, * it does not attempt to space out or collapse executions intelligently. * + * The rate limiter supports two types of windows: + * - 'fixed': A strict window that resets after the window period. All executions within the window count + * towards the limit, and the window resets completely after the period. + * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more + * consistent rate of execution over time. + * * For smoother execution patterns: * - Use throttling when you want consistent spacing between executions (e.g. UI updates) * - Use debouncing when you want to collapse rapid-fire events (e.g. search input) @@ -36,10 +42,11 @@ export interface SolidRateLimiter * * @example * ```tsx - * // Basic rate limiting - max 5 calls per minute + * // Basic rate limiting - max 5 calls per minute with a sliding window * const rateLimiter = createRateLimiter(apiCall, { * limit: 5, * window: 60000, + * windowType: 'sliding' * }); * * // Monitor rate limit status diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b1be4aba..2f31fcb46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,7 +75,7 @@ importers: examples/react/asyncDebounce: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -100,7 +100,7 @@ importers: examples/react/asyncRateLimit: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -125,7 +125,7 @@ importers: examples/react/asyncThrottle: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -150,7 +150,7 @@ importers: examples/react/debounce: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -175,7 +175,7 @@ importers: examples/react/queue: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -200,7 +200,7 @@ importers: examples/react/rateLimit: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -225,7 +225,7 @@ importers: examples/react/react-query-debounced-prefetch: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer '@tanstack/react-query': specifier: ^5.75.2 @@ -256,7 +256,7 @@ importers: examples/react/react-query-queued-prefetch: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer '@tanstack/react-query': specifier: ^5.75.2 @@ -287,7 +287,7 @@ importers: examples/react/react-query-throttled-prefetch: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer '@tanstack/react-query': specifier: ^5.75.2 @@ -318,7 +318,7 @@ importers: examples/react/throttle: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -343,7 +343,7 @@ importers: examples/react/useAsyncDebouncer: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -368,7 +368,7 @@ importers: examples/react/useAsyncQueuedState: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -393,7 +393,7 @@ importers: examples/react/useAsyncQueuer: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -418,7 +418,7 @@ importers: examples/react/useAsyncRateLimiter: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -443,7 +443,7 @@ importers: examples/react/useAsyncThrottler: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -468,7 +468,7 @@ importers: examples/react/useDebouncedCallback: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -493,7 +493,7 @@ importers: examples/react/useDebouncedState: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -518,7 +518,7 @@ importers: examples/react/useDebouncedValue: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -543,7 +543,7 @@ importers: examples/react/useDebouncer: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -568,7 +568,7 @@ importers: examples/react/useQueuedState: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -593,7 +593,7 @@ importers: examples/react/useQueuedValue: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -618,7 +618,7 @@ importers: examples/react/useQueuer: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -643,7 +643,7 @@ importers: examples/react/useRateLimitedCallback: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -668,7 +668,7 @@ importers: examples/react/useRateLimitedState: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -693,7 +693,7 @@ importers: examples/react/useRateLimitedValue: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -718,7 +718,7 @@ importers: examples/react/useRateLimiter: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -743,7 +743,7 @@ importers: examples/react/useThrottledCallback: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -768,7 +768,7 @@ importers: examples/react/useThrottledState: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -793,7 +793,7 @@ importers: examples/react/useThrottledValue: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -818,7 +818,7 @@ importers: examples/react/useThrottler: dependencies: '@tanstack/react-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/react-pacer react: specifier: ^19.1.0 @@ -843,7 +843,7 @@ importers: examples/solid/asyncDebounce: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -859,7 +859,7 @@ importers: examples/solid/asyncRateLimit: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -875,7 +875,7 @@ importers: examples/solid/asyncThrottle: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -891,7 +891,7 @@ importers: examples/solid/createAsyncDebouncer: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -907,7 +907,7 @@ importers: examples/solid/createAsyncQueuer: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -923,7 +923,7 @@ importers: examples/solid/createAsyncRateLimiter: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -939,7 +939,7 @@ importers: examples/solid/createAsyncThrottler: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -955,7 +955,7 @@ importers: examples/solid/createDebouncedSignal: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -971,7 +971,7 @@ importers: examples/solid/createDebouncedValue: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -987,7 +987,7 @@ importers: examples/solid/createDebouncer: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1003,7 +1003,7 @@ importers: examples/solid/createQueuer: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1019,7 +1019,7 @@ importers: examples/solid/createRateLimitedSignal: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1035,7 +1035,7 @@ importers: examples/solid/createRateLimitedValue: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1051,7 +1051,7 @@ importers: examples/solid/createRateLimiter: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1067,7 +1067,7 @@ importers: examples/solid/createThrottledSignal: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1083,7 +1083,7 @@ importers: examples/solid/createThrottledValue: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1099,7 +1099,7 @@ importers: examples/solid/createThrottler: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1115,7 +1115,7 @@ importers: examples/solid/debounce: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1131,7 +1131,7 @@ importers: examples/solid/queue: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1147,7 +1147,7 @@ importers: examples/solid/rateLimit: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6 @@ -1163,7 +1163,7 @@ importers: examples/solid/throttle: dependencies: '@tanstack/solid-pacer': - specifier: ^0.3.0 + specifier: ^0.4.0 version: link:../../../packages/solid-pacer solid-js: specifier: ^1.9.6