@tanstack/pacer 0.4.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 +39 -15
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +63 -11
- package/dist/cjs/async-queuer.cjs +102 -119
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +94 -52
- package/dist/cjs/async-rate-limiter.cjs +48 -16
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +74 -9
- package/dist/cjs/async-throttler.cjs +42 -17
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +62 -10
- package/dist/cjs/debouncer.cjs +16 -3
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +14 -4
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/queuer.cjs +13 -5
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +9 -3
- package/dist/cjs/rate-limiter.cjs +26 -7
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +20 -6
- package/dist/cjs/throttler.cjs +19 -5
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +15 -4
- package/dist/cjs/types.d.cts +1 -0
- package/dist/cjs/utils.cjs +18 -7
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +3 -0
- package/dist/esm/async-debouncer.d.ts +63 -11
- package/dist/esm/async-debouncer.js +39 -15
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +94 -52
- package/dist/esm/async-queuer.js +102 -119
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +74 -9
- package/dist/esm/async-rate-limiter.js +48 -16
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +62 -10
- package/dist/esm/async-throttler.js +42 -17
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/debouncer.d.ts +14 -4
- package/dist/esm/debouncer.js +16 -3
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/queuer.d.ts +9 -3
- package/dist/esm/queuer.js +13 -5
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +20 -6
- package/dist/esm/rate-limiter.js +26 -7
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +15 -4
- package/dist/esm/throttler.js +19 -5
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/types.d.ts +1 -0
- package/dist/esm/utils.d.ts +3 -0
- package/dist/esm/utils.js +19 -8
- package/dist/esm/utils.js.map +1 -1
- package/package.json +9 -3
- package/src/async-debouncer.ts +89 -22
- package/src/async-queuer.ts +205 -175
- package/src/async-rate-limiter.ts +111 -25
- package/src/async-throttler.ts +92 -24
- package/src/debouncer.ts +24 -7
- package/src/queuer.ts +19 -7
- package/src/rate-limiter.ts +37 -13
- package/src/throttler.ts +28 -9
- package/src/types.ts +3 -0
- package/src/utils.ts +19 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
2
|
+
import type { AnyAsyncFunction, OptionalKeys } from './types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Options for configuring an async rate-limited function
|
|
@@ -6,15 +7,19 @@ import type { AnyAsyncFunction } from './types'
|
|
|
6
7
|
export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
7
8
|
/**
|
|
8
9
|
* Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
10
|
+
* Can be a boolean or a function that returns a boolean.
|
|
9
11
|
* Defaults to true.
|
|
10
12
|
*/
|
|
11
|
-
enabled?: boolean
|
|
13
|
+
enabled?: boolean | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)
|
|
12
14
|
/**
|
|
13
|
-
* Maximum number of executions allowed within the time window
|
|
15
|
+
* Maximum number of executions allowed within the time window.
|
|
16
|
+
* Can be a number or a function that returns a number.
|
|
14
17
|
*/
|
|
15
|
-
limit: number
|
|
18
|
+
limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)
|
|
16
19
|
/**
|
|
17
|
-
* 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.
|
|
18
23
|
*/
|
|
19
24
|
onError?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void
|
|
20
25
|
/**
|
|
@@ -33,9 +38,16 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
33
38
|
rateLimiter: AsyncRateLimiter<TFn>,
|
|
34
39
|
) => void
|
|
35
40
|
/**
|
|
36
|
-
*
|
|
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.
|
|
37
44
|
*/
|
|
38
|
-
|
|
45
|
+
throwOnError?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* Time window in milliseconds within which the limit applies.
|
|
48
|
+
* Can be a number or a function that returns a number.
|
|
49
|
+
*/
|
|
50
|
+
window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)
|
|
39
51
|
/**
|
|
40
52
|
* Type of window to use for rate limiting
|
|
41
53
|
* - 'fixed': Uses a fixed window that resets after the window period
|
|
@@ -45,14 +57,16 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
45
57
|
windowType?: 'fixed' | 'sliding'
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
type AsyncRateLimiterOptionsWithOptionalCallbacks = OptionalKeys<
|
|
61
|
+
AsyncRateLimiterOptions<any>,
|
|
62
|
+
'onError' | 'onReject' | 'onSettled' | 'onSuccess'
|
|
63
|
+
>
|
|
64
|
+
|
|
65
|
+
const defaultOptions: Omit<
|
|
66
|
+
AsyncRateLimiterOptionsWithOptionalCallbacks,
|
|
67
|
+
'limit' | 'window'
|
|
50
68
|
> = {
|
|
51
69
|
enabled: true,
|
|
52
|
-
onError: () => {},
|
|
53
|
-
onReject: () => {},
|
|
54
|
-
onSettled: () => {},
|
|
55
|
-
onSuccess: () => {},
|
|
56
70
|
windowType: 'fixed',
|
|
57
71
|
}
|
|
58
72
|
|
|
@@ -80,11 +94,29 @@ const defaultOptions: Required<
|
|
|
80
94
|
* Rate limiting is best used for hard API limits or resource constraints. For UI updates or
|
|
81
95
|
* smoothing out frequent events, throttling or debouncing usually provide better user experience.
|
|
82
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
|
+
*
|
|
83
105
|
* @example
|
|
84
106
|
* ```ts
|
|
85
107
|
* const rateLimiter = new AsyncRateLimiter(
|
|
86
108
|
* async (id: string) => await api.getData(id),
|
|
87
|
-
* {
|
|
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
|
+
* }
|
|
88
120
|
* );
|
|
89
121
|
*
|
|
90
122
|
* // Will execute immediately until limit reached, then block
|
|
@@ -93,7 +125,7 @@ const defaultOptions: Required<
|
|
|
93
125
|
* ```
|
|
94
126
|
*/
|
|
95
127
|
export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
96
|
-
private _options:
|
|
128
|
+
private _options: AsyncRateLimiterOptionsWithOptionalCallbacks
|
|
97
129
|
private _errorCount = 0
|
|
98
130
|
private _executionTimes: Array<number> = []
|
|
99
131
|
private _lastResult: ReturnType<TFn> | undefined
|
|
@@ -109,6 +141,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
109
141
|
this._options = {
|
|
110
142
|
...defaultOptions,
|
|
111
143
|
...initialOptions,
|
|
144
|
+
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
|
|
112
145
|
}
|
|
113
146
|
}
|
|
114
147
|
|
|
@@ -123,8 +156,29 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
123
156
|
/**
|
|
124
157
|
* Returns the current rate limiter options
|
|
125
158
|
*/
|
|
126
|
-
getOptions():
|
|
127
|
-
return this._options
|
|
159
|
+
getOptions(): AsyncRateLimiterOptions<TFn> {
|
|
160
|
+
return this._options
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Returns the current enabled state of the rate limiter
|
|
165
|
+
*/
|
|
166
|
+
getEnabled(): boolean {
|
|
167
|
+
return !!parseFunctionOrValue(this._options.enabled, this)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns the current limit of executions allowed within the time window
|
|
172
|
+
*/
|
|
173
|
+
getLimit(): number {
|
|
174
|
+
return parseFunctionOrValue(this._options.limit, this)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Returns the current time window in milliseconds
|
|
179
|
+
*/
|
|
180
|
+
getWindow(): number {
|
|
181
|
+
return parseFunctionOrValue(this._options.window, this)
|
|
128
182
|
}
|
|
129
183
|
|
|
130
184
|
/**
|
|
@@ -132,6 +186,19 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
132
186
|
* Will reject execution if the number of calls in the current window exceeds the limit.
|
|
133
187
|
* If execution is allowed, waits for any previous execution to complete before proceeding.
|
|
134
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
|
+
*
|
|
135
202
|
* @example
|
|
136
203
|
* ```ts
|
|
137
204
|
* const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });
|
|
@@ -148,9 +215,12 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
148
215
|
): Promise<ReturnType<TFn> | undefined> {
|
|
149
216
|
this.cleanupOldExecutions()
|
|
150
217
|
|
|
218
|
+
const limit = this.getLimit()
|
|
219
|
+
const window = this.getWindow()
|
|
220
|
+
|
|
151
221
|
if (this._options.windowType === 'sliding') {
|
|
152
222
|
// For sliding window, we can execute if we have capacity in the current window
|
|
153
|
-
if (this._executionTimes.length <
|
|
223
|
+
if (this._executionTimes.length < limit) {
|
|
154
224
|
await this.executeFunction(...args)
|
|
155
225
|
return this._lastResult
|
|
156
226
|
}
|
|
@@ -158,9 +228,9 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
158
228
|
// For fixed window, we need to check if we're in a new window
|
|
159
229
|
const now = Date.now()
|
|
160
230
|
const oldestExecution = Math.min(...this._executionTimes)
|
|
161
|
-
const isNewWindow = oldestExecution +
|
|
231
|
+
const isNewWindow = oldestExecution + window <= now
|
|
162
232
|
|
|
163
|
-
if (isNewWindow || this._executionTimes.length <
|
|
233
|
+
if (isNewWindow || this._executionTimes.length < limit) {
|
|
164
234
|
await this.executeFunction(...args)
|
|
165
235
|
return this._lastResult
|
|
166
236
|
}
|
|
@@ -173,7 +243,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
173
243
|
private async executeFunction(
|
|
174
244
|
...args: Parameters<TFn>
|
|
175
245
|
): Promise<ReturnType<TFn> | undefined> {
|
|
176
|
-
if (!this.
|
|
246
|
+
if (!this.getEnabled()) return
|
|
177
247
|
this._isExecuting = true
|
|
178
248
|
const now = Date.now()
|
|
179
249
|
this._executionTimes.push(now)
|
|
@@ -185,6 +255,11 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
185
255
|
} catch (error) {
|
|
186
256
|
this._errorCount++
|
|
187
257
|
this._options.onError?.(error, this)
|
|
258
|
+
if (this._options.throwOnError) {
|
|
259
|
+
throw error
|
|
260
|
+
} else {
|
|
261
|
+
console.error(error)
|
|
262
|
+
}
|
|
188
263
|
} finally {
|
|
189
264
|
this._isExecuting = false
|
|
190
265
|
this._settleCount++
|
|
@@ -203,7 +278,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
203
278
|
|
|
204
279
|
private cleanupOldExecutions(): void {
|
|
205
280
|
const now = Date.now()
|
|
206
|
-
const windowStart = now - this.
|
|
281
|
+
const windowStart = now - this.getWindow()
|
|
207
282
|
this._executionTimes = this._executionTimes.filter(
|
|
208
283
|
(time) => time > windowStart,
|
|
209
284
|
)
|
|
@@ -214,7 +289,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
214
289
|
*/
|
|
215
290
|
getRemainingInWindow(): number {
|
|
216
291
|
this.cleanupOldExecutions()
|
|
217
|
-
return Math.max(0, this.
|
|
292
|
+
return Math.max(0, this.getLimit() - this._executionTimes.length)
|
|
218
293
|
}
|
|
219
294
|
|
|
220
295
|
/**
|
|
@@ -227,7 +302,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
227
302
|
return 0
|
|
228
303
|
}
|
|
229
304
|
const oldestExecution = Math.min(...this._executionTimes)
|
|
230
|
-
return oldestExecution + this.
|
|
305
|
+
return oldestExecution + this.getWindow() - Date.now()
|
|
231
306
|
}
|
|
232
307
|
|
|
233
308
|
/**
|
|
@@ -298,6 +373,14 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
298
373
|
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
|
|
299
374
|
* need to enforce a hard limit on the number of executions within a time period.
|
|
300
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
|
+
*
|
|
301
384
|
* @example
|
|
302
385
|
* ```ts
|
|
303
386
|
* // Rate limit to 5 calls per minute with a sliding window
|
|
@@ -305,6 +388,9 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
305
388
|
* limit: 5,
|
|
306
389
|
* window: 60000,
|
|
307
390
|
* windowType: 'sliding',
|
|
391
|
+
* onError: (error) => {
|
|
392
|
+
* console.error('API call failed:', error);
|
|
393
|
+
* },
|
|
308
394
|
* onReject: (rateLimiter) => {
|
|
309
395
|
* console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
|
|
310
396
|
* }
|
|
@@ -321,7 +407,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
321
407
|
*/
|
|
322
408
|
export function asyncRateLimit<TFn extends AnyAsyncFunction>(
|
|
323
409
|
fn: TFn,
|
|
324
|
-
initialOptions:
|
|
410
|
+
initialOptions: AsyncRateLimiterOptions<TFn>,
|
|
325
411
|
) {
|
|
326
412
|
const rateLimiter = new AsyncRateLimiter(fn, initialOptions)
|
|
327
413
|
return rateLimiter.maybeExecute.bind(rateLimiter)
|
package/src/async-throttler.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
2
|
+
import type { AnyAsyncFunction, OptionalKeys } from './types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Options for configuring an async throttled function
|
|
@@ -6,16 +7,19 @@ import type { AnyAsyncFunction } from './types'
|
|
|
6
7
|
export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
7
8
|
/**
|
|
8
9
|
* Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
10
|
+
* Can be a boolean or a function that returns a boolean.
|
|
9
11
|
* Defaults to true.
|
|
10
12
|
*/
|
|
11
|
-
enabled?: boolean
|
|
13
|
+
enabled?: boolean | ((throttler: AsyncThrottler<TFn>) => boolean)
|
|
12
14
|
/**
|
|
13
15
|
* Whether to execute the function immediately when called
|
|
14
16
|
* Defaults to true
|
|
15
17
|
*/
|
|
16
18
|
leading?: boolean
|
|
17
19
|
/**
|
|
18
|
-
* 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.
|
|
19
23
|
*/
|
|
20
24
|
onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void
|
|
21
25
|
/**
|
|
@@ -29,24 +33,33 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
|
29
33
|
result: ReturnType<TFn>,
|
|
30
34
|
asyncThrottler: AsyncThrottler<TFn>,
|
|
31
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
|
|
32
42
|
/**
|
|
33
43
|
* Whether to execute the function on the trailing edge of the wait period
|
|
34
44
|
* Defaults to true
|
|
35
45
|
*/
|
|
36
46
|
trailing?: boolean
|
|
37
47
|
/**
|
|
38
|
-
* Time window in milliseconds during which the function can only be executed once
|
|
48
|
+
* Time window in milliseconds during which the function can only be executed once.
|
|
49
|
+
* Can be a number or a function that returns a number.
|
|
39
50
|
* Defaults to 0ms
|
|
40
51
|
*/
|
|
41
|
-
wait: number
|
|
52
|
+
wait: number | ((throttler: AsyncThrottler<TFn>) => number)
|
|
42
53
|
}
|
|
43
54
|
|
|
44
|
-
|
|
55
|
+
type AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<
|
|
56
|
+
AsyncThrottlerOptions<any>,
|
|
57
|
+
'onError' | 'onSettled' | 'onSuccess'
|
|
58
|
+
>
|
|
59
|
+
|
|
60
|
+
const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {
|
|
45
61
|
enabled: true,
|
|
46
62
|
leading: true,
|
|
47
|
-
onError: () => {},
|
|
48
|
-
onSettled: () => {},
|
|
49
|
-
onSuccess: () => {},
|
|
50
63
|
trailing: true,
|
|
51
64
|
wait: 0,
|
|
52
65
|
}
|
|
@@ -65,12 +78,24 @@ const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
|
|
|
65
78
|
* This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to
|
|
66
79
|
* ensure a maximum execution frequency.
|
|
67
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
|
+
*
|
|
68
88
|
* @example
|
|
69
89
|
* ```ts
|
|
70
90
|
* const throttler = new AsyncThrottler(async (value: string) => {
|
|
71
91
|
* const result = await saveToAPI(value);
|
|
72
92
|
* return result; // Return value is preserved
|
|
73
|
-
* }, {
|
|
93
|
+
* }, {
|
|
94
|
+
* wait: 1000,
|
|
95
|
+
* onError: (error) => {
|
|
96
|
+
* console.error('API call failed:', error);
|
|
97
|
+
* }
|
|
98
|
+
* });
|
|
74
99
|
*
|
|
75
100
|
* // Will only execute once per second no matter how often called
|
|
76
101
|
* // Returns the API response directly
|
|
@@ -78,7 +103,7 @@ const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
|
|
|
78
103
|
* ```
|
|
79
104
|
*/
|
|
80
105
|
export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
81
|
-
private _options:
|
|
106
|
+
private _options: AsyncThrottlerOptionsWithOptionalCallbacks
|
|
82
107
|
private _abortController: AbortController | null = null
|
|
83
108
|
private _errorCount = 0
|
|
84
109
|
private _isExecuting = false
|
|
@@ -97,6 +122,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
97
122
|
this._options = {
|
|
98
123
|
...defaultOptions,
|
|
99
124
|
...initialOptions,
|
|
125
|
+
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
|
|
100
126
|
}
|
|
101
127
|
}
|
|
102
128
|
|
|
@@ -116,22 +142,47 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
116
142
|
/**
|
|
117
143
|
* Returns the current options
|
|
118
144
|
*/
|
|
119
|
-
getOptions():
|
|
145
|
+
getOptions(): AsyncThrottlerOptions<TFn> {
|
|
120
146
|
return this._options
|
|
121
147
|
}
|
|
122
148
|
|
|
123
149
|
/**
|
|
124
|
-
*
|
|
125
|
-
|
|
150
|
+
* Returns the current enabled state of the throttler
|
|
151
|
+
*/
|
|
152
|
+
getEnabled(): boolean {
|
|
153
|
+
return !!parseFunctionOrValue(this._options.enabled, this)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Returns the current wait time in milliseconds
|
|
158
|
+
*/
|
|
159
|
+
getWait(): number {
|
|
160
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
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
|
|
126
176
|
*/
|
|
127
177
|
async maybeExecute(
|
|
128
178
|
...args: Parameters<TFn>
|
|
129
179
|
): Promise<ReturnType<TFn> | undefined> {
|
|
130
180
|
const now = Date.now()
|
|
131
181
|
const timeSinceLastExecution = now - this._lastExecutionTime
|
|
182
|
+
const wait = this.getWait()
|
|
132
183
|
|
|
133
184
|
// Handle leading execution
|
|
134
|
-
if (this._options.leading && timeSinceLastExecution >=
|
|
185
|
+
if (this._options.leading && timeSinceLastExecution >= wait) {
|
|
135
186
|
await this.executeFunction(...args)
|
|
136
187
|
return this._lastResult
|
|
137
188
|
} else {
|
|
@@ -149,7 +200,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
149
200
|
const _timeSinceLastExecution = this._lastExecutionTime
|
|
150
201
|
? now - this._lastExecutionTime
|
|
151
202
|
: 0
|
|
152
|
-
const timeoutDuration =
|
|
203
|
+
const timeoutDuration = wait - _timeSinceLastExecution
|
|
153
204
|
this._timeoutId = setTimeout(async () => {
|
|
154
205
|
if (this._lastArgs !== undefined) {
|
|
155
206
|
await this.executeFunction(...this._lastArgs)
|
|
@@ -164,23 +215,28 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
164
215
|
private async executeFunction(
|
|
165
216
|
...args: Parameters<TFn>
|
|
166
217
|
): Promise<ReturnType<TFn> | undefined> {
|
|
167
|
-
if (!this.
|
|
218
|
+
if (!this.getEnabled() || this._isExecuting) return undefined
|
|
168
219
|
this._abortController = new AbortController()
|
|
169
220
|
try {
|
|
170
221
|
this._isExecuting = true
|
|
171
222
|
this._lastResult = await this.fn(...args) // EXECUTE!
|
|
172
223
|
this._successCount++
|
|
173
|
-
this._options.onSuccess(this._lastResult!, this)
|
|
224
|
+
this._options.onSuccess?.(this._lastResult!, this)
|
|
174
225
|
} catch (error) {
|
|
175
226
|
this._errorCount++
|
|
176
|
-
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
|
+
}
|
|
177
233
|
} finally {
|
|
178
234
|
this._isExecuting = false
|
|
179
235
|
this._settleCount++
|
|
180
236
|
this._abortController = null
|
|
181
237
|
this._lastExecutionTime = Date.now()
|
|
182
|
-
this._nextExecutionTime = this._lastExecutionTime + this.
|
|
183
|
-
this._options.onSettled(this)
|
|
238
|
+
this._nextExecutionTime = this._lastExecutionTime + this.getWait()
|
|
239
|
+
this._options.onSettled?.(this)
|
|
184
240
|
}
|
|
185
241
|
return this._lastResult
|
|
186
242
|
}
|
|
@@ -246,7 +302,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
246
302
|
* Returns the current pending state
|
|
247
303
|
*/
|
|
248
304
|
getIsPending(): boolean {
|
|
249
|
-
return this.
|
|
305
|
+
return this.getEnabled() && !!this._timeoutId
|
|
250
306
|
}
|
|
251
307
|
|
|
252
308
|
/**
|
|
@@ -266,12 +322,24 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
266
322
|
* making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
|
|
267
323
|
* instead of setting the result on a state variable from within the throttled function.
|
|
268
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
|
+
*
|
|
269
332
|
* @example
|
|
270
333
|
* ```ts
|
|
271
334
|
* const throttled = asyncThrottle(async (value: string) => {
|
|
272
335
|
* const result = await saveToAPI(value);
|
|
273
336
|
* return result; // Return value is preserved
|
|
274
|
-
* }, {
|
|
337
|
+
* }, {
|
|
338
|
+
* wait: 1000,
|
|
339
|
+
* onError: (error) => {
|
|
340
|
+
* console.error('API call failed:', error);
|
|
341
|
+
* }
|
|
342
|
+
* });
|
|
275
343
|
*
|
|
276
344
|
* // This will execute at most once per second
|
|
277
345
|
* // Returns the API response directly
|
|
@@ -280,7 +348,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
280
348
|
*/
|
|
281
349
|
export function asyncThrottle<TFn extends AnyAsyncFunction>(
|
|
282
350
|
fn: TFn,
|
|
283
|
-
initialOptions:
|
|
351
|
+
initialOptions: AsyncThrottlerOptions<TFn>,
|
|
284
352
|
) {
|
|
285
353
|
const asyncThrottler = new AsyncThrottler(fn, initialOptions)
|
|
286
354
|
return asyncThrottler.maybeExecute.bind(asyncThrottler)
|
package/src/debouncer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
1
2
|
import type { AnyFunction } from './types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -6,9 +7,10 @@ import type { AnyFunction } from './types'
|
|
|
6
7
|
export interface DebouncerOptions<TFn extends AnyFunction> {
|
|
7
8
|
/**
|
|
8
9
|
* Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
10
|
+
* Can be a boolean or a function that returns a boolean.
|
|
9
11
|
* Defaults to true.
|
|
10
12
|
*/
|
|
11
|
-
enabled?: boolean
|
|
13
|
+
enabled?: boolean | ((debouncer: Debouncer<TFn>) => boolean)
|
|
12
14
|
/**
|
|
13
15
|
* Whether to execute on the leading edge of the timeout.
|
|
14
16
|
* The first call will execute immediately and the rest will wait the delay.
|
|
@@ -25,10 +27,11 @@ export interface DebouncerOptions<TFn extends AnyFunction> {
|
|
|
25
27
|
*/
|
|
26
28
|
trailing?: boolean
|
|
27
29
|
/**
|
|
28
|
-
* Delay in milliseconds before executing the function
|
|
30
|
+
* Delay in milliseconds before executing the function.
|
|
31
|
+
* Can be a number or a function that returns a number.
|
|
29
32
|
* Defaults to 0ms
|
|
30
33
|
*/
|
|
31
|
-
wait: number
|
|
34
|
+
wait: number | ((debouncer: Debouncer<TFn>) => number)
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
const defaultOptions: Required<DebouncerOptions<any>> = {
|
|
@@ -99,6 +102,20 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
99
102
|
return this._options
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Returns the current enabled state of the debouncer
|
|
107
|
+
*/
|
|
108
|
+
getEnabled(): boolean {
|
|
109
|
+
return parseFunctionOrValue(this._options.enabled, this)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns the current wait time in milliseconds
|
|
114
|
+
*/
|
|
115
|
+
getWait(): number {
|
|
116
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
/**
|
|
103
120
|
* Attempts to execute the debounced function
|
|
104
121
|
* If a call is already in progress, it will be queued
|
|
@@ -127,11 +144,11 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
127
144
|
if (this._options.trailing && !_didLeadingExecute) {
|
|
128
145
|
this.executeFunction(...args)
|
|
129
146
|
}
|
|
130
|
-
}, this.
|
|
147
|
+
}, this.getWait())
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
private executeFunction(...args: Parameters<TFn>): void {
|
|
134
|
-
if (!this.
|
|
151
|
+
if (!this.getEnabled()) return undefined
|
|
135
152
|
this.fn(...args) // EXECUTE!
|
|
136
153
|
this._isPending = false
|
|
137
154
|
this._executionCount++
|
|
@@ -160,7 +177,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
160
177
|
* Returns `true` if debouncing
|
|
161
178
|
*/
|
|
162
179
|
getIsPending(): boolean {
|
|
163
|
-
return this.
|
|
180
|
+
return this.getEnabled() && this._isPending
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
183
|
|
|
@@ -186,7 +203,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
186
203
|
*/
|
|
187
204
|
export function debounce<TFn extends AnyFunction>(
|
|
188
205
|
fn: TFn,
|
|
189
|
-
initialOptions:
|
|
206
|
+
initialOptions: DebouncerOptions<TFn>,
|
|
190
207
|
): (...args: Parameters<TFn>) => void {
|
|
191
208
|
const debouncer = new Debouncer(fn, initialOptions)
|
|
192
209
|
return debouncer.maybeExecute.bind(debouncer)
|
package/src/queuer.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Options for configuring a Queuer instance
|
|
3
5
|
*/
|
|
@@ -60,9 +62,11 @@ export interface QueuerOptions<TValue> {
|
|
|
60
62
|
*/
|
|
61
63
|
started?: boolean
|
|
62
64
|
/**
|
|
63
|
-
* Time in milliseconds to wait between processing items
|
|
65
|
+
* Time in milliseconds to wait between processing items.
|
|
66
|
+
* Can be a number or a function that returns a number.
|
|
67
|
+
* @default 0
|
|
64
68
|
*/
|
|
65
|
-
wait?: number
|
|
69
|
+
wait?: number | ((queuer: Queuer<TValue>) => number)
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
const defaultOptions: Required<QueuerOptions<any>> = {
|
|
@@ -78,7 +82,7 @@ const defaultOptions: Required<QueuerOptions<any>> = {
|
|
|
78
82
|
onItemsChange: () => {},
|
|
79
83
|
onReject: () => {},
|
|
80
84
|
onExpire: () => {},
|
|
81
|
-
started:
|
|
85
|
+
started: true,
|
|
82
86
|
wait: 0,
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -178,6 +182,13 @@ export class Queuer<TValue> {
|
|
|
178
182
|
return this._options
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Returns the current wait time in milliseconds
|
|
187
|
+
*/
|
|
188
|
+
getWait(): number {
|
|
189
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
190
|
+
}
|
|
191
|
+
|
|
181
192
|
/**
|
|
182
193
|
* Processes items in the queuer
|
|
183
194
|
*/
|
|
@@ -197,9 +208,10 @@ export class Queuer<TValue> {
|
|
|
197
208
|
}
|
|
198
209
|
this._onItemsChanges.forEach((cb) => cb(nextItem))
|
|
199
210
|
|
|
200
|
-
|
|
211
|
+
const wait = this.getWait()
|
|
212
|
+
if (wait > 0) {
|
|
201
213
|
// Use setTimeout to wait before processing next item
|
|
202
|
-
setTimeout(() => this.tick(),
|
|
214
|
+
setTimeout(() => this.tick(), wait)
|
|
203
215
|
return
|
|
204
216
|
}
|
|
205
217
|
|
|
@@ -493,7 +505,7 @@ export class Queuer<TValue> {
|
|
|
493
505
|
* processPriority(3) // Processed before 1
|
|
494
506
|
* ```
|
|
495
507
|
*/
|
|
496
|
-
export function queue<TValue>(options: QueuerOptions<TValue>
|
|
497
|
-
const queuer = new Queuer<TValue>(
|
|
508
|
+
export function queue<TValue>(options: QueuerOptions<TValue>) {
|
|
509
|
+
const queuer = new Queuer<TValue>(options)
|
|
498
510
|
return queuer.addItem.bind(queuer)
|
|
499
511
|
}
|