@tanstack/pacer 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { parseFunctionOrValue } from './utils'
2
- import type { AnyAsyncFunction } from './types'
2
+ import type { AnyAsyncFunction, OptionalKeys } from './types'
3
3
 
4
4
  /**
5
5
  * Options for configuring an async debounced function
@@ -17,7 +17,9 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
17
17
  */
18
18
  leading?: boolean
19
19
  /**
20
- * Optional error handler for when the debounced function throws
20
+ * Optional error handler for when the debounced function throws.
21
+ * If provided, the handler will be called with the error and debouncer instance.
22
+ * This can be used alongside throwOnError - the handler will be called before any error is thrown.
21
23
  */
22
24
  onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void
23
25
  /**
@@ -28,6 +30,12 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
28
30
  * Optional callback to call when the debounced function is executed
29
31
  */
30
32
  onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => void
33
+ /**
34
+ * Whether to throw errors when they occur.
35
+ * Defaults to true if no onError handler is provided, false if an onError handler is provided.
36
+ * Can be explicitly set to override these defaults.
37
+ */
38
+ throwOnError?: boolean
31
39
  /**
32
40
  * Whether to execute on the trailing edge of the timeout.
33
41
  * Defaults to true.
@@ -41,12 +49,14 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
41
49
  wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)
42
50
  }
43
51
 
44
- const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
52
+ type AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<
53
+ AsyncDebouncerOptions<any>,
54
+ 'onError' | 'onSettled' | 'onSuccess'
55
+ >
56
+
57
+ const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {
45
58
  enabled: true,
46
59
  leading: false,
47
- onError: () => {},
48
- onSettled: () => {},
49
- onSuccess: () => {},
50
60
  trailing: true,
51
61
  wait: 0,
52
62
  }
@@ -65,12 +75,23 @@ const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
65
75
  * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
66
76
  * instead of setting the result on a state variable from within the debounced function.
67
77
  *
78
+ * 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.
83
+ *
68
84
  * @example
69
85
  * ```ts
70
86
  * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {
71
87
  * const results = await searchAPI(value);
72
88
  * return results; // Return value is preserved
73
- * }, { wait: 500 });
89
+ * }, {
90
+ * wait: 500,
91
+ * onError: (error) => {
92
+ * console.error('Search failed:', error);
93
+ * }
94
+ * });
74
95
  *
75
96
  * // Called on each keystroke but only executes after 500ms of no typing
76
97
  * // Returns the API response directly
@@ -78,6 +99,7 @@ const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
78
99
  * ```
79
100
  */
80
101
  export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
102
+ private _options: AsyncDebouncerOptionsWithOptionalCallbacks
81
103
  private _abortController: AbortController | null = null
82
104
  private _canLeadingExecute = true
83
105
  private _errorCount = 0
@@ -85,7 +107,6 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
85
107
  private _isPending = false
86
108
  private _lastArgs: Parameters<TFn> | undefined
87
109
  private _lastResult: ReturnType<TFn> | undefined
88
- private _options: Required<AsyncDebouncerOptions<TFn>>
89
110
  private _settleCount = 0
90
111
  private _successCount = 0
91
112
  private _timeoutId: NodeJS.Timeout | null = null
@@ -97,6 +118,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
97
118
  this._options = {
98
119
  ...defaultOptions,
99
120
  ...initialOptions,
121
+ throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
100
122
  }
101
123
  }
102
124
 
@@ -116,7 +138,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
116
138
  /**
117
139
  * Returns the current debouncer options
118
140
  */
119
- getOptions(): Required<AsyncDebouncerOptions<TFn>> {
141
+ getOptions(): AsyncDebouncerOptions<TFn> {
120
142
  return this._options
121
143
  }
122
144
 
@@ -124,7 +146,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
124
146
  * Returns the current debouncer enabled state
125
147
  */
126
148
  getEnabled(): boolean {
127
- return parseFunctionOrValue(this._options.enabled, this)
149
+ return !!parseFunctionOrValue(this._options.enabled, this)
128
150
  }
129
151
 
130
152
  /**
@@ -135,8 +157,18 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
135
157
  }
136
158
 
137
159
  /**
138
- * Attempts to execute the debounced function
139
- * If a call is already in progress, it will be queued
160
+ * Attempts to execute the debounced function.
161
+ * If a call is already in progress, it will be queued.
162
+ *
163
+ * Error Handling:
164
+ * - If the debounced function throws and no `onError` handler is configured,
165
+ * the error will be thrown from this method.
166
+ * - If an `onError` handler is configured, errors will be caught and passed to the handler,
167
+ * and this method will return undefined.
168
+ * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
169
+ *
170
+ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
171
+ * @throws The error from the debounced function if no onError handler is configured
140
172
  */
141
173
  async maybeExecute(
142
174
  ...args: Parameters<TFn>
@@ -179,16 +211,21 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
179
211
  this._isExecuting = true
180
212
  this._lastResult = await this.fn(...args) // EXECUTE!
181
213
  this._successCount++
182
- this._options.onSuccess(this._lastResult!, this)
214
+ this._options.onSuccess?.(this._lastResult!, this)
183
215
  } catch (error) {
184
216
  this._errorCount++
185
- this._options.onError(error, this)
217
+ this._options.onError?.(error, this)
218
+ if (this._options.throwOnError) {
219
+ throw error
220
+ } else {
221
+ console.error(error)
222
+ }
186
223
  } finally {
187
224
  this._isExecuting = false
188
225
  this._isPending = false
189
226
  this._settleCount++
190
227
  this._abortController = null
191
- this._options.onSettled(this)
228
+ this._options.onSettled?.(this)
192
229
  }
193
230
  return this._lastResult
194
231
  }
@@ -270,12 +307,25 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
270
307
  * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
271
308
  * instead of setting the result on a state variable from within the debounced function.
272
309
  *
310
+ * Error Handling:
311
+ * - If an `onError` handler is provided, it will be called with the error and debouncer instance
312
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
313
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
314
+ * - The error state can be checked using the underlying AsyncDebouncer instance
315
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
316
+ *
273
317
  * @example
274
318
  * ```ts
275
319
  * const debounced = asyncDebounce(async (value: string) => {
276
320
  * const result = await saveToAPI(value);
277
321
  * return result; // Return value is preserved
278
- * }, { wait: 1000 });
322
+ * }, {
323
+ * wait: 1000,
324
+ * onError: (error) => {
325
+ * console.error('API call failed:', error);
326
+ * },
327
+ * throwOnError: true // Will both log the error and throw it
328
+ * });
279
329
  *
280
330
  * // Will only execute once, 1 second after the last call
281
331
  * // Returns the API response directly