@tanstack/pacer 0.22.0 → 0.23.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.
Files changed (100) hide show
  1. package/README.md +4 -4
  2. package/dist/async-batcher.d.ts +11 -13
  3. package/dist/async-batcher.js +185 -131
  4. package/dist/async-debouncer.d.ts +6 -8
  5. package/dist/async-debouncer.js +186 -142
  6. package/dist/async-queuer.d.ts +11 -13
  7. package/dist/async-queuer.js +383 -290
  8. package/dist/async-rate-limiter.d.ts +6 -8
  9. package/dist/async-rate-limiter.js +197 -143
  10. package/dist/async-retryer.d.ts +6 -8
  11. package/dist/async-retryer.js +209 -179
  12. package/dist/async-throttler.d.ts +6 -8
  13. package/dist/async-throttler.js +214 -157
  14. package/dist/batcher.d.ts +5 -7
  15. package/dist/batcher.js +107 -87
  16. package/dist/debouncer.d.ts +6 -8
  17. package/dist/debouncer.js +99 -86
  18. package/dist/event-client.d.ts +8 -10
  19. package/dist/event-client.js +1 -2
  20. package/dist/queuer.d.ts +7 -9
  21. package/dist/queuer.js +277 -212
  22. package/dist/rate-limiter.d.ts +6 -8
  23. package/dist/rate-limiter.js +127 -109
  24. package/dist/throttler.d.ts +6 -8
  25. package/dist/throttler.js +127 -90
  26. package/dist/types.d.ts +4 -6
  27. package/dist/utils.d.ts +3 -5
  28. package/dist/utils.js +1 -2
  29. package/package.json +21 -68
  30. package/dist/async-batcher.cjs +0 -337
  31. package/dist/async-batcher.cjs.map +0 -1
  32. package/dist/async-batcher.d.cts +0 -343
  33. package/dist/async-batcher.js.map +0 -1
  34. package/dist/async-debouncer.cjs +0 -327
  35. package/dist/async-debouncer.cjs.map +0 -1
  36. package/dist/async-debouncer.d.cts +0 -299
  37. package/dist/async-debouncer.js.map +0 -1
  38. package/dist/async-queuer.cjs +0 -516
  39. package/dist/async-queuer.cjs.map +0 -1
  40. package/dist/async-queuer.d.cts +0 -440
  41. package/dist/async-queuer.js.map +0 -1
  42. package/dist/async-rate-limiter.cjs +0 -370
  43. package/dist/async-rate-limiter.cjs.map +0 -1
  44. package/dist/async-rate-limiter.d.cts +0 -356
  45. package/dist/async-rate-limiter.js.map +0 -1
  46. package/dist/async-retryer.cjs +0 -365
  47. package/dist/async-retryer.cjs.map +0 -1
  48. package/dist/async-retryer.d.cts +0 -321
  49. package/dist/async-retryer.js.map +0 -1
  50. package/dist/async-throttler.cjs +0 -344
  51. package/dist/async-throttler.cjs.map +0 -1
  52. package/dist/async-throttler.d.cts +0 -319
  53. package/dist/async-throttler.js.map +0 -1
  54. package/dist/batcher.cjs +0 -200
  55. package/dist/batcher.cjs.map +0 -1
  56. package/dist/batcher.d.cts +0 -179
  57. package/dist/batcher.js.map +0 -1
  58. package/dist/debouncer.cjs +0 -203
  59. package/dist/debouncer.cjs.map +0 -1
  60. package/dist/debouncer.d.cts +0 -166
  61. package/dist/debouncer.js.map +0 -1
  62. package/dist/event-client.cjs +0 -64
  63. package/dist/event-client.cjs.map +0 -1
  64. package/dist/event-client.d.cts +0 -65
  65. package/dist/event-client.js.map +0 -1
  66. package/dist/index.cjs +0 -52
  67. package/dist/index.d.cts +0 -15
  68. package/dist/queuer.cjs +0 -406
  69. package/dist/queuer.cjs.map +0 -1
  70. package/dist/queuer.d.cts +0 -345
  71. package/dist/queuer.js.map +0 -1
  72. package/dist/rate-limiter.cjs +0 -263
  73. package/dist/rate-limiter.cjs.map +0 -1
  74. package/dist/rate-limiter.d.cts +0 -214
  75. package/dist/rate-limiter.js.map +0 -1
  76. package/dist/throttler.cjs +0 -215
  77. package/dist/throttler.cjs.map +0 -1
  78. package/dist/throttler.d.cts +0 -206
  79. package/dist/throttler.js.map +0 -1
  80. package/dist/types.cjs +0 -0
  81. package/dist/types.d.cts +0 -13
  82. package/dist/utils.cjs +0 -13
  83. package/dist/utils.cjs.map +0 -1
  84. package/dist/utils.d.cts +0 -7
  85. package/dist/utils.js.map +0 -1
  86. package/src/async-batcher.ts +0 -594
  87. package/src/async-debouncer.ts +0 -566
  88. package/src/async-queuer.ts +0 -988
  89. package/src/async-rate-limiter.ts +0 -648
  90. package/src/async-retryer.ts +0 -673
  91. package/src/async-throttler.ts +0 -634
  92. package/src/batcher.ts +0 -329
  93. package/src/debouncer.ts +0 -334
  94. package/src/event-client.ts +0 -129
  95. package/src/index.ts +0 -24
  96. package/src/queuer.ts +0 -751
  97. package/src/rate-limiter.ts +0 -429
  98. package/src/throttler.ts +0 -380
  99. package/src/types.ts +0 -12
  100. package/src/utils.ts +0 -12
