@tanstack/pacer 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/cjs/async-debouncer.cjs +39 -15
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +63 -11
  4. package/dist/cjs/async-queuer.cjs +102 -119
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +94 -52
  7. package/dist/cjs/async-rate-limiter.cjs +48 -16
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +74 -9
  10. package/dist/cjs/async-throttler.cjs +42 -17
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +62 -10
  13. package/dist/cjs/debouncer.cjs +16 -3
  14. package/dist/cjs/debouncer.cjs.map +1 -1
  15. package/dist/cjs/debouncer.d.cts +14 -4
  16. package/dist/cjs/index.cjs +2 -0
  17. package/dist/cjs/index.cjs.map +1 -1
  18. package/dist/cjs/queuer.cjs +13 -5
  19. package/dist/cjs/queuer.cjs.map +1 -1
  20. package/dist/cjs/queuer.d.cts +9 -3
  21. package/dist/cjs/rate-limiter.cjs +26 -7
  22. package/dist/cjs/rate-limiter.cjs.map +1 -1
  23. package/dist/cjs/rate-limiter.d.cts +20 -6
  24. package/dist/cjs/throttler.cjs +19 -5
  25. package/dist/cjs/throttler.cjs.map +1 -1
  26. package/dist/cjs/throttler.d.cts +15 -4
  27. package/dist/cjs/types.d.cts +1 -0
  28. package/dist/cjs/utils.cjs +18 -7
  29. package/dist/cjs/utils.cjs.map +1 -1
  30. package/dist/cjs/utils.d.cts +3 -0
  31. package/dist/esm/async-debouncer.d.ts +63 -11
  32. package/dist/esm/async-debouncer.js +39 -15
  33. package/dist/esm/async-debouncer.js.map +1 -1
  34. package/dist/esm/async-queuer.d.ts +94 -52
  35. package/dist/esm/async-queuer.js +102 -119
  36. package/dist/esm/async-queuer.js.map +1 -1
  37. package/dist/esm/async-rate-limiter.d.ts +74 -9
  38. package/dist/esm/async-rate-limiter.js +48 -16
  39. package/dist/esm/async-rate-limiter.js.map +1 -1
  40. package/dist/esm/async-throttler.d.ts +62 -10
  41. package/dist/esm/async-throttler.js +42 -17
  42. package/dist/esm/async-throttler.js.map +1 -1
  43. package/dist/esm/debouncer.d.ts +14 -4
  44. package/dist/esm/debouncer.js +16 -3
  45. package/dist/esm/debouncer.js.map +1 -1
  46. package/dist/esm/index.js +3 -1
  47. package/dist/esm/queuer.d.ts +9 -3
  48. package/dist/esm/queuer.js +13 -5
  49. package/dist/esm/queuer.js.map +1 -1
  50. package/dist/esm/rate-limiter.d.ts +20 -6
  51. package/dist/esm/rate-limiter.js +26 -7
  52. package/dist/esm/rate-limiter.js.map +1 -1
  53. package/dist/esm/throttler.d.ts +15 -4
  54. package/dist/esm/throttler.js +19 -5
  55. package/dist/esm/throttler.js.map +1 -1
  56. package/dist/esm/types.d.ts +1 -0
  57. package/dist/esm/utils.d.ts +3 -0
  58. package/dist/esm/utils.js +19 -8
  59. package/dist/esm/utils.js.map +1 -1
  60. package/package.json +9 -3
  61. package/src/async-debouncer.ts +89 -22
  62. package/src/async-queuer.ts +205 -175
  63. package/src/async-rate-limiter.ts +111 -25
  64. package/src/async-throttler.ts +92 -24
  65. package/src/debouncer.ts +24 -7
  66. package/src/queuer.ts +19 -7
  67. package/src/rate-limiter.ts +37 -13
  68. package/src/throttler.ts +28 -9
  69. package/src/types.ts +3 -0
  70. package/src/utils.ts +19 -5
package/dist/esm/utils.js CHANGED
@@ -1,13 +1,24 @@
1
+ function isFunction(value) {
2
+ return typeof value === "function";
3
+ }
4
+ function parseFunctionOrValue(value, ...args) {
5
+ return isFunction(value) ? value(...args) : value;
6
+ }
1
7
  function bindInstanceMethods(instance) {
2
- return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).filter((key) => typeof instance[key] === "function").reduce((acc, key) => {
3
- const method = instance[key];
4
- if (typeof method === "function") {
5
- acc[key] = method.bind(instance);
6
- }
7
- return acc;
8
- }, {});
8
+ return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(
9
+ (acc, key) => {
10
+ const method = instance[key];
11
+ if (isFunction(method)) {
12
+ acc[key] = method.bind(instance);
13
+ }
14
+ return acc;
15
+ },
16
+ instance
17
+ );
9
18
  }
10
19
  export {
11
- bindInstanceMethods
20
+ bindInstanceMethods,
21
+ isFunction,
22
+ parseFunctionOrValue
12
23
  };
13
24
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["export function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n): T {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance))\n .filter((key) => typeof instance[key as keyof T] === 'function')\n .reduce((acc: any, key) => {\n const method = instance[key as keyof T]\n if (typeof method === 'function') {\n acc[key] = method.bind(instance)\n }\n return acc\n }, {} as T)\n}\n"],"names":[],"mappings":"AAAO,SAAS,oBACd,UACG;AACH,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAC9D,OAAO,CAAC,QAAQ,OAAO,SAAS,GAAc,MAAM,UAAU,EAC9D,OAAO,CAAC,KAAU,QAAQ;AACnB,UAAA,SAAS,SAAS,GAAc;AAClC,QAAA,OAAO,WAAW,YAAY;AAChC,UAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,IAAA;AAE1B,WAAA;AAAA,EACT,GAAG,EAAO;AACd;"}
1
+ {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n\nexport function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n): T {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(\n (acc: any, key) => {\n const method = instance[key as keyof T]\n if (isFunction(method)) {\n acc[key] = method.bind(instance)\n }\n return acc\n },\n instance,\n )\n}\n"],"names":[],"mappings":"AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEgB,SAAA,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;AAEO,SAAS,oBACd,UACG;AACH,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAAE;AAAA,IACjE,CAAC,KAAU,QAAQ;AACX,YAAA,SAAS,SAAS,GAAc;AAClC,UAAA,WAAW,MAAM,GAAG;AACtB,YAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,MAAA;AAE1B,aAAA;AAAA,IACT;AAAA,IACA;AAAA,EACF;AACF;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/pacer",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Utilities for debouncing, throttling, rate-limiting, queuing, and more.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -131,8 +131,14 @@
131
131
  "types": "./dist/esm/types.d.ts"
132
132
  },
133
133
  "./utils": {
134
- "types": "./dist/esm/utils.d.ts",
135
- "default": "./dist/esm/utils.js"
134
+ "import": {
135
+ "types": "./dist/esm/utils.d.ts",
136
+ "default": "./dist/esm/utils.js"
137
+ },
138
+ "require": {
139
+ "types": "./dist/cjs/utils.d.cts",
140
+ "default": "./dist/cjs/utils.cjs"
141
+ }
136
142
  },
137
143
  "./package.json": "./package.json"
138
144
  },
@@ -1,4 +1,5 @@
1
- import type { AnyAsyncFunction } from './types'
1
+ import { parseFunctionOrValue } from './utils'
2
+ import type { AnyAsyncFunction, OptionalKeys } from './types'
2
3
 
3
4
  /**
4
5
  * Options for configuring an async debounced function
@@ -6,16 +7,19 @@ import type { AnyAsyncFunction } from './types'
6
7
  export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
7
8
  /**
8
9
  * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
10
+ * Can be a boolean or a function that returns a boolean.
9
11
  * Defaults to true.
10
12
  */
11
- enabled?: boolean
13
+ enabled?: boolean | ((debouncer: AsyncDebouncer<TFn>) => boolean)
12
14
  /**
13
15
  * Whether to execute on the leading edge of the timeout.
14
16
  * Defaults to false.
15
17
  */
16
18
  leading?: boolean
17
19
  /**
18
- * 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.
19
23
  */
20
24
  onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void
21
25
  /**
@@ -26,24 +30,33 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
26
30
  * Optional callback to call when the debounced function is executed
27
31
  */
28
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
29
39
  /**
30
40
  * Whether to execute on the trailing edge of the timeout.
31
41
  * Defaults to true.
32
42
  */
33
43
  trailing?: boolean
34
44
  /**
35
- * Delay in milliseconds to wait after the last call before executing
45
+ * Delay in milliseconds to wait after the last call before executing.
46
+ * Can be a number or a function that returns a number.
36
47
  * Defaults to 0ms
37
48
  */
38
- wait: number
49
+ wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)
39
50
  }
40
51
 
41
- const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
52
+ type AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<
53
+ AsyncDebouncerOptions<any>,
54
+ 'onError' | 'onSettled' | 'onSuccess'
55
+ >
56
+
57
+ const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {
42
58
  enabled: true,
43
59
  leading: false,
44
- onError: () => {},
45
- onSettled: () => {},
46
- onSuccess: () => {},
47
60
  trailing: true,
48
61
  wait: 0,
49
62
  }
@@ -62,12 +75,23 @@ const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
62
75
  * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
63
76
  * instead of setting the result on a state variable from within the debounced function.
64
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
+ *
65
84
  * @example
66
85
  * ```ts
67
86
  * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {
68
87
  * const results = await searchAPI(value);
69
88
  * return results; // Return value is preserved
70
- * }, { wait: 500 });
89
+ * }, {
90
+ * wait: 500,
91
+ * onError: (error) => {
92
+ * console.error('Search failed:', error);
93
+ * }
94
+ * });
71
95
  *
72
96
  * // Called on each keystroke but only executes after 500ms of no typing
73
97
  * // Returns the API response directly
@@ -75,6 +99,7 @@ const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
75
99
  * ```
76
100
  */
77
101
  export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
102
+ private _options: AsyncDebouncerOptionsWithOptionalCallbacks
78
103
  private _abortController: AbortController | null = null
79
104
  private _canLeadingExecute = true
80
105
  private _errorCount = 0
@@ -82,7 +107,6 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
82
107
  private _isPending = false
83
108
  private _lastArgs: Parameters<TFn> | undefined
84
109
  private _lastResult: ReturnType<TFn> | undefined
85
- private _options: Required<AsyncDebouncerOptions<TFn>>
86
110
  private _settleCount = 0
87
111
  private _successCount = 0
88
112
  private _timeoutId: NodeJS.Timeout | null = null
@@ -94,6 +118,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
94
118
  this._options = {
95
119
  ...defaultOptions,
96
120
  ...initialOptions,
121
+ throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
97
122
  }
98
123
  }
99
124
 
@@ -113,13 +138,37 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
113
138
  /**
114
139
  * Returns the current debouncer options
115
140
  */
116
- getOptions(): Required<AsyncDebouncerOptions<TFn>> {
141
+ getOptions(): AsyncDebouncerOptions<TFn> {
117
142
  return this._options
118
143
  }
119
144
 
120
145
  /**
121
- * Attempts to execute the debounced function
122
- * If a call is already in progress, it will be queued
146
+ * Returns the current debouncer enabled state
147
+ */
148
+ getEnabled(): boolean {
149
+ return !!parseFunctionOrValue(this._options.enabled, this)
150
+ }
151
+
152
+ /**
153
+ * Returns the current debouncer wait state
154
+ */
155
+ getWait(): number {
156
+ return parseFunctionOrValue(this._options.wait, this)
157
+ }
158
+
159
+ /**
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
123
172
  */
124
173
  async maybeExecute(
125
174
  ...args: Parameters<TFn>
@@ -149,29 +198,34 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
149
198
  // Reset state and resolve
150
199
  this._canLeadingExecute = true
151
200
  resolve(this._lastResult)
152
- }, this._options.wait)
201
+ }, this.getWait())
153
202
  })
154
203
  }
155
204
 
156
205
  private async executeFunction(
157
206
  ...args: Parameters<TFn>
158
207
  ): Promise<ReturnType<TFn> | undefined> {
159
- if (!this._options.enabled) return undefined
208
+ if (!this.getEnabled()) return undefined
160
209
  this._abortController = new AbortController()
161
210
  try {
162
211
  this._isExecuting = true
163
212
  this._lastResult = await this.fn(...args) // EXECUTE!
164
213
  this._successCount++
165
- this._options.onSuccess(this._lastResult!, this)
214
+ this._options.onSuccess?.(this._lastResult!, this)
166
215
  } catch (error) {
167
216
  this._errorCount++
168
- 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
+ }
169
223
  } finally {
170
224
  this._isExecuting = false
171
225
  this._isPending = false
172
226
  this._settleCount++
173
227
  this._abortController = null
174
- this._options.onSettled(this)
228
+ this._options.onSettled?.(this)
175
229
  }
176
230
  return this._lastResult
177
231
  }
@@ -233,7 +287,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
233
287
  * Returns `true` if there is a pending execution queued up for trailing execution
234
288
  */
235
289
  getIsPending(): boolean {
236
- return this._options.enabled && this._isPending
290
+ return this.getEnabled() && this._isPending
237
291
  }
238
292
 
239
293
  /**
@@ -253,12 +307,25 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
253
307
  * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
254
308
  * instead of setting the result on a state variable from within the debounced function.
255
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
+ *
256
317
  * @example
257
318
  * ```ts
258
319
  * const debounced = asyncDebounce(async (value: string) => {
259
320
  * const result = await saveToAPI(value);
260
321
  * return result; // Return value is preserved
261
- * }, { 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
+ * });
262
329
  *
263
330
  * // Will only execute once, 1 second after the last call
264
331
  * // Returns the API response directly
@@ -267,7 +334,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
267
334
  */
268
335
  export function asyncDebounce<TFn extends AnyAsyncFunction>(
269
336
  fn: TFn,
270
- initialOptions: Omit<AsyncDebouncerOptions<TFn>, 'enabled'>,
337
+ initialOptions: AsyncDebouncerOptions<TFn>,
271
338
  ) {
272
339
  const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
273
340
  return asyncDebouncer.maybeExecute.bind(asyncDebouncer)