@tanstack/pacer 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/async-debouncer.cjs +24 -13
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +49 -7
- package/dist/cjs/async-queuer.cjs +83 -114
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +79 -50
- package/dist/cjs/async-rate-limiter.cjs +20 -9
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +53 -3
- package/dist/cjs/async-throttler.cjs +24 -13
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +48 -6
- package/dist/cjs/types.d.cts +1 -0
- package/dist/esm/async-debouncer.d.ts +49 -7
- package/dist/esm/async-debouncer.js +24 -13
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +79 -50
- package/dist/esm/async-queuer.js +83 -114
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +53 -3
- package/dist/esm/async-rate-limiter.js +20 -9
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +48 -6
- package/dist/esm/async-throttler.js +24 -13
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/async-debouncer.ts +66 -16
- package/src/async-queuer.ts +180 -169
- package/src/async-rate-limiter.ts +70 -12
- package/src/async-throttler.ts +66 -16
- package/src/types.ts +3 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseFunctionOrValue } from './utils'
|
|
2
|
-
import type { AnyAsyncFunction } from './types'
|
|
2
|
+
import type { AnyAsyncFunction, OptionalKeys } from './types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Options for configuring an async rate-limited function
|
|
@@ -17,7 +17,9 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
17
17
|
*/
|
|
18
18
|
limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)
|
|
19
19
|
/**
|
|
20
|
-
* Optional error handler for when the rate-limited function throws
|
|
20
|
+
* Optional error handler for when the rate-limited function throws.
|
|
21
|
+
* If provided, the handler will be called with the error and rate limiter instance.
|
|
22
|
+
* This can be used alongside throwOnError - the handler will be called before any error is thrown.
|
|
21
23
|
*/
|
|
22
24
|
onError?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void
|
|
23
25
|
/**
|
|
@@ -35,6 +37,12 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
35
37
|
result: ReturnType<TFn>,
|
|
36
38
|
rateLimiter: AsyncRateLimiter<TFn>,
|
|
37
39
|
) => void
|
|
40
|
+
/**
|
|
41
|
+
* Whether to throw errors when they occur.
|
|
42
|
+
* Defaults to true if no onError handler is provided, false if an onError handler is provided.
|
|
43
|
+
* Can be explicitly set to override these defaults.
|
|
44
|
+
*/
|
|
45
|
+
throwOnError?: boolean
|
|
38
46
|
/**
|
|
39
47
|
* Time window in milliseconds within which the limit applies.
|
|
40
48
|
* Can be a number or a function that returns a number.
|
|
@@ -49,14 +57,16 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
49
57
|
windowType?: 'fixed' | 'sliding'
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
type AsyncRateLimiterOptionsWithOptionalCallbacks = OptionalKeys<
|
|
61
|
+
AsyncRateLimiterOptions<any>,
|
|
62
|
+
'onError' | 'onReject' | 'onSettled' | 'onSuccess'
|
|
63
|
+
>
|
|
64
|
+
|
|
65
|
+
const defaultOptions: Omit<
|
|
66
|
+
AsyncRateLimiterOptionsWithOptionalCallbacks,
|
|
67
|
+
'limit' | 'window'
|
|
54
68
|
> = {
|
|
55
69
|
enabled: true,
|
|
56
|
-
onError: () => {},
|
|
57
|
-
onReject: () => {},
|
|
58
|
-
onSettled: () => {},
|
|
59
|
-
onSuccess: () => {},
|
|
60
70
|
windowType: 'fixed',
|
|
61
71
|
}
|
|
62
72
|
|
|
@@ -84,11 +94,29 @@ const defaultOptions: Required<
|
|
|
84
94
|
* Rate limiting is best used for hard API limits or resource constraints. For UI updates or
|
|
85
95
|
* smoothing out frequent events, throttling or debouncing usually provide better user experience.
|
|
86
96
|
*
|
|
97
|
+
* Error Handling:
|
|
98
|
+
* - If an `onError` handler is provided, it will be called with the error and rate limiter instance
|
|
99
|
+
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
|
|
100
|
+
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
|
|
101
|
+
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
|
|
102
|
+
* - The error state can be checked using the underlying AsyncRateLimiter instance
|
|
103
|
+
* - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler
|
|
104
|
+
*
|
|
87
105
|
* @example
|
|
88
106
|
* ```ts
|
|
89
107
|
* const rateLimiter = new AsyncRateLimiter(
|
|
90
108
|
* async (id: string) => await api.getData(id),
|
|
91
|
-
* {
|
|
109
|
+
* {
|
|
110
|
+
* limit: 5,
|
|
111
|
+
* window: 1000,
|
|
112
|
+
* windowType: 'sliding',
|
|
113
|
+
* onError: (error) => {
|
|
114
|
+
* console.error('API call failed:', error);
|
|
115
|
+
* },
|
|
116
|
+
* onReject: (limiter) => {
|
|
117
|
+
* console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);
|
|
118
|
+
* }
|
|
119
|
+
* }
|
|
92
120
|
* );
|
|
93
121
|
*
|
|
94
122
|
* // Will execute immediately until limit reached, then block
|
|
@@ -97,7 +125,7 @@ const defaultOptions: Required<
|
|
|
97
125
|
* ```
|
|
98
126
|
*/
|
|
99
127
|
export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
100
|
-
private _options:
|
|
128
|
+
private _options: AsyncRateLimiterOptionsWithOptionalCallbacks
|
|
101
129
|
private _errorCount = 0
|
|
102
130
|
private _executionTimes: Array<number> = []
|
|
103
131
|
private _lastResult: ReturnType<TFn> | undefined
|
|
@@ -113,6 +141,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
113
141
|
this._options = {
|
|
114
142
|
...defaultOptions,
|
|
115
143
|
...initialOptions,
|
|
144
|
+
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
|
|
116
145
|
}
|
|
117
146
|
}
|
|
118
147
|
|
|
@@ -127,8 +156,8 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
127
156
|
/**
|
|
128
157
|
* Returns the current rate limiter options
|
|
129
158
|
*/
|
|
130
|
-
getOptions():
|
|
131
|
-
return this._options
|
|
159
|
+
getOptions(): AsyncRateLimiterOptions<TFn> {
|
|
160
|
+
return this._options
|
|
132
161
|
}
|
|
133
162
|
|
|
134
163
|
/**
|
|
@@ -157,6 +186,19 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
157
186
|
* Will reject execution if the number of calls in the current window exceeds the limit.
|
|
158
187
|
* If execution is allowed, waits for any previous execution to complete before proceeding.
|
|
159
188
|
*
|
|
189
|
+
* Error Handling:
|
|
190
|
+
* - If the rate-limited function throws and no `onError` handler is configured,
|
|
191
|
+
* the error will be thrown from this method.
|
|
192
|
+
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
|
|
193
|
+
* and this method will return undefined.
|
|
194
|
+
* - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler
|
|
195
|
+
* will be called if configured.
|
|
196
|
+
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
|
|
197
|
+
* - Rate limit rejections can be tracked using `getRejectionCount()`.
|
|
198
|
+
*
|
|
199
|
+
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
|
|
200
|
+
* @throws The error from the rate-limited function if no onError handler is configured
|
|
201
|
+
*
|
|
160
202
|
* @example
|
|
161
203
|
* ```ts
|
|
162
204
|
* const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });
|
|
@@ -213,6 +255,11 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
213
255
|
} catch (error) {
|
|
214
256
|
this._errorCount++
|
|
215
257
|
this._options.onError?.(error, this)
|
|
258
|
+
if (this._options.throwOnError) {
|
|
259
|
+
throw error
|
|
260
|
+
} else {
|
|
261
|
+
console.error(error)
|
|
262
|
+
}
|
|
216
263
|
} finally {
|
|
217
264
|
this._isExecuting = false
|
|
218
265
|
this._settleCount++
|
|
@@ -326,6 +373,14 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
326
373
|
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
|
|
327
374
|
* need to enforce a hard limit on the number of executions within a time period.
|
|
328
375
|
*
|
|
376
|
+
* Error Handling:
|
|
377
|
+
* - If an `onError` handler is provided, it will be called with the error and rate limiter instance
|
|
378
|
+
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
|
|
379
|
+
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
|
|
380
|
+
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
|
|
381
|
+
* - The error state can be checked using the underlying AsyncRateLimiter instance
|
|
382
|
+
* - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler
|
|
383
|
+
*
|
|
329
384
|
* @example
|
|
330
385
|
* ```ts
|
|
331
386
|
* // Rate limit to 5 calls per minute with a sliding window
|
|
@@ -333,6 +388,9 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
333
388
|
* limit: 5,
|
|
334
389
|
* window: 60000,
|
|
335
390
|
* windowType: 'sliding',
|
|
391
|
+
* onError: (error) => {
|
|
392
|
+
* console.error('API call failed:', error);
|
|
393
|
+
* },
|
|
336
394
|
* onReject: (rateLimiter) => {
|
|
337
395
|
* console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
|
|
338
396
|
* }
|
package/src/async-throttler.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseFunctionOrValue } from './utils'
|
|
2
|
-
import type { AnyAsyncFunction } from './types'
|
|
2
|
+
import type { AnyAsyncFunction, OptionalKeys } from './types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Options for configuring an async throttled function
|
|
@@ -17,7 +17,9 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
|
17
17
|
*/
|
|
18
18
|
leading?: boolean
|
|
19
19
|
/**
|
|
20
|
-
* Optional error handler for when the throttled function throws
|
|
20
|
+
* Optional error handler for when the throttled function throws.
|
|
21
|
+
* If provided, the handler will be called with the error and throttler instance.
|
|
22
|
+
* This can be used alongside throwOnError - the handler will be called before any error is thrown.
|
|
21
23
|
*/
|
|
22
24
|
onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void
|
|
23
25
|
/**
|
|
@@ -31,6 +33,12 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
|
31
33
|
result: ReturnType<TFn>,
|
|
32
34
|
asyncThrottler: AsyncThrottler<TFn>,
|
|
33
35
|
) => void
|
|
36
|
+
/**
|
|
37
|
+
* Whether to throw errors when they occur.
|
|
38
|
+
* Defaults to true if no onError handler is provided, false if an onError handler is provided.
|
|
39
|
+
* Can be explicitly set to override these defaults.
|
|
40
|
+
*/
|
|
41
|
+
throwOnError?: boolean
|
|
34
42
|
/**
|
|
35
43
|
* Whether to execute the function on the trailing edge of the wait period
|
|
36
44
|
* Defaults to true
|
|
@@ -44,12 +52,14 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
|
44
52
|
wait: number | ((throttler: AsyncThrottler<TFn>) => number)
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
type AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<
|
|
56
|
+
AsyncThrottlerOptions<any>,
|
|
57
|
+
'onError' | 'onSettled' | 'onSuccess'
|
|
58
|
+
>
|
|
59
|
+
|
|
60
|
+
const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {
|
|
48
61
|
enabled: true,
|
|
49
62
|
leading: true,
|
|
50
|
-
onError: () => {},
|
|
51
|
-
onSettled: () => {},
|
|
52
|
-
onSuccess: () => {},
|
|
53
63
|
trailing: true,
|
|
54
64
|
wait: 0,
|
|
55
65
|
}
|
|
@@ -68,12 +78,24 @@ const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
|
|
|
68
78
|
* This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to
|
|
69
79
|
* ensure a maximum execution frequency.
|
|
70
80
|
*
|
|
81
|
+
* Error Handling:
|
|
82
|
+
* - If an `onError` handler is provided, it will be called with the error and throttler instance
|
|
83
|
+
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
|
|
84
|
+
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
|
|
85
|
+
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
|
|
86
|
+
* - The error state can be checked using the underlying AsyncThrottler instance
|
|
87
|
+
*
|
|
71
88
|
* @example
|
|
72
89
|
* ```ts
|
|
73
90
|
* const throttler = new AsyncThrottler(async (value: string) => {
|
|
74
91
|
* const result = await saveToAPI(value);
|
|
75
92
|
* return result; // Return value is preserved
|
|
76
|
-
* }, {
|
|
93
|
+
* }, {
|
|
94
|
+
* wait: 1000,
|
|
95
|
+
* onError: (error) => {
|
|
96
|
+
* console.error('API call failed:', error);
|
|
97
|
+
* }
|
|
98
|
+
* });
|
|
77
99
|
*
|
|
78
100
|
* // Will only execute once per second no matter how often called
|
|
79
101
|
* // Returns the API response directly
|
|
@@ -81,7 +103,7 @@ const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
|
|
|
81
103
|
* ```
|
|
82
104
|
*/
|
|
83
105
|
export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
84
|
-
private _options:
|
|
106
|
+
private _options: AsyncThrottlerOptionsWithOptionalCallbacks
|
|
85
107
|
private _abortController: AbortController | null = null
|
|
86
108
|
private _errorCount = 0
|
|
87
109
|
private _isExecuting = false
|
|
@@ -100,6 +122,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
100
122
|
this._options = {
|
|
101
123
|
...defaultOptions,
|
|
102
124
|
...initialOptions,
|
|
125
|
+
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
|
|
103
126
|
}
|
|
104
127
|
}
|
|
105
128
|
|
|
@@ -119,7 +142,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
119
142
|
/**
|
|
120
143
|
* Returns the current options
|
|
121
144
|
*/
|
|
122
|
-
getOptions():
|
|
145
|
+
getOptions(): AsyncThrottlerOptions<TFn> {
|
|
123
146
|
return this._options
|
|
124
147
|
}
|
|
125
148
|
|
|
@@ -127,7 +150,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
127
150
|
* Returns the current enabled state of the throttler
|
|
128
151
|
*/
|
|
129
152
|
getEnabled(): boolean {
|
|
130
|
-
return parseFunctionOrValue(this._options.enabled, this)
|
|
153
|
+
return !!parseFunctionOrValue(this._options.enabled, this)
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
/**
|
|
@@ -138,8 +161,18 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
138
161
|
}
|
|
139
162
|
|
|
140
163
|
/**
|
|
141
|
-
* Attempts to execute the throttled function
|
|
142
|
-
* If a call is already in progress, it may be blocked or queued depending on the `wait` option
|
|
164
|
+
* Attempts to execute the throttled function.
|
|
165
|
+
* If a call is already in progress, it may be blocked or queued depending on the `wait` option.
|
|
166
|
+
*
|
|
167
|
+
* Error Handling:
|
|
168
|
+
* - If the throttled function throws and no `onError` handler is configured,
|
|
169
|
+
* the error will be thrown from this method.
|
|
170
|
+
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
|
|
171
|
+
* and this method will return undefined.
|
|
172
|
+
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
|
|
173
|
+
*
|
|
174
|
+
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
|
|
175
|
+
* @throws The error from the throttled function if no onError handler is configured
|
|
143
176
|
*/
|
|
144
177
|
async maybeExecute(
|
|
145
178
|
...args: Parameters<TFn>
|
|
@@ -188,17 +221,22 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
188
221
|
this._isExecuting = true
|
|
189
222
|
this._lastResult = await this.fn(...args) // EXECUTE!
|
|
190
223
|
this._successCount++
|
|
191
|
-
this._options.onSuccess(this._lastResult!, this)
|
|
224
|
+
this._options.onSuccess?.(this._lastResult!, this)
|
|
192
225
|
} catch (error) {
|
|
193
226
|
this._errorCount++
|
|
194
|
-
this._options.onError(error, this)
|
|
227
|
+
this._options.onError?.(error, this)
|
|
228
|
+
if (this._options.throwOnError) {
|
|
229
|
+
throw error
|
|
230
|
+
} else {
|
|
231
|
+
console.error(error)
|
|
232
|
+
}
|
|
195
233
|
} finally {
|
|
196
234
|
this._isExecuting = false
|
|
197
235
|
this._settleCount++
|
|
198
236
|
this._abortController = null
|
|
199
237
|
this._lastExecutionTime = Date.now()
|
|
200
238
|
this._nextExecutionTime = this._lastExecutionTime + this.getWait()
|
|
201
|
-
this._options.onSettled(this)
|
|
239
|
+
this._options.onSettled?.(this)
|
|
202
240
|
}
|
|
203
241
|
return this._lastResult
|
|
204
242
|
}
|
|
@@ -284,12 +322,24 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
284
322
|
* making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
|
|
285
323
|
* instead of setting the result on a state variable from within the throttled function.
|
|
286
324
|
*
|
|
325
|
+
* Error Handling:
|
|
326
|
+
* - If an `onError` handler is provided, it will be called with the error and throttler instance
|
|
327
|
+
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
|
|
328
|
+
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
|
|
329
|
+
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
|
|
330
|
+
* - The error state can be checked using the underlying AsyncThrottler instance
|
|
331
|
+
*
|
|
287
332
|
* @example
|
|
288
333
|
* ```ts
|
|
289
334
|
* const throttled = asyncThrottle(async (value: string) => {
|
|
290
335
|
* const result = await saveToAPI(value);
|
|
291
336
|
* return result; // Return value is preserved
|
|
292
|
-
* }, {
|
|
337
|
+
* }, {
|
|
338
|
+
* wait: 1000,
|
|
339
|
+
* onError: (error) => {
|
|
340
|
+
* console.error('API call failed:', error);
|
|
341
|
+
* }
|
|
342
|
+
* });
|
|
293
343
|
*
|
|
294
344
|
* // This will execute at most once per second
|
|
295
345
|
* // Returns the API response directly
|
package/src/types.ts
CHANGED
|
@@ -7,3 +7,6 @@ export type AnyFunction = (...args: Array<any>) => any
|
|
|
7
7
|
* Represents an asynchronous function that can be called with any arguments and returns a promise.
|
|
8
8
|
*/
|
|
9
9
|
export type AnyAsyncFunction = (...args: Array<any>) => Promise<any>
|
|
10
|
+
|
|
11
|
+
export type OptionalKeys<T, TKey extends keyof T> = Omit<T, TKey> &
|
|
12
|
+
Partial<Pick<T, TKey>>
|