@@ -1,634 +0,0 @@
1
- import { Store } from '@tanstack/store'
2
- import { AsyncRetryer } from './async-retryer'
3
- import { parseFunctionOrValue } from './utils'
4
- import { emitChange, pacerEventClient } from './event-client'
5
- import type { AsyncRetryerOptions } from './async-retryer'
6
- import type { AnyAsyncFunction, OptionalKeys } from './types'
7
-
8
- export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
9
- /**
10
- * Number of function executions that have resulted in errors
11
- */
12
- errorCount: number
13
- /**
14
- * Whether the throttled function is currently executing asynchronously
15
- */
16
- isExecuting: boolean
17
- /**
18
- * Whether the throttler is waiting for the timeout to trigger execution
19
- */
20
- isPending: boolean
21
- /**
22
- * The arguments from the most recent call to maybeExecute
23
- */
24
- lastArgs: Parameters<TFn> | undefined
25
- /**
26
- * Timestamp of the last function execution in milliseconds
27
- */
28
- lastExecutionTime: number
29
- /**
30
- * The result from the most recent successful function execution
31
- */
32
- lastResult: Awaited<ReturnType<TFn>> | undefined
33
- /**
34
- * Number of times maybeExecute has been called (for reduction calculations)
35
- */
36
- maybeExecuteCount: number
37
- /**
38
- * Timestamp when the next execution can occur in milliseconds
39
- */
40
- nextExecutionTime: number | undefined
41
- /**
42
- * Number of function executions that have completed (either successfully or with errors)
43
- */
44
- settleCount: number
45
- /**
46
- * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed
47
- */
48
- status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'
49
- /**
50
- * Number of function executions that have completed successfully
51
- */
52
- successCount: number
53
- }
54
-
55
- function getDefaultAsyncThrottlerState<
56
- TFn extends AnyAsyncFunction,
57
- >(): AsyncThrottlerState<TFn> {
58
- return {
59
- errorCount: 0,
60
- isExecuting: false,
61
- isPending: false,
62
- lastArgs: undefined,
63
- lastExecutionTime: 0,
64
- lastResult: undefined,
65
- maybeExecuteCount: 0,
66
- nextExecutionTime: undefined,
67
- settleCount: 0,
68
- status: 'idle',
69
- successCount: 0,
70
- }
71
- }
72
-
73
- /**
74
- * Options for configuring an async throttled function
75
- */
76
- export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
77
- /**
78
- * Options for configuring the underlying async retryer
79
- */
80
- asyncRetryerOptions?: AsyncRetryerOptions<TFn>
81
- /**
82
- * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
83
- * Can be a boolean or a function that returns a boolean.
84
- * Defaults to true.
85
- */
86
- enabled?: boolean | ((throttler: AsyncThrottler<TFn>) => boolean)
87
- /**
88
- * Initial state for the async throttler
89
- */
90
- initialState?: Partial<AsyncThrottlerState<TFn>>
91
- /**
92
- * Optional key to identify this async throttler instance.
93
- * If provided, the async throttler will be identified by this key in the devtools and PacerProvider if applicable.
94
- */
95
- key?: string
96
- /**
97
- * Whether to execute the function immediately when called
98
- * Defaults to true
99
- */
100
- leading?: boolean
101
- /**
102
- * Optional error handler for when the throttled function throws.
103
- * If provided, the handler will be called with the error and throttler instance.
104
- * This can be used alongside throwOnError - the handler will be called before any error is thrown.
105
- */
106
- onError?: (
107
- error: Error,
108
- args: Parameters<TFn>,
109
- asyncThrottler: AsyncThrottler<TFn>,
110
- ) => void
111
- /**
112
- * Optional function to call when the throttled function is executed
113
- */
114
- onSettled?: (
115
- args: Parameters<TFn>,
116
- asyncThrottler: AsyncThrottler<TFn>,
117
- ) => void
118
- /**
119
- * Optional function to call when the throttled function is executed
120
- */
121
- onSuccess?: (
122
- result: Awaited<ReturnType<TFn>>,
123
- args: Parameters<TFn>,
124
- asyncThrottler: AsyncThrottler<TFn>,
125
- ) => void
126
- /**
127
- * Whether to throw errors when they occur.
128
- * Defaults to true if no onError handler is provided, false if an onError handler is provided.
129
- * Can be explicitly set to override these defaults.
130
- */
131
- throwOnError?: boolean
132
- /**
133
- * Whether to execute the function on the trailing edge of the wait period
134
- * Defaults to true
135
- */
136
- trailing?: boolean
137
- /**
138
- * Time window in milliseconds during which the function can only be executed once.
139
- * Can be a number or a function that returns a number.
140
- * Defaults to 0ms
141
- */
142
- wait: number | ((throttler: AsyncThrottler<TFn>) => number)
143
- }
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
-
157
- type AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<
158
- AsyncThrottlerOptions<any>,
159
- 'initialState' | 'onError' | 'onSettled' | 'onSuccess'
160
- >
161
-
162
- const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {
163
- asyncRetryerOptions: {
164
- maxAttempts: 1,
165
- },
166
- enabled: true,
167
- leading: true,
168
- trailing: true,
169
- wait: 0,
170
- }
171
-
172
- /**
173
- * A class that creates an async throttled function.
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?
189
- * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.
190
- * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a
191
- * regular interval regardless of how often it's called.
192
- *
193
- * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to
194
- * ensure a maximum execution frequency.
195
- *
196
- * Error Handling:
197
- * - If an `onError` handler is provided, it will be called with the error and throttler instance
198
- * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
199
- * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
200
- * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
201
- * - The error state can be checked using the underlying AsyncThrottler instance
202
- *
203
- * State Management:
204
- * - Uses TanStack Store for reactive state management
205
- * - Use `initialState` to provide initial state values when creating the async throttler
206
- * - Use `onSuccess` callback to react to successful function execution and implement custom logic
207
- * - Use `onError` callback to react to function execution errors and implement custom error handling
208
- * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
209
- * - The state includes error count, execution status, last execution time, and success/settle counts
210
- * - State can be accessed via `asyncThrottler.store.state` when using the class directly
211
- * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`
212
- *
213
- * @example
214
- * ```ts
215
- * const throttler = new AsyncThrottler(async (value: string) => {
216
- * const result = await saveToAPI(value);
217
- * return result; // Return value is preserved
218
- * }, {
219
- * wait: 1000,
220
- * onError: (error) => {
221
- * console.error('API call failed:', error);
222
- * }
223
- * });
224
- *
225
- * // Will only execute once per second no matter how often called
226
- * // Returns the API response directly
227
- * const result = await throttler.maybeExecute(inputElement.value);
228
- * ```
229
- */
230
- export class AsyncThrottler<TFn extends AnyAsyncFunction> {
231
- readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<
232
- AsyncThrottlerState<TFn>
233
- >(getDefaultAsyncThrottlerState<TFn>())
234
- key: string | undefined
235
- options: AsyncThrottlerOptions<TFn>
236
- asyncRetryers = new Map<number, AsyncRetryer<TFn>>()
237
- #timeoutId: ReturnType<typeof setTimeout> | null = null
238
- #resolvePreviousPromise:
239
- ((value?: Awaited<ReturnType<TFn>> | undefined) => void) | null = null
240
-
241
- constructor(
242
- public fn: TFn,
243
- initialOptions: AsyncThrottlerOptions<TFn>,
244
- ) {
245
- this.key = initialOptions.key
246
- this.options = {
247
- ...defaultOptions,
248
- ...initialOptions,
249
- throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
250
- }
251
- this.#setState(this.options.initialState ?? {})
252
-
253
- if (this.key) {
254
- pacerEventClient.on('d-AsyncThrottler', (event) => {
255
- if (event.payload.key !== this.key) return
256
- this.#setState(
257
- event.payload.store.state as Partial<AsyncThrottlerState<TFn>>,
258
- )
259
- this.setOptions(
260
- event.payload.options as Partial<AsyncThrottlerOptions<TFn>>,
261
- )
262
- })
263
- }
264
- }
265
-
266
- /**
267
- * Updates the async throttler options
268
- */
269
- setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {
270
- this.options = { ...this.options, ...newOptions }
271
-
272
- // End the pending state if the throttler is disabled
273
- if (!this.#getEnabled()) {
274
- this.cancel()
275
- }
276
- }
277
-
278
- #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {
279
- this.store.setState((state) => {
280
- const combinedState = {
281
- ...state,
282
- ...newState,
283
- }
284
- const { isPending, isExecuting, settleCount } = combinedState
285
- return {
286
- ...combinedState,
287
- status: !this.#getEnabled()
288
- ? 'disabled'
289
- : isPending
290
- ? 'pending'
291
- : isExecuting
292
- ? 'executing'
293
- : settleCount > 0
294
- ? 'settled'
295
- : 'idle',
296
- }
297
- })
298
- emitChange('AsyncThrottler', this)
299
- }
300
-
301
- /**
302
- * Returns the current enabled state of the async throttler
303
- */
304
- #getEnabled = (): boolean => {
305
- return !!parseFunctionOrValue(this.options.enabled, this)
306
- }
307
-
308
- /**
309
- * Returns the current wait time in milliseconds
310
- */
311
- #getWait = (): number => {
312
- return parseFunctionOrValue(this.options.wait, this)
313
- }
314
-
315
- /**
316
- * Attempts to execute the throttled function. The execution behavior depends on the throttler options:
317
- *
318
- * - If enough time has passed since the last execution (>= wait period):
319
- * - With leading=true: Executes immediately
320
- * - With leading=false: Waits for the next trailing execution
321
- *
322
- * - If within the wait period:
323
- * - With trailing=true: Schedules execution for end of wait period
324
- * - With trailing=false: Drops the execution
325
- *
326
- * @example
327
- * ```ts
328
- * const throttled = new AsyncThrottler(fn, { wait: 1000 });
329
- *
330
- * // First call executes immediately
331
- * await throttled.maybeExecute('a', 'b');
332
- *
333
- * // Call during wait period - gets throttled
334
- * await throttled.maybeExecute('c', 'd');
335
- * ```
336
- */
337
- maybeExecute = async (
338
- ...args: Parameters<TFn>
339
- ): Promise<Awaited<ReturnType<TFn>> | undefined> => {
340
- if (!this.#getEnabled()) return undefined
341
-
342
- this.#resolvePreviousPromiseInternal()
343
-
344
- this.#setState({
345
- maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
346
- lastArgs: args, // store the arguments for potential trailing execution
347
- })
348
-
349
- const wait = this.#getWait()
350
- const thisMaybeExecuteNumber = this.store.state.maybeExecuteCount
351
-
352
- // Wait for the wait period for the previous execution to complete if it's still running
353
- for (
354
- let maxNumIterations = wait / 10;
355
- this.store.state.isExecuting && maxNumIterations > 0;
356
- maxNumIterations--
357
- ) {
358
- await new Promise((resolve) => setTimeout(resolve, 10))
359
- if (this.store.state.maybeExecuteCount !== thisMaybeExecuteNumber) {
360
- // cancel the current maybeExecute loop because a new maybeExecute call was made
361
- return this.store.state.lastResult
362
- }
363
- }
364
-
365
- const now = Date.now()
366
- const timeSinceLastExecution = now - this.store.state.lastExecutionTime
367
-
368
- if (
369
- this.options.leading &&
370
- !this.store.state.isPending &&
371
- timeSinceLastExecution >= wait
372
- ) {
373
- await this.#execute(...args) // Leading EXECUTE!
374
- } else if (this.options.trailing) {
375
- // replace old pending execution with a new one
376
- this.cancel()
377
- this.#setState({
378
- isPending: true,
379
- })
380
-
381
- // Set up new trailing execution
382
- return new Promise<Awaited<ReturnType<TFn>> | undefined>(
383
- (resolve, reject) => {
384
- this.#resolvePreviousPromise = resolve
385
-
386
- const newTimeSinceLastExecution = this.store.state.lastExecutionTime
387
- ? now - this.store.state.lastExecutionTime
388
- : 0
389
- const timeoutDuration = Math.max(0, wait - newTimeSinceLastExecution)
390
-
391
- this.#timeoutId = setTimeout(async () => {
392
- this.#clearTimeout()
393
- if (this.store.state.lastArgs !== undefined) {
394
- try {
395
- await this.#execute(...this.store.state.lastArgs) // Trailing EXECUTE!
396
- } catch (error) {
397
- reject(error)
398
- }
399
- }
400
- this.#resolvePreviousPromise = null
401
- resolve(this.store.state.lastResult)
402
- }, timeoutDuration)
403
- },
404
- )
405
- }
406
- return this.store.state.lastResult
407
- }
408
-
409
- #execute = async (
410
- ...args: Parameters<TFn>
411
- ): Promise<Awaited<ReturnType<TFn>> | undefined> => {
412
- if (!this.#getEnabled()) return undefined
413
-
414
- const currentMaybeExecute = this.store.state.maybeExecuteCount
415
-
416
- try {
417
- this.#setState({ isExecuting: true })
418
- const currentAsyncRetryer = new AsyncRetryer(
419
- this.fn,
420
- this.options.asyncRetryerOptions,
421
- )
422
- this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)
423
- const result = await currentAsyncRetryer.execute(...args) // EXECUTE!
424
- this.#setState({
425
- lastResult: result,
426
- successCount: this.store.state.successCount + 1,
427
- })
428
- this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)
429
- } catch (error) {
430
- this.#setState({
431
- errorCount: this.store.state.errorCount + 1,
432
- })
433
- this.options.onError?.(error as Error, args, this)
434
- if (this.options.throwOnError) {
435
- throw error
436
- }
437
- } finally {
438
- this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer
439
- const lastExecutionTime = Date.now()
440
- const wait = this.#getWait()
441
- const nextExecutionTime = lastExecutionTime + wait
442
- this.#setState({
443
- isExecuting: false,
444
- isPending: !!this.#timeoutId,
445
- settleCount: this.store.state.settleCount + 1,
446
- lastExecutionTime,
447
- nextExecutionTime,
448
- })
449
- this.options.onSettled?.(args, this)
450
- setTimeout(() => {
451
- if (!this.store.state.isPending) {
452
- // clear nextExecutionTime if there is no pending execution
453
- this.#setState({ nextExecutionTime: undefined })
454
- }
455
- }, wait)
456
- }
457
- return this.store.state.lastResult
458
- }
459
-
460
- /**
461
- * Processes the current pending execution immediately
462
- */
463
- flush = async (): Promise<Awaited<ReturnType<TFn>> | undefined> => {
464
- if (this.store.state.isPending && this.store.state.lastArgs) {
465
- // Store the pending promise resolver before clearing timeout
466
- const resolvePromise = this.#resolvePreviousPromise
467
-
468
- // Clear timeout and state without resolving the promise
469
- this.#clearTimeout()
470
- this.#setState({
471
- isPending: false,
472
- })
473
-
474
- const result = await this.#execute(...this.store.state.lastArgs)
475
-
476
- // Resolve the pending promise with the result
477
- if (resolvePromise) {
478
- resolvePromise(result)
479
- }
480
-
481
- return result
482
- }
483
- return undefined
484
- }
485
-
486
- #resolvePreviousPromiseInternal = (): void => {
487
- if (this.#resolvePreviousPromise) {
488
- this.#resolvePreviousPromise(this.store.state.lastResult)
489
- this.#resolvePreviousPromise = null
490
- }
491
- }
492
-
493
- #clearTimeout = (): void => {
494
- if (this.#timeoutId) {
495
- clearTimeout(this.#timeoutId)
496
- this.#timeoutId = null
497
- }
498
- }
499
-
500
- /**
501
- * Returns the AbortSignal for a specific execution.
502
- * If no maybeExecuteCount is provided, returns the signal for the most recent execution.
503
- * Returns null if no execution is found or not currently executing.
504
- *
505
- * @param maybeExecuteCount - Optional specific execution to get signal for
506
- * @example
507
- * ```typescript
508
- * const throttler = new AsyncThrottler(
509
- * async (data: string) => {
510
- * const signal = throttler.getAbortSignal()
511
- * if (signal) {
512
- * const response = await fetch('/api/save', {
513
- * method: 'POST',
514
- * body: data,
515
- * signal
516
- * })
517
- * return response.json()
518
- * }
519
- * },
520
- * { wait: 1000 }
521
- * )
522
- * ```
523
- */
524
- getAbortSignal = (maybeExecuteCount?: number): AbortSignal | null => {
525
- const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount
526
- const retryer = this.asyncRetryers.get(count)
527
- return retryer?.getAbortSignal() ?? null
528
- }
529
-
530
- /**
531
- * Aborts all ongoing executions with the internal abort controllers.
532
- * Does NOT cancel any pending execution that have not started yet.
533
- */
534
- abort = (): void => {
535
- this.asyncRetryers.forEach((retryer) => retryer.abort())
536
- this.asyncRetryers.clear()
537
- this.#setState({ isExecuting: false })
538
- }
539
-
540
- /**
541
- * Cancels any pending execution that have not started yet.
542
- * Does NOT abort any execution already in progress.
543
- */
544
- cancel = (): void => {
545
- this.#clearTimeout()
546
- if (this.#resolvePreviousPromise) {
547
- this.#resolvePreviousPromiseInternal()
548
- this.#resolvePreviousPromise = null
549
- }
550
- this.#setState({
551
- isPending: false,
552
- })
553
- }
554
-
555
- /**
556
- * Resets the debouncer state to its default values
557
- */
558
- reset = (): void => {
559
- this.#setState(getDefaultAsyncThrottlerState<TFn>())
560
- this.asyncRetryers.forEach((retryer) => retryer.reset())
561
- }
562
- }
563
-
564
- /**
565
- * Creates an async throttled function that limits how often the function can execute.
566
- * The throttled function will execute at most once per wait period, even if called multiple times.
567
- * If called while executing, it will wait until execution completes before scheduling the next call.
568
- *
569
- * Async vs Sync Versions:
570
- * The async version provides advanced features over the sync throttle function:
571
- * - Returns promises that can be awaited for throttled function results
572
- * - Built-in retry support via AsyncRetryer integration
573
- * - Abort support to cancel in-flight executions
574
- * - Cancel support to prevent pending executions from starting
575
- * - Comprehensive error handling with onError callbacks and throwOnError control
576
- * - Detailed execution tracking (success/error/settle counts)
577
- * - Waits for ongoing executions to complete before scheduling the next one
578
- *
579
- * The sync throttle function is lighter weight and simpler when you don't need async features,
580
- * return values, or execution control.
581
- *
582
- * What is Throttling?
583
- * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.
584
- * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a
585
- * regular interval regardless of how often it's called.
586
- *
587
- * Configuration Options:
588
- * - `wait`: Time window in milliseconds during which the function can only execute once (required)
589
- * - `leading`: Execute immediately when called (default: true)
590
- * - `trailing`: Execute on the trailing edge of the wait period (default: true)
591
- * - `enabled`: Whether the throttler is enabled (default: true)
592
- * - `asyncRetryerOptions`: Configure retry behavior for executions
593
- *
594
- * Error Handling:
595
- * - If an `onError` handler is provided, it will be called with the error and throttler instance
596
- * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
597
- * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
598
- * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
599
- * - The error state can be checked using the underlying AsyncThrottler instance
600
- *
601
- * State Management:
602
- * - Uses TanStack Store for reactive state management
603
- * - Use `initialState` to provide initial state values when creating the async throttler
604
- * - Use `onSuccess` callback to react to successful function execution and implement custom logic
605
- * - Use `onError` callback to react to function execution errors and implement custom error handling
606
- * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
607
- * - The state includes error count, execution status, last execution time, and success/settle counts
608
- * - State can be accessed via the underlying AsyncThrottler instance's `store.state` property
609
- * - When using framework adapters (React/Solid), state is accessed from the hook's state property
610
- *
611
- * @example
612
- * ```ts
613
- * const throttled = asyncThrottle(async (value: string) => {
614
- * const result = await saveToAPI(value);
615
- * return result; // Return value is preserved
616
- * }, {
617
- * wait: 1000,
618
- * onError: (error) => {
619
- * console.error('API call failed:', error);
620
- * }
621
- * });
622
- *
623
- * // This will execute at most once per second
624
- * // Returns the API response directly
625
- * const result = await throttled(inputElement.value);
626
- * ```
627
- */
628
- export function asyncThrottle<TFn extends AnyAsyncFunction>(
629
- fn: TFn,
630
- initialOptions: AsyncThrottlerOptions<TFn>,
631
- ) {
632
- const asyncThrottler = new AsyncThrottler(fn, initialOptions)
633
- return asyncThrottler.maybeExecute
634
- }