@tanstack/pacer 0.15.4 → 0.16.1
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-batcher.cjs +66 -5
- package/dist/cjs/async-batcher.cjs.map +1 -1
- package/dist/cjs/async-batcher.d.cts +94 -17
- package/dist/cjs/async-debouncer.cjs +53 -19
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +76 -10
- package/dist/cjs/async-queuer.cjs +58 -2
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +100 -10
- package/dist/cjs/async-rate-limiter.cjs +51 -2
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +91 -28
- package/dist/cjs/async-retryer.cjs +285 -0
- package/dist/cjs/async-retryer.cjs.map +1 -0
- package/dist/cjs/async-retryer.d.cts +308 -0
- package/dist/cjs/async-throttler.cjs +101 -53
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +82 -10
- package/dist/cjs/batcher.cjs +5 -1
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +10 -2
- package/dist/cjs/debouncer.cjs +5 -1
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +7 -3
- package/dist/cjs/event-client.cjs +3 -1
- package/dist/cjs/event-client.cjs.map +1 -1
- package/dist/cjs/event-client.d.cts +3 -0
- package/dist/cjs/index.cjs +13 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/queuer.cjs +5 -1
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +8 -4
- package/dist/cjs/rate-limiter.cjs +5 -1
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +8 -1
- package/dist/cjs/throttler.cjs +5 -1
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +7 -0
- package/dist/cjs/utils.cjs +0 -10
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +0 -1
- package/dist/esm/async-batcher.d.ts +94 -17
- package/dist/esm/async-batcher.js +68 -7
- package/dist/esm/async-batcher.js.map +1 -1
- package/dist/esm/async-debouncer.d.ts +76 -10
- package/dist/esm/async-debouncer.js +55 -21
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +100 -10
- package/dist/esm/async-queuer.js +60 -4
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +91 -28
- package/dist/esm/async-rate-limiter.js +53 -4
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-retryer.d.ts +308 -0
- package/dist/esm/async-retryer.js +285 -0
- package/dist/esm/async-retryer.js.map +1 -0
- package/dist/esm/async-throttler.d.ts +82 -10
- package/dist/esm/async-throttler.js +103 -55
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +10 -2
- package/dist/esm/batcher.js +6 -2
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/debouncer.d.ts +7 -3
- package/dist/esm/debouncer.js +7 -3
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/event-client.d.ts +3 -0
- package/dist/esm/event-client.js +3 -1
- package/dist/esm/event-client.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +24 -12
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +8 -4
- package/dist/esm/queuer.js +7 -3
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +8 -1
- package/dist/esm/rate-limiter.js +7 -3
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +7 -0
- package/dist/esm/throttler.js +7 -3
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.d.ts +0 -1
- package/dist/esm/utils.js +0 -10
- package/dist/esm/utils.js.map +1 -1
- package/package.json +13 -3
- package/src/async-batcher.ts +144 -22
- package/src/async-debouncer.ts +118 -33
- package/src/async-queuer.ts +147 -14
- package/src/async-rate-limiter.ts +129 -33
- package/src/async-retryer.ts +664 -0
- package/src/async-throttler.ts +196 -75
- package/src/batcher.ts +16 -4
- package/src/debouncer.ts +17 -5
- package/src/event-client.ts +6 -1
- package/src/index.ts +2 -0
- package/src/queuer.ts +19 -6
- package/src/rate-limiter.ts +18 -3
- package/src/throttler.ts +17 -2
- package/src/utils.ts +0 -15
package/src/async-throttler.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
|
-
import {
|
|
2
|
+
import { AsyncRetryer } from './async-retryer'
|
|
3
|
+
import { parseFunctionOrValue } from './utils'
|
|
3
4
|
import { emitChange, pacerEventClient } from './event-client'
|
|
5
|
+
import type { AsyncRetryerOptions } from './async-retryer'
|
|
4
6
|
import type { AnyAsyncFunction, OptionalKeys } from './types'
|
|
5
7
|
|
|
6
8
|
export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
|
|
@@ -72,6 +74,10 @@ function getDefaultAsyncThrottlerState<
|
|
|
72
74
|
* Options for configuring an async throttled function
|
|
73
75
|
*/
|
|
74
76
|
export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
77
|
+
/**
|
|
78
|
+
* Options for configuring the underlying async retryer
|
|
79
|
+
*/
|
|
80
|
+
asyncRetryerOptions?: AsyncRetryerOptions<TFn>
|
|
75
81
|
/**
|
|
76
82
|
* Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
77
83
|
* Can be a boolean or a function that returns a boolean.
|
|
@@ -98,7 +104,7 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
|
98
104
|
* This can be used alongside throwOnError - the handler will be called before any error is thrown.
|
|
99
105
|
*/
|
|
100
106
|
onError?: (
|
|
101
|
-
error:
|
|
107
|
+
error: Error,
|
|
102
108
|
args: Parameters<TFn>,
|
|
103
109
|
asyncThrottler: AsyncThrottler<TFn>,
|
|
104
110
|
) => void
|
|
@@ -136,12 +142,27 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
|
136
142
|
wait: number | ((throttler: AsyncThrottler<TFn>) => number)
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Utility function for sharing common `AsyncThrottlerOptions` options between different `AsyncThrottler` instances.
|
|
147
|
+
*/
|
|
148
|
+
export function asyncThrottlerOptions<
|
|
149
|
+
TFn extends AnyAsyncFunction = AnyAsyncFunction,
|
|
150
|
+
TOptions extends Partial<AsyncThrottlerOptions<TFn>> = Partial<
|
|
151
|
+
AsyncThrottlerOptions<TFn>
|
|
152
|
+
>,
|
|
153
|
+
>(options: TOptions): TOptions {
|
|
154
|
+
return options
|
|
155
|
+
}
|
|
156
|
+
|
|
139
157
|
type AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<
|
|
140
158
|
AsyncThrottlerOptions<any>,
|
|
141
159
|
'initialState' | 'onError' | 'onSettled' | 'onSuccess'
|
|
142
160
|
>
|
|
143
161
|
|
|
144
162
|
const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {
|
|
163
|
+
asyncRetryerOptions: {
|
|
164
|
+
maxAttempts: 1,
|
|
165
|
+
},
|
|
145
166
|
enabled: true,
|
|
146
167
|
leading: true,
|
|
147
168
|
trailing: true,
|
|
@@ -151,14 +172,24 @@ const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {
|
|
|
151
172
|
/**
|
|
152
173
|
* A class that creates an async throttled function.
|
|
153
174
|
*
|
|
175
|
+
* Async vs Sync Versions:
|
|
176
|
+
* The async version provides advanced features over the sync Throttler:
|
|
177
|
+
* - Returns promises that can be awaited for throttled function results
|
|
178
|
+
* - Built-in retry support via AsyncRetryer integration
|
|
179
|
+
* - Abort support to cancel in-flight executions
|
|
180
|
+
* - Cancel support to prevent pending executions from starting
|
|
181
|
+
* - Comprehensive error handling with onError callbacks and throwOnError control
|
|
182
|
+
* - Detailed execution tracking (success/error/settle counts)
|
|
183
|
+
* - Waits for ongoing executions to complete before scheduling the next one
|
|
184
|
+
*
|
|
185
|
+
* The sync Throttler is lighter weight and simpler when you don't need async features,
|
|
186
|
+
* return values, or execution control.
|
|
187
|
+
*
|
|
188
|
+
* What is Throttling?
|
|
154
189
|
* Throttling limits how often a function can be executed, allowing only one execution within a specified time window.
|
|
155
190
|
* Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a
|
|
156
191
|
* regular interval regardless of how often it's called.
|
|
157
192
|
*
|
|
158
|
-
* Unlike the non-async Throttler, this async version supports returning values from the throttled function,
|
|
159
|
-
* making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
|
|
160
|
-
* instead of setting the result on a state variable from within the throttled function.
|
|
161
|
-
*
|
|
162
193
|
* This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to
|
|
163
194
|
* ensure a maximum execution frequency.
|
|
164
195
|
*
|
|
@@ -200,9 +231,9 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
200
231
|
readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<
|
|
201
232
|
AsyncThrottlerState<TFn>
|
|
202
233
|
>(getDefaultAsyncThrottlerState<TFn>())
|
|
203
|
-
key: string
|
|
234
|
+
key: string | undefined
|
|
204
235
|
options: AsyncThrottlerOptions<TFn>
|
|
205
|
-
|
|
236
|
+
asyncRetryers = new Map<number, AsyncRetryer<TFn>>()
|
|
206
237
|
#timeoutId: NodeJS.Timeout | null = null
|
|
207
238
|
#resolvePreviousPromise:
|
|
208
239
|
| ((value?: ReturnType<TFn> | undefined) => void)
|
|
@@ -212,13 +243,14 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
212
243
|
public fn: TFn,
|
|
213
244
|
initialOptions: AsyncThrottlerOptions<TFn>,
|
|
214
245
|
) {
|
|
215
|
-
this.key =
|
|
246
|
+
this.key = initialOptions.key
|
|
216
247
|
this.options = {
|
|
217
248
|
...defaultOptions,
|
|
218
249
|
...initialOptions,
|
|
219
250
|
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
|
|
220
251
|
}
|
|
221
252
|
this.#setState(this.options.initialState ?? {})
|
|
253
|
+
|
|
222
254
|
pacerEventClient.on('d-AsyncThrottler', (event) => {
|
|
223
255
|
if (event.payload.key !== this.key) return
|
|
224
256
|
this.#setState(event.payload.store.state as AsyncThrottlerState<TFn>)
|
|
@@ -301,88 +333,119 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
301
333
|
...args: Parameters<TFn>
|
|
302
334
|
): Promise<ReturnType<TFn> | undefined> => {
|
|
303
335
|
if (!this.#getEnabled()) return undefined
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// Store the most recent arguments for potential trailing execution
|
|
336
|
+
|
|
337
|
+
this.#resolvePreviousPromiseInternal()
|
|
338
|
+
|
|
308
339
|
this.#setState({
|
|
309
|
-
lastArgs: args,
|
|
310
340
|
maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
|
|
341
|
+
lastArgs: args, // store the arguments for potential trailing execution
|
|
311
342
|
})
|
|
312
343
|
|
|
313
|
-
this.#
|
|
344
|
+
const wait = this.#getWait()
|
|
345
|
+
const thisMaybeExecuteNumber = this.store.state.maybeExecuteCount
|
|
314
346
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
347
|
+
// Wait for the wait period for the previous execution to complete if it's still running
|
|
348
|
+
for (
|
|
349
|
+
let maxNumIterations = wait / 10;
|
|
350
|
+
this.store.state.isExecuting && maxNumIterations > 0;
|
|
351
|
+
maxNumIterations--
|
|
352
|
+
) {
|
|
353
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
354
|
+
if (this.store.state.maybeExecuteCount !== thisMaybeExecuteNumber) {
|
|
355
|
+
// cancel the current maybeExecute loop because a new maybeExecute call was made
|
|
356
|
+
return this.store.state.lastResult
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const now = Date.now()
|
|
361
|
+
const timeSinceLastExecution = now - this.store.state.lastExecutionTime
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
this.options.leading &&
|
|
365
|
+
!this.store.state.isPending &&
|
|
366
|
+
timeSinceLastExecution >= wait
|
|
367
|
+
) {
|
|
368
|
+
await this.#execute(...args) // Leading EXECUTE!
|
|
369
|
+
} else if (this.options.trailing) {
|
|
370
|
+
// replace old pending execution with a new one
|
|
371
|
+
this.cancel()
|
|
372
|
+
this.#setState({
|
|
373
|
+
isPending: true,
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
// Set up new trailing execution
|
|
320
377
|
return new Promise((resolve, reject) => {
|
|
321
378
|
this.#resolvePreviousPromise = resolve
|
|
322
|
-
|
|
323
|
-
this
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
await this.#execute(...this.store.state.lastArgs) // EXECUTE!
|
|
336
|
-
} catch (error) {
|
|
337
|
-
reject(error)
|
|
338
|
-
}
|
|
379
|
+
|
|
380
|
+
const newTimeSinceLastExecution = this.store.state.lastExecutionTime
|
|
381
|
+
? now - this.store.state.lastExecutionTime
|
|
382
|
+
: 0
|
|
383
|
+
const timeoutDuration = Math.max(0, wait - newTimeSinceLastExecution)
|
|
384
|
+
|
|
385
|
+
this.#timeoutId = setTimeout(async () => {
|
|
386
|
+
this.#clearTimeout()
|
|
387
|
+
if (this.store.state.lastArgs !== undefined) {
|
|
388
|
+
try {
|
|
389
|
+
await this.#execute(...this.store.state.lastArgs) // Trailing EXECUTE!
|
|
390
|
+
} catch (error) {
|
|
391
|
+
reject(error)
|
|
339
392
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
393
|
+
}
|
|
394
|
+
this.#resolvePreviousPromise = null
|
|
395
|
+
resolve(this.store.state.lastResult)
|
|
396
|
+
}, timeoutDuration)
|
|
344
397
|
})
|
|
345
398
|
}
|
|
399
|
+
return this.store.state.lastResult
|
|
346
400
|
}
|
|
347
401
|
|
|
348
402
|
#execute = async (
|
|
349
403
|
...args: Parameters<TFn>
|
|
350
404
|
): Promise<ReturnType<TFn> | undefined> => {
|
|
351
|
-
if (!this.#getEnabled()
|
|
352
|
-
|
|
405
|
+
if (!this.#getEnabled()) return undefined
|
|
406
|
+
|
|
407
|
+
const currentMaybeExecute = this.store.state.maybeExecuteCount
|
|
408
|
+
|
|
353
409
|
try {
|
|
354
410
|
this.#setState({ isExecuting: true })
|
|
355
|
-
const
|
|
411
|
+
const currentAsyncRetryer = new AsyncRetryer(this.fn, {
|
|
412
|
+
...this.options.asyncRetryerOptions,
|
|
413
|
+
key: `${this.key}-retryer-${currentMaybeExecute}`,
|
|
414
|
+
})
|
|
415
|
+
this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)
|
|
416
|
+
const result = await currentAsyncRetryer.execute(...args) // EXECUTE!
|
|
356
417
|
this.#setState({
|
|
357
418
|
lastResult: result,
|
|
358
419
|
successCount: this.store.state.successCount + 1,
|
|
359
420
|
})
|
|
360
|
-
this.options.onSuccess?.(result
|
|
421
|
+
this.options.onSuccess?.(result as ReturnType<TFn>, args, this)
|
|
361
422
|
} catch (error) {
|
|
362
423
|
this.#setState({
|
|
363
424
|
errorCount: this.store.state.errorCount + 1,
|
|
364
425
|
})
|
|
365
|
-
this.options.onError?.(error, args, this)
|
|
426
|
+
this.options.onError?.(error as Error, args, this)
|
|
366
427
|
if (this.options.throwOnError) {
|
|
367
428
|
throw error
|
|
368
429
|
}
|
|
369
430
|
} finally {
|
|
431
|
+
this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer
|
|
370
432
|
const lastExecutionTime = Date.now()
|
|
371
|
-
const
|
|
433
|
+
const wait = this.#getWait()
|
|
434
|
+
const nextExecutionTime = lastExecutionTime + wait
|
|
372
435
|
this.#setState({
|
|
373
436
|
isExecuting: false,
|
|
374
|
-
isPending:
|
|
437
|
+
isPending: !!this.#timeoutId,
|
|
375
438
|
settleCount: this.store.state.settleCount + 1,
|
|
376
439
|
lastExecutionTime,
|
|
377
440
|
nextExecutionTime,
|
|
378
441
|
})
|
|
379
|
-
this.#abortController = null
|
|
380
442
|
this.options.onSettled?.(args, this)
|
|
381
443
|
setTimeout(() => {
|
|
382
444
|
if (!this.store.state.isPending) {
|
|
445
|
+
// clear nextExecutionTime if there is no pending execution
|
|
383
446
|
this.#setState({ nextExecutionTime: undefined })
|
|
384
447
|
}
|
|
385
|
-
},
|
|
448
|
+
}, wait)
|
|
386
449
|
}
|
|
387
450
|
return this.store.state.lastResult
|
|
388
451
|
}
|
|
@@ -392,12 +455,21 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
392
455
|
*/
|
|
393
456
|
flush = async (): Promise<ReturnType<TFn> | undefined> => {
|
|
394
457
|
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
395
|
-
|
|
396
|
-
|
|
458
|
+
// Store the pending promise resolver before clearing timeout
|
|
459
|
+
const resolvePromise = this.#resolvePreviousPromise
|
|
460
|
+
|
|
461
|
+
// Clear timeout and state without resolving the promise
|
|
462
|
+
this.#clearTimeout()
|
|
463
|
+
this.#setState({
|
|
464
|
+
isPending: false,
|
|
465
|
+
})
|
|
466
|
+
|
|
397
467
|
const result = await this.#execute(...this.store.state.lastArgs)
|
|
398
468
|
|
|
399
|
-
// Resolve
|
|
400
|
-
|
|
469
|
+
// Resolve the pending promise with the result
|
|
470
|
+
if (resolvePromise) {
|
|
471
|
+
resolvePromise(result)
|
|
472
|
+
}
|
|
401
473
|
|
|
402
474
|
return result
|
|
403
475
|
}
|
|
@@ -418,7 +490,51 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
418
490
|
}
|
|
419
491
|
}
|
|
420
492
|
|
|
421
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Returns the AbortSignal for a specific execution.
|
|
495
|
+
* If no maybeExecuteCount is provided, returns the signal for the most recent execution.
|
|
496
|
+
* Returns null if no execution is found or not currently executing.
|
|
497
|
+
*
|
|
498
|
+
* @param maybeExecuteCount - Optional specific execution to get signal for
|
|
499
|
+
* @example
|
|
500
|
+
* ```typescript
|
|
501
|
+
* const throttler = new AsyncThrottler(
|
|
502
|
+
* async (data: string) => {
|
|
503
|
+
* const signal = throttler.getAbortSignal()
|
|
504
|
+
* if (signal) {
|
|
505
|
+
* const response = await fetch('/api/save', {
|
|
506
|
+
* method: 'POST',
|
|
507
|
+
* body: data,
|
|
508
|
+
* signal
|
|
509
|
+
* })
|
|
510
|
+
* return response.json()
|
|
511
|
+
* }
|
|
512
|
+
* },
|
|
513
|
+
* { wait: 1000 }
|
|
514
|
+
* )
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
517
|
+
getAbortSignal(maybeExecuteCount?: number): AbortSignal | null {
|
|
518
|
+
const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount
|
|
519
|
+
const retryer = this.asyncRetryers.get(count)
|
|
520
|
+
return retryer?.getAbortSignal() ?? null
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Aborts all ongoing executions with the internal abort controllers.
|
|
525
|
+
* Does NOT cancel any pending execution that have not started yet.
|
|
526
|
+
*/
|
|
527
|
+
abort = (): void => {
|
|
528
|
+
this.asyncRetryers.forEach((retryer) => retryer.abort())
|
|
529
|
+
this.asyncRetryers.clear()
|
|
530
|
+
this.#setState({ isExecuting: false })
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Cancels any pending execution that have not started yet.
|
|
535
|
+
* Does NOT abort any execution already in progress.
|
|
536
|
+
*/
|
|
537
|
+
cancel = (): void => {
|
|
422
538
|
this.#clearTimeout()
|
|
423
539
|
if (this.#resolvePreviousPromise) {
|
|
424
540
|
this.#resolvePreviousPromiseInternal()
|
|
@@ -426,31 +542,15 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
426
542
|
}
|
|
427
543
|
this.#setState({
|
|
428
544
|
isPending: false,
|
|
429
|
-
isExecuting: false,
|
|
430
|
-
lastArgs: undefined,
|
|
431
545
|
})
|
|
432
546
|
}
|
|
433
547
|
|
|
434
|
-
#abortExecution = (): void => {
|
|
435
|
-
if (this.#abortController) {
|
|
436
|
-
this.#abortController.abort()
|
|
437
|
-
this.#abortController = null
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Cancels any pending execution or aborts any execution in progress
|
|
443
|
-
*/
|
|
444
|
-
cancel = (): void => {
|
|
445
|
-
this.#cancelPendingExecution()
|
|
446
|
-
this.#abortExecution()
|
|
447
|
-
}
|
|
448
|
-
|
|
449
548
|
/**
|
|
450
549
|
* Resets the debouncer state to its default values
|
|
451
550
|
*/
|
|
452
551
|
reset = (): void => {
|
|
453
552
|
this.#setState(getDefaultAsyncThrottlerState<TFn>())
|
|
553
|
+
this.asyncRetryers.forEach((retryer) => retryer.reset())
|
|
454
554
|
}
|
|
455
555
|
}
|
|
456
556
|
|
|
@@ -459,9 +559,30 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
459
559
|
* The throttled function will execute at most once per wait period, even if called multiple times.
|
|
460
560
|
* If called while executing, it will wait until execution completes before scheduling the next call.
|
|
461
561
|
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
562
|
+
* Async vs Sync Versions:
|
|
563
|
+
* The async version provides advanced features over the sync throttle function:
|
|
564
|
+
* - Returns promises that can be awaited for throttled function results
|
|
565
|
+
* - Built-in retry support via AsyncRetryer integration
|
|
566
|
+
* - Abort support to cancel in-flight executions
|
|
567
|
+
* - Cancel support to prevent pending executions from starting
|
|
568
|
+
* - Comprehensive error handling with onError callbacks and throwOnError control
|
|
569
|
+
* - Detailed execution tracking (success/error/settle counts)
|
|
570
|
+
* - Waits for ongoing executions to complete before scheduling the next one
|
|
571
|
+
*
|
|
572
|
+
* The sync throttle function is lighter weight and simpler when you don't need async features,
|
|
573
|
+
* return values, or execution control.
|
|
574
|
+
*
|
|
575
|
+
* What is Throttling?
|
|
576
|
+
* Throttling limits how often a function can be executed, allowing only one execution within a specified time window.
|
|
577
|
+
* Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a
|
|
578
|
+
* regular interval regardless of how often it's called.
|
|
579
|
+
*
|
|
580
|
+
* Configuration Options:
|
|
581
|
+
* - `wait`: Time window in milliseconds during which the function can only execute once (required)
|
|
582
|
+
* - `leading`: Execute immediately when called (default: true)
|
|
583
|
+
* - `trailing`: Execute on the trailing edge of the wait period (default: true)
|
|
584
|
+
* - `enabled`: Whether the throttler is enabled (default: true)
|
|
585
|
+
* - `asyncRetryerOptions`: Configure retry behavior for executions
|
|
465
586
|
*
|
|
466
587
|
* Error Handling:
|
|
467
588
|
* - If an `onError` handler is provided, it will be called with the error and throttler instance
|
package/src/batcher.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
|
-
import {
|
|
2
|
+
import { parseFunctionOrValue } from './utils'
|
|
3
3
|
import { emitChange, pacerEventClient } from './event-client'
|
|
4
4
|
import type { OptionalKeys } from './types'
|
|
5
5
|
|
|
@@ -107,6 +107,7 @@ const defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {
|
|
|
107
107
|
* A class that collects items and processes them in batches.
|
|
108
108
|
*
|
|
109
109
|
* Batching is a technique for grouping multiple operations together to be processed as a single unit.
|
|
110
|
+
* This synchronous version is lighter weight and often all you need - upgrade to AsyncBatcher when you need promises, retry support, abort/cancel capabilities, or advanced error handling.
|
|
110
111
|
*
|
|
111
112
|
* The Batcher provides a flexible way to implement batching with configurable:
|
|
112
113
|
* - Maximum batch size (number of items per batch)
|
|
@@ -145,7 +146,7 @@ export class Batcher<TValue> {
|
|
|
145
146
|
readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(
|
|
146
147
|
getDefaultBatcherState<TValue>(),
|
|
147
148
|
)
|
|
148
|
-
key: string
|
|
149
|
+
key: string | undefined
|
|
149
150
|
options: BatcherOptionsWithOptionalCallbacks<TValue>
|
|
150
151
|
#timeoutId: NodeJS.Timeout | null = null
|
|
151
152
|
|
|
@@ -153,7 +154,7 @@ export class Batcher<TValue> {
|
|
|
153
154
|
public fn: (items: Array<TValue>) => void,
|
|
154
155
|
initialOptions: BatcherOptions<TValue>,
|
|
155
156
|
) {
|
|
156
|
-
this.key =
|
|
157
|
+
this.key = initialOptions.key
|
|
157
158
|
this.options = {
|
|
158
159
|
...defaultOptions,
|
|
159
160
|
...initialOptions,
|
|
@@ -275,6 +276,15 @@ export class Batcher<TValue> {
|
|
|
275
276
|
this.#setState({ items: [], isPending: false })
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Cancels any pending execution that was scheduled.
|
|
281
|
+
* Does NOT clear out the items.
|
|
282
|
+
*/
|
|
283
|
+
cancel = (): void => {
|
|
284
|
+
this.#clearTimeout()
|
|
285
|
+
this.#setState({ isPending: false })
|
|
286
|
+
}
|
|
287
|
+
|
|
278
288
|
/**
|
|
279
289
|
* Resets the batcher state to its default values
|
|
280
290
|
*/
|
|
@@ -285,7 +295,9 @@ export class Batcher<TValue> {
|
|
|
285
295
|
}
|
|
286
296
|
|
|
287
297
|
/**
|
|
288
|
-
* Creates a batcher that processes items in batches
|
|
298
|
+
* Creates a batcher that processes items in batches.
|
|
299
|
+
*
|
|
300
|
+
* This synchronous version is lighter weight and often all you need - upgrade to asyncBatch when you need promises, retry support, abort/cancel capabilities, or advanced error handling.
|
|
289
301
|
*
|
|
290
302
|
* @example
|
|
291
303
|
* ```ts
|
package/src/debouncer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
|
-
import {
|
|
2
|
+
import { parseFunctionOrValue } from './utils'
|
|
3
3
|
import { emitChange, pacerEventClient } from './event-client'
|
|
4
4
|
import type { AnyFunction } from './types'
|
|
5
5
|
|
|
@@ -85,6 +85,18 @@ export interface DebouncerOptions<TFn extends AnyFunction> {
|
|
|
85
85
|
wait: number | ((debouncer: Debouncer<TFn>) => number)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Utility function for sharing common `DebouncerOptions` options between different `Debouncer` instances.
|
|
90
|
+
*/
|
|
91
|
+
export function debouncerOptions<
|
|
92
|
+
TFn extends AnyFunction = AnyFunction,
|
|
93
|
+
TOptions extends Partial<DebouncerOptions<TFn>> = Partial<
|
|
94
|
+
DebouncerOptions<TFn>
|
|
95
|
+
>,
|
|
96
|
+
>(options: TOptions): TOptions {
|
|
97
|
+
return options
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
const defaultOptions: Omit<
|
|
89
101
|
Required<DebouncerOptions<any>>,
|
|
90
102
|
'initialState' | 'onExecute' | 'key'
|
|
@@ -101,6 +113,7 @@ const defaultOptions: Omit<
|
|
|
101
113
|
* Debouncing ensures that a function is only executed after a certain amount of time has passed
|
|
102
114
|
* since its last invocation. This is useful for handling frequent events like window resizing,
|
|
103
115
|
* scroll events, or input changes where you want to limit the rate of execution.
|
|
116
|
+
* This synchronous version is lighter weight and often all you need - upgrade to AsyncDebouncer when you need promises, retry support, abort/cancel capabilities, or advanced error handling.
|
|
104
117
|
*
|
|
105
118
|
* The debounced function can be configured to execute either at the start of the delay period
|
|
106
119
|
* (leading edge) or at the end (trailing edge, default). Each new call during the wait period
|
|
@@ -130,7 +143,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
130
143
|
readonly store: Store<Readonly<DebouncerState<TFn>>> = new Store(
|
|
131
144
|
getDefaultDebouncerState<TFn>(),
|
|
132
145
|
)
|
|
133
|
-
key: string
|
|
146
|
+
key: string | undefined
|
|
134
147
|
options: DebouncerOptions<TFn>
|
|
135
148
|
#timeoutId: NodeJS.Timeout | undefined
|
|
136
149
|
|
|
@@ -138,7 +151,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
138
151
|
public fn: TFn,
|
|
139
152
|
initialOptions: DebouncerOptions<TFn>,
|
|
140
153
|
) {
|
|
141
|
-
this.key =
|
|
154
|
+
this.key = initialOptions.key
|
|
142
155
|
this.options = {
|
|
143
156
|
...defaultOptions,
|
|
144
157
|
...initialOptions,
|
|
@@ -285,8 +298,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
285
298
|
* Creates a debounced function that delays invoking the provided function until after a specified wait time.
|
|
286
299
|
* Multiple calls during the wait period will cancel previous pending invocations and reset the timer.
|
|
287
300
|
*
|
|
288
|
-
* This
|
|
289
|
-
* more control over the debouncing behavior, use the Debouncer class directly.
|
|
301
|
+
* This synchronous version is lighter weight and often all you need - upgrade to asyncDebounce when you need promises, retry support, abort/cancel capabilities, or advanced error handling.
|
|
290
302
|
*
|
|
291
303
|
* If leading option is true, the function will execute immediately on the first call, then wait the delay
|
|
292
304
|
* before allowing another execution.
|
package/src/event-client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AsyncBatcher } from './async-batcher'
|
|
|
3
3
|
import type { AsyncDebouncer } from './async-debouncer'
|
|
4
4
|
import type { AsyncQueuer } from './async-queuer'
|
|
5
5
|
import type { AsyncRateLimiter } from './async-rate-limiter'
|
|
6
|
+
import type { AsyncRetryer } from './async-retryer'
|
|
6
7
|
import type { AsyncThrottler } from './async-throttler'
|
|
7
8
|
import type { Debouncer } from './debouncer'
|
|
8
9
|
import type { Batcher } from './batcher'
|
|
@@ -15,6 +16,7 @@ export interface PacerEventMap {
|
|
|
15
16
|
'pacer:d-AsyncDebouncer': AsyncDebouncer<any>
|
|
16
17
|
'pacer:d-AsyncQueuer': AsyncQueuer<any>
|
|
17
18
|
'pacer:d-AsyncRateLimiter': AsyncRateLimiter<any>
|
|
19
|
+
'pacer:d-AsyncRetryer': AsyncRetryer<any>
|
|
18
20
|
'pacer:d-AsyncThrottler': AsyncThrottler<any>
|
|
19
21
|
'pacer:d-Batcher': Batcher<any>
|
|
20
22
|
'pacer:d-Debouncer': Debouncer<any>
|
|
@@ -25,6 +27,7 @@ export interface PacerEventMap {
|
|
|
25
27
|
'pacer:AsyncDebouncer': AsyncDebouncer<any>
|
|
26
28
|
'pacer:AsyncQueuer': AsyncQueuer<any>
|
|
27
29
|
'pacer:AsyncRateLimiter': AsyncRateLimiter<any>
|
|
30
|
+
'pacer:AsyncRetryer': AsyncRetryer<any>
|
|
28
31
|
'pacer:AsyncThrottler': AsyncThrottler<any>
|
|
29
32
|
'pacer:Batcher': Batcher<any>
|
|
30
33
|
'pacer:Debouncer': Debouncer<any>
|
|
@@ -57,7 +60,9 @@ export const emitChange = <
|
|
|
57
60
|
event: TSuffix,
|
|
58
61
|
payload: PacerEventMap[`pacer:${TSuffix}`],
|
|
59
62
|
) => {
|
|
60
|
-
|
|
63
|
+
if (payload.key) {
|
|
64
|
+
pacerEventClient.emit(event, payload)
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
export const pacerEventClient = new PacerEventClient()
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './async-batcher'
|
|
|
2
2
|
export * from './async-debouncer'
|
|
3
3
|
export * from './async-queuer'
|
|
4
4
|
export * from './async-rate-limiter'
|
|
5
|
+
export * from './async-retryer'
|
|
5
6
|
export * from './async-throttler'
|
|
6
7
|
export * from './batcher'
|
|
7
8
|
export * from './debouncer'
|
|
@@ -10,5 +11,6 @@ export * from './rate-limiter'
|
|
|
10
11
|
export * from './throttler'
|
|
11
12
|
export * from './types'
|
|
12
13
|
export * from './utils'
|
|
14
|
+
|
|
13
15
|
export { pacerEventClient } from './event-client'
|
|
14
16
|
export type { PacerEventMap, PacerEventName } from './event-client'
|
package/src/queuer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
|
-
import {
|
|
2
|
+
import { parseFunctionOrValue } from './utils'
|
|
3
3
|
import { emitChange, pacerEventClient } from './event-client'
|
|
4
4
|
|
|
5
5
|
export interface QueuerState<TValue> {
|
|
@@ -151,6 +151,18 @@ export interface QueuerOptions<TValue> {
|
|
|
151
151
|
wait?: number | ((queuer: Queuer<TValue>) => number)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Utility function for sharing common `QueuerOptions` options between different `Queuer` instances.
|
|
156
|
+
*/
|
|
157
|
+
export function queuerOptions<
|
|
158
|
+
TValue = any,
|
|
159
|
+
TOptions extends Partial<QueuerOptions<TValue>> = Partial<
|
|
160
|
+
QueuerOptions<TValue>
|
|
161
|
+
>,
|
|
162
|
+
>(options: TOptions): TOptions {
|
|
163
|
+
return options
|
|
164
|
+
}
|
|
165
|
+
|
|
154
166
|
const defaultOptions: Omit<
|
|
155
167
|
Required<QueuerOptions<any>>,
|
|
156
168
|
| 'initialState'
|
|
@@ -183,6 +195,8 @@ export type QueuePosition = 'front' | 'back'
|
|
|
183
195
|
/**
|
|
184
196
|
* A flexible queue that processes items with configurable wait times, expiration, and priority.
|
|
185
197
|
*
|
|
198
|
+
* This synchronous version is lighter weight and often all you need - upgrade to AsyncQueuer when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.
|
|
199
|
+
*
|
|
186
200
|
* Features:
|
|
187
201
|
* - Automatic or manual processing of items
|
|
188
202
|
* - FIFO (First In First Out), LIFO (Last In First Out), or double-ended queue behavior
|
|
@@ -256,7 +270,7 @@ export class Queuer<TValue> {
|
|
|
256
270
|
readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(
|
|
257
271
|
getDefaultQueuerState<TValue>(),
|
|
258
272
|
)
|
|
259
|
-
key: string
|
|
273
|
+
key: string | undefined
|
|
260
274
|
options: QueuerOptions<TValue>
|
|
261
275
|
#timeoutId: NodeJS.Timeout | null = null
|
|
262
276
|
|
|
@@ -264,7 +278,7 @@ export class Queuer<TValue> {
|
|
|
264
278
|
public fn: (item: TValue) => void,
|
|
265
279
|
initialOptions: QueuerOptions<TValue> = {},
|
|
266
280
|
) {
|
|
267
|
-
this.key =
|
|
281
|
+
this.key = initialOptions.key
|
|
268
282
|
this.options = {
|
|
269
283
|
...defaultOptions,
|
|
270
284
|
...initialOptions,
|
|
@@ -287,6 +301,7 @@ export class Queuer<TValue> {
|
|
|
287
301
|
this.addItem(item, this.options.addItemsTo ?? 'back', isLast)
|
|
288
302
|
}
|
|
289
303
|
}
|
|
304
|
+
|
|
290
305
|
pacerEventClient.on('d-Queuer', (event) => {
|
|
291
306
|
if (event.payload.key !== this.key) return
|
|
292
307
|
this.#setState(event.payload.store.state)
|
|
@@ -681,9 +696,7 @@ export class Queuer<TValue> {
|
|
|
681
696
|
* Creates a queue that processes items immediately upon addition.
|
|
682
697
|
* Items are processed sequentially in FIFO order by default.
|
|
683
698
|
*
|
|
684
|
-
* This is
|
|
685
|
-
* `addItem` method. The queue is always isRunning and will process items as they are added.
|
|
686
|
-
* For more control over queue processing, use the Queuer class directly.
|
|
699
|
+
* This synchronous version is lighter weight and often all you need - upgrade to asyncQueue when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.
|
|
687
700
|
*
|
|
688
701
|
* State Management:
|
|
689
702
|
* - Uses TanStack Store for reactive state management
|