@tanstack/pacer 0.7.0 → 0.9.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 (91) hide show
  1. package/dist/cjs/async-batcher.cjs +163 -0
  2. package/dist/cjs/async-batcher.cjs.map +1 -0
  3. package/dist/cjs/async-batcher.d.cts +273 -0
  4. package/dist/cjs/async-debouncer.cjs +149 -162
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +76 -57
  7. package/dist/cjs/async-queuer.cjs +282 -343
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +123 -102
  10. package/dist/cjs/async-rate-limiter.cjs +128 -185
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +72 -61
  13. package/dist/cjs/async-throttler.cjs +168 -178
  14. package/dist/cjs/async-throttler.cjs.map +1 -1
  15. package/dist/cjs/async-throttler.d.cts +97 -69
  16. package/dist/cjs/batcher.cjs +110 -119
  17. package/dist/cjs/batcher.cjs.map +1 -1
  18. package/dist/cjs/batcher.d.cts +76 -51
  19. package/dist/cjs/debouncer.cjs +97 -85
  20. package/dist/cjs/debouncer.cjs.map +1 -1
  21. package/dist/cjs/debouncer.d.cts +54 -26
  22. package/dist/cjs/index.cjs +3 -6
  23. package/dist/cjs/index.cjs.map +1 -1
  24. package/dist/cjs/index.d.cts +1 -1
  25. package/dist/cjs/queuer.cjs +246 -294
  26. package/dist/cjs/queuer.cjs.map +1 -1
  27. package/dist/cjs/queuer.d.cts +105 -84
  28. package/dist/cjs/rate-limiter.cjs +97 -130
  29. package/dist/cjs/rate-limiter.cjs.map +1 -1
  30. package/dist/cjs/rate-limiter.d.cts +50 -37
  31. package/dist/cjs/throttler.cjs +107 -123
  32. package/dist/cjs/throttler.cjs.map +1 -1
  33. package/dist/cjs/throttler.d.cts +59 -35
  34. package/dist/cjs/utils.cjs +0 -13
  35. package/dist/cjs/utils.cjs.map +1 -1
  36. package/dist/cjs/utils.d.cts +0 -1
  37. package/dist/esm/async-batcher.d.ts +273 -0
  38. package/dist/esm/async-batcher.js +163 -0
  39. package/dist/esm/async-batcher.js.map +1 -0
  40. package/dist/esm/async-debouncer.d.ts +76 -57
  41. package/dist/esm/async-debouncer.js +149 -162
  42. package/dist/esm/async-debouncer.js.map +1 -1
  43. package/dist/esm/async-queuer.d.ts +123 -102
  44. package/dist/esm/async-queuer.js +282 -343
  45. package/dist/esm/async-queuer.js.map +1 -1
  46. package/dist/esm/async-rate-limiter.d.ts +72 -61
  47. package/dist/esm/async-rate-limiter.js +128 -185
  48. package/dist/esm/async-rate-limiter.js.map +1 -1
  49. package/dist/esm/async-throttler.d.ts +97 -69
  50. package/dist/esm/async-throttler.js +168 -178
  51. package/dist/esm/async-throttler.js.map +1 -1
  52. package/dist/esm/batcher.d.ts +76 -51
  53. package/dist/esm/batcher.js +110 -119
  54. package/dist/esm/batcher.js.map +1 -1
  55. package/dist/esm/debouncer.d.ts +54 -26
  56. package/dist/esm/debouncer.js +97 -85
  57. package/dist/esm/debouncer.js.map +1 -1
  58. package/dist/esm/index.d.ts +1 -1
  59. package/dist/esm/index.js +4 -7
  60. package/dist/esm/queuer.d.ts +105 -84
  61. package/dist/esm/queuer.js +246 -294
  62. package/dist/esm/queuer.js.map +1 -1
  63. package/dist/esm/rate-limiter.d.ts +50 -37
  64. package/dist/esm/rate-limiter.js +97 -130
  65. package/dist/esm/rate-limiter.js.map +1 -1
  66. package/dist/esm/throttler.d.ts +59 -35
  67. package/dist/esm/throttler.js +107 -123
  68. package/dist/esm/throttler.js.map +1 -1
  69. package/dist/esm/utils.d.ts +0 -1
  70. package/dist/esm/utils.js +0 -13
  71. package/dist/esm/utils.js.map +1 -1
  72. package/package.json +14 -11
  73. package/src/async-batcher.ts +475 -0
  74. package/src/async-debouncer.ts +201 -121
  75. package/src/async-queuer.ts +341 -220
  76. package/src/async-rate-limiter.ts +176 -136
  77. package/src/async-throttler.ts +233 -139
  78. package/src/batcher.ts +159 -93
  79. package/src/debouncer.ts +135 -52
  80. package/src/index.ts +1 -1
  81. package/src/queuer.ts +349 -227
  82. package/src/rate-limiter.ts +125 -80
  83. package/src/throttler.ts +152 -78
  84. package/src/utils.ts +0 -15
  85. package/dist/cjs/compare.cjs +0 -72
  86. package/dist/cjs/compare.cjs.map +0 -1
  87. package/dist/cjs/compare.d.cts +0 -12
  88. package/dist/esm/compare.d.ts +0 -12
  89. package/dist/esm/compare.js +0 -72
  90. package/dist/esm/compare.js.map +0 -1
  91. package/src/compare.ts +0 -105
@@ -1,6 +1,62 @@
1
+ import { Store } from '@tanstack/store'
1
2
  import { parseFunctionOrValue } from './utils'
2
3
  import type { AnyAsyncFunction, OptionalKeys } from './types'
3
4
 
5
+ export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {
6
+ /**
7
+ * Whether the debouncer can execute on the leading edge of the timeout
8
+ */
9
+ canLeadingExecute: boolean
10
+ /**
11
+ * Number of function executions that have resulted in errors
12
+ */
13
+ errorCount: number
14
+ /**
15
+ * Whether the debounced function is currently executing asynchronously
16
+ */
17
+ isExecuting: boolean
18
+ /**
19
+ * Whether the debouncer is waiting for the timeout to trigger execution
20
+ */
21
+ isPending: boolean
22
+ /**
23
+ * The arguments from the most recent call to maybeExecute
24
+ */
25
+ lastArgs: Parameters<TFn> | undefined
26
+ /**
27
+ * The result from the most recent successful function execution
28
+ */
29
+ lastResult: ReturnType<TFn> | undefined
30
+ /**
31
+ * Number of function executions that have completed (either successfully or with errors)
32
+ */
33
+ settleCount: number
34
+ /**
35
+ * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed
36
+ */
37
+ status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'
38
+ /**
39
+ * Number of function executions that have completed successfully
40
+ */
41
+ successCount: number
42
+ }
43
+
44
+ function getDefaultAsyncDebouncerState<
45
+ TFn extends AnyAsyncFunction,
46
+ >(): AsyncDebouncerState<TFn> {
47
+ return structuredClone({
48
+ canLeadingExecute: true,
49
+ errorCount: 0,
50
+ isExecuting: false,
51
+ isPending: false,
52
+ lastArgs: undefined,
53
+ lastResult: undefined,
54
+ settleCount: 0,
55
+ successCount: 0,
56
+ status: 'idle',
57
+ })
58
+ }
59
+
4
60
  /**
5
61
  * Options for configuring an async debounced function
6
62
  */
@@ -11,6 +67,10 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
11
67
  * Defaults to true.
12
68
  */
13
69
  enabled?: boolean | ((debouncer: AsyncDebouncer<TFn>) => boolean)
70
+ /**
71
+ * Initial state for the async debouncer
72
+ */
73
+ initialState?: Partial<AsyncDebouncerState<TFn>>
14
74
  /**
15
75
  * Whether to execute on the leading edge of the timeout.
16
76
  * Defaults to false.
@@ -51,7 +111,7 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
51
111
 
52
112
  type AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<
53
113
  AsyncDebouncerOptions<any>,
54
- 'onError' | 'onSettled' | 'onSuccess'
114
+ 'initialState' | 'onError' | 'onSettled' | 'onSuccess'
55
115
  >
56
116
 
57
117
  const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {
@@ -76,10 +136,18 @@ const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {
76
136
  * instead of setting the result on a state variable from within the debounced function.
77
137
  *
78
138
  * Error Handling:
79
- * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller.
80
- * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown.
81
- * - The error count can be tracked using `getErrorCount()`.
82
- * - The debouncer maintains its state and can continue to be used after an error occurs.
139
+ * - If an `onError` handler is provided, it will be called with the error and debouncer instance
140
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
141
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
142
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
143
+ * - The error state can be checked using the underlying store
144
+ *
145
+ * State Management:
146
+ * - The debouncer uses a reactive store for state management
147
+ * - Use `initialState` to provide initial state values when creating the async debouncer
148
+ * - The state includes canLeadingExecute, error count, execution status, and success/settle counts
149
+ * - State can be accessed via the `store` property and its `state` getter
150
+ * - The store is reactive and will notify subscribers of state changes
83
151
  *
84
152
  * @example
85
153
  * ```ts
@@ -99,18 +167,13 @@ const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {
99
167
  * ```
100
168
  */
101
169
  export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
102
- private _options: AsyncDebouncerOptionsWithOptionalCallbacks
103
- private _abortController: AbortController | null = null
104
- private _canLeadingExecute = true
105
- private _errorCount = 0
106
- private _isExecuting = false
107
- private _isPending = false
108
- private _lastArgs: Parameters<TFn> | undefined
109
- private _lastResult: ReturnType<TFn> | undefined
110
- private _settleCount = 0
111
- private _successCount = 0
112
- private _timeoutId: NodeJS.Timeout | null = null
113
- private _resolvePreviousPromise:
170
+ readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<
171
+ AsyncDebouncerState<TFn>
172
+ >(getDefaultAsyncDebouncerState<TFn>())
173
+ options: AsyncDebouncerOptions<TFn>
174
+ #abortController: AbortController | null = null
175
+ #timeoutId: NodeJS.Timeout | null = null
176
+ #resolvePreviousPromise:
114
177
  | ((value?: ReturnType<TFn> | undefined) => void)
115
178
  | null = null
116
179
 
@@ -118,44 +181,60 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
118
181
  private fn: TFn,
119
182
  initialOptions: AsyncDebouncerOptions<TFn>,
120
183
  ) {
121
- this._options = {
184
+ this.options = {
122
185
  ...defaultOptions,
123
186
  ...initialOptions,
124
187
  throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
125
188
  }
189
+ this.#setState(this.options.initialState ?? {})
126
190
  }
127
191
 
128
192
  /**
129
- * Updates the debouncer options
193
+ * Updates the async debouncer options
130
194
  */
131
- setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {
132
- this._options = { ...this._options, ...newOptions }
195
+ setOptions = (newOptions: Partial<AsyncDebouncerOptions<TFn>>): void => {
196
+ this.options = { ...this.options, ...newOptions }
133
197
 
134
- // End the pending state if the debouncer is disabled
135
- if (!this._options.enabled) {
136
- this._isPending = false
198
+ // Cancel pending execution if the debouncer is disabled
199
+ if (!this.#getEnabled()) {
200
+ this.cancel()
137
201
  }
138
202
  }
139
203
 
140
- /**
141
- * Returns the current debouncer options
142
- */
143
- getOptions(): AsyncDebouncerOptions<TFn> {
144
- return this._options
204
+ #setState = (newState: Partial<AsyncDebouncerState<TFn>>): void => {
205
+ this.store.setState((state) => {
206
+ const combinedState = {
207
+ ...state,
208
+ ...newState,
209
+ }
210
+ const { isPending, isExecuting, settleCount } = combinedState
211
+ return {
212
+ ...combinedState,
213
+ status: !this.#getEnabled()
214
+ ? 'disabled'
215
+ : isPending
216
+ ? 'pending'
217
+ : isExecuting
218
+ ? 'executing'
219
+ : settleCount > 0
220
+ ? 'settled'
221
+ : 'idle',
222
+ }
223
+ })
145
224
  }
146
225
 
147
226
  /**
148
227
  * Returns the current debouncer enabled state
149
228
  */
150
- getEnabled(): boolean {
151
- return !!parseFunctionOrValue(this._options.enabled, this)
229
+ #getEnabled = (): boolean => {
230
+ return !!parseFunctionOrValue(this.options.enabled, this)
152
231
  }
153
232
 
154
233
  /**
155
234
  * Returns the current debouncer wait state
156
235
  */
157
- getWait(): number {
158
- return parseFunctionOrValue(this._options.wait, this)
236
+ #getWait = (): number => {
237
+ return parseFunctionOrValue(this.options.wait, this)
159
238
  }
160
239
 
161
240
  /**
@@ -172,135 +251,126 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
172
251
  * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
173
252
  * @throws The error from the debounced function if no onError handler is configured
174
253
  */
175
- async maybeExecute(
254
+ maybeExecute = async (
176
255
  ...args: Parameters<TFn>
177
- ): Promise<ReturnType<TFn> | undefined> {
178
- this._cancel()
179
- this._lastArgs = args
256
+ ): Promise<ReturnType<TFn> | undefined> => {
257
+ if (!this.#getEnabled()) return undefined
258
+ this.#cancelPendingExecution()
259
+ this.#setState({ lastArgs: args })
180
260
 
181
261
  // Handle leading execution
182
- if (this._options.leading && this._canLeadingExecute) {
183
- this._canLeadingExecute = false
184
- await this.execute(...args)
185
- return this._lastResult
262
+ if (this.options.leading && this.store.state.canLeadingExecute) {
263
+ this.#setState({ canLeadingExecute: false })
264
+ await this.#execute(...args)
265
+ return this.store.state.lastResult
186
266
  }
187
267
 
188
268
  // Handle trailing execution
189
- if (this._options.trailing) {
190
- this._isPending = true
269
+ if (this.options.trailing && this.#getEnabled()) {
270
+ this.#setState({ isPending: true })
191
271
  }
192
272
 
193
273
  return new Promise((resolve) => {
194
- this._resolvePreviousPromise = resolve
195
- this._timeoutId = setTimeout(async () => {
274
+ this.#resolvePreviousPromise = resolve
275
+ this.#timeoutId = setTimeout(async () => {
196
276
  // Execute trailing if enabled
197
- if (this._options.trailing && this._lastArgs) {
198
- await this.execute(...this._lastArgs)
277
+ if (this.options.trailing && this.store.state.lastArgs) {
278
+ await this.#execute(...this.store.state.lastArgs)
199
279
  }
200
280
 
201
281
  // Reset state and resolve
202
- this._canLeadingExecute = true
203
- this._resolvePreviousPromise = null
204
- resolve(this._lastResult)
205
- }, this.getWait())
282
+ this.#setState({ canLeadingExecute: true })
283
+ this.#resolvePreviousPromise = null
284
+ resolve(this.store.state.lastResult)
285
+ }, this.#getWait())
206
286
  })
207
287
  }
208
288
 
209
- private async execute(
289
+ #execute = async (
210
290
  ...args: Parameters<TFn>
211
- ): Promise<ReturnType<TFn> | undefined> {
212
- if (!this.getEnabled()) return undefined
213
- this._abortController = new AbortController()
291
+ ): Promise<ReturnType<TFn> | undefined> => {
292
+ if (!this.#getEnabled()) return undefined
293
+ this.#abortController = new AbortController()
214
294
  try {
215
- this._isExecuting = true
216
- this._lastResult = await this.fn(...args) // EXECUTE!
217
- this._successCount++
218
- this._options.onSuccess?.(this._lastResult!, this)
295
+ this.#setState({ isExecuting: true })
296
+ const result = await this.fn(...args) // EXECUTE!
297
+ this.#setState({
298
+ lastResult: result,
299
+ successCount: this.store.state.successCount + 1,
300
+ })
301
+ this.options.onSuccess?.(result, this)
219
302
  } catch (error) {
220
- this._errorCount++
221
- this._options.onError?.(error, this)
222
- if (this._options.throwOnError) {
303
+ this.#setState({
304
+ errorCount: this.store.state.errorCount + 1,
305
+ })
306
+ this.options.onError?.(error, this)
307
+ if (this.options.throwOnError) {
223
308
  throw error
224
309
  }
225
310
  } finally {
226
- this._isExecuting = false
227
- this._isPending = false
228
- this._settleCount++
229
- this._abortController = null
230
- this._options.onSettled?.(this)
311
+ this.#setState({
312
+ isExecuting: false,
313
+ isPending: false,
314
+ settleCount: this.store.state.settleCount + 1,
315
+ })
316
+ this.#abortController = null
317
+ this.options.onSettled?.(this)
231
318
  }
232
- return this._lastResult
319
+ return this.store.state.lastResult
233
320
  }
234
321
 
235
322
  /**
236
- * Cancel without resetting _canLeadingExecute
323
+ * Processes the current pending execution immediately
237
324
  */
238
- private _cancel(): void {
239
- if (this._timeoutId) {
240
- clearTimeout(this._timeoutId)
241
- this._timeoutId = null
242
- }
243
- if (this._abortController) {
244
- this._abortController.abort()
245
- this._abortController = null
246
- }
247
- if (this._resolvePreviousPromise) {
248
- this._resolvePreviousPromise(this._lastResult)
249
- this._resolvePreviousPromise = null
325
+ flush = (): void => {
326
+ if (this.store.state.isPending && this.store.state.lastArgs) {
327
+ this.#abortExecution() // abort any current execution
328
+ this.#clearTimeout() // clear any existing timeout
329
+ this.#execute(...this.store.state.lastArgs)
250
330
  }
251
- this._lastArgs = undefined
252
- this._isPending = false
253
- this._isExecuting = false
254
- }
255
-
256
- /**
257
- * Cancels any pending execution or aborts any execution in progress
258
- */
259
- cancel(): void {
260
- this._canLeadingExecute = true
261
- this._cancel()
262
- }
263
-
264
- /**
265
- * Returns the last result of the debounced function
266
- */
267
- getLastResult(): ReturnType<TFn> | undefined {
268
- return this._lastResult
269
331
  }
270
332
 
271
- /**
272
- * Returns the number of times the function has been executed successfully
273
- */
274
- getSuccessCount(): number {
275
- return this._successCount
333
+ #clearTimeout = (): void => {
334
+ if (this.#timeoutId) {
335
+ clearTimeout(this.#timeoutId)
336
+ this.#timeoutId = null
337
+ }
276
338
  }
277
339
 
278
- /**
279
- * Returns the number of times the function has settled (completed or errored)
280
- */
281
- getSettleCount(): number {
282
- return this._settleCount
340
+ #cancelPendingExecution = (): void => {
341
+ this.#clearTimeout()
342
+ if (this.#resolvePreviousPromise) {
343
+ this.#resolvePreviousPromise(this.store.state.lastResult)
344
+ this.#resolvePreviousPromise = null
345
+ }
346
+ this.#setState({
347
+ isPending: false,
348
+ isExecuting: false,
349
+ lastArgs: undefined,
350
+ })
283
351
  }
284
352
 
285
- /**
286
- * Returns the number of times the function has errored
287
- */
288
- getErrorCount(): number {
289
- return this._errorCount
353
+ #abortExecution = (): void => {
354
+ if (this.#abortController) {
355
+ this.#abortController.abort()
356
+ this.#abortController = null
357
+ }
290
358
  }
291
359
 
292
360
  /**
293
- * Returns `true` if there is a pending execution queued up for trailing execution
361
+ * Cancels any pending execution or aborts any execution in progress
294
362
  */
295
- getIsPending(): boolean {
296
- return this.getEnabled() && this._isPending
363
+ cancel = (): void => {
364
+ this.#cancelPendingExecution()
365
+ this.#abortExecution()
366
+ this.#setState({ canLeadingExecute: true })
297
367
  }
298
368
 
299
369
  /**
300
- * Returns `true` if there is currently an execution in progress
370
+ * Resets the debouncer state to its default values
301
371
  */
302
- getIsExecuting(): boolean {
303
- return this._isExecuting
372
+ reset = (): void => {
373
+ this.#setState(getDefaultAsyncDebouncerState<TFn>())
304
374
  }
305
375
  }
306
376
 
@@ -320,6 +390,16 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
320
390
  * - The error state can be checked using the underlying AsyncDebouncer instance
321
391
  * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
322
392
  *
393
+ * State Management:
394
+ * - Uses TanStack Store for reactive state management
395
+ * - Use `initialState` to provide initial state values when creating the async debouncer
396
+ * - Use `onSuccess` callback to react to successful function execution and implement custom logic
397
+ * - Use `onError` callback to react to function execution errors and implement custom error handling
398
+ * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
399
+ * - The state includes canLeadingExecute, error count, execution status, and success/settle counts
400
+ * - State can be accessed via `asyncDebouncer.store.state` when using the class directly
401
+ * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`
402
+ *
323
403
  * @example
324
404
  * ```ts
325
405
  * const debounced = asyncDebounce(async (value: string) => {
@@ -343,5 +423,5 @@ export function asyncDebounce<TFn extends AnyAsyncFunction>(
343
423
  initialOptions: AsyncDebouncerOptions<TFn>,
344
424
  ) {
345
425
  const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
346
- return asyncDebouncer.maybeExecute.bind(asyncDebouncer)
426
+ return asyncDebouncer.maybeExecute
347
427
  }