@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
@@ -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 rate-limited function
@@ -6,15 +7,19 @@ import type { AnyAsyncFunction } from './types'
6
7
  export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
7
8
  /**
8
9
  * Whether the rate limiter 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 | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)
12
14
  /**
13
- * Maximum number of executions allowed within the time window
15
+ * Maximum number of executions allowed within the time window.
16
+ * Can be a number or a function that returns a number.
14
17
  */
15
- limit: number
18
+ limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)
16
19
  /**
17
- * Optional error handler for when the rate-limited function throws
20
+ * Optional error handler for when the rate-limited function throws.
21
+ * If provided, the handler will be called with the error and rate limiter instance.
22
+ * This can be used alongside throwOnError - the handler will be called before any error is thrown.
18
23
  */
19
24
  onError?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void
20
25
  /**
@@ -33,9 +38,16 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
33
38
  rateLimiter: AsyncRateLimiter<TFn>,
34
39
  ) => void
35
40
  /**
36
- * Time window in milliseconds within which the limit applies
41
+ * Whether to throw errors when they occur.
42
+ * Defaults to true if no onError handler is provided, false if an onError handler is provided.
43
+ * Can be explicitly set to override these defaults.
37
44
  */
38
- window: number
45
+ throwOnError?: boolean
46
+ /**
47
+ * Time window in milliseconds within which the limit applies.
48
+ * Can be a number or a function that returns a number.
49
+ */
50
+ window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)
39
51
  /**
40
52
  * Type of window to use for rate limiting
41
53
  * - 'fixed': Uses a fixed window that resets after the window period
@@ -45,14 +57,16 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
45
57
  windowType?: 'fixed' | 'sliding'
46
58
  }
47
59
 
48
- const defaultOptions: Required<
49
- Omit<AsyncRateLimiterOptions<any>, 'limit' | 'window'>
60
+ type AsyncRateLimiterOptionsWithOptionalCallbacks = OptionalKeys<
61
+ AsyncRateLimiterOptions<any>,
62
+ 'onError' | 'onReject' | 'onSettled' | 'onSuccess'
63
+ >
64
+
65
+ const defaultOptions: Omit<
66
+ AsyncRateLimiterOptionsWithOptionalCallbacks,
67
+ 'limit' | 'window'
50
68
  > = {
51
69
  enabled: true,
52
- onError: () => {},
53
- onReject: () => {},
54
- onSettled: () => {},
55
- onSuccess: () => {},
56
70
  windowType: 'fixed',
57
71
  }
58
72
 
@@ -80,11 +94,29 @@ const defaultOptions: Required<
80
94
  * Rate limiting is best used for hard API limits or resource constraints. For UI updates or
81
95
  * smoothing out frequent events, throttling or debouncing usually provide better user experience.
82
96
  *
97
+ * Error Handling:
98
+ * - If an `onError` handler is provided, it will be called with the error and rate limiter instance
99
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
100
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
101
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
102
+ * - The error state can be checked using the underlying AsyncRateLimiter instance
103
+ * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler
104
+ *
83
105
  * @example
84
106
  * ```ts
85
107
  * const rateLimiter = new AsyncRateLimiter(
86
108
  * async (id: string) => await api.getData(id),
87
- * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window
109
+ * {
110
+ * limit: 5,
111
+ * window: 1000,
112
+ * windowType: 'sliding',
113
+ * onError: (error) => {
114
+ * console.error('API call failed:', error);
115
+ * },
116
+ * onReject: (limiter) => {
117
+ * console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);
118
+ * }
119
+ * }
88
120
  * );
89
121
  *
90
122
  * // Will execute immediately until limit reached, then block
@@ -93,7 +125,7 @@ const defaultOptions: Required<
93
125
  * ```
94
126
  */
95
127
  export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
96
- private _options: AsyncRateLimiterOptions<TFn>
128
+ private _options: AsyncRateLimiterOptionsWithOptionalCallbacks
97
129
  private _errorCount = 0
98
130
  private _executionTimes: Array<number> = []
99
131
  private _lastResult: ReturnType<TFn> | undefined
@@ -109,6 +141,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
109
141
  this._options = {
110
142
  ...defaultOptions,
111
143
  ...initialOptions,
144
+ throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
112
145
  }
113
146
  }
114
147
 
@@ -123,8 +156,29 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
123
156
  /**
124
157
  * Returns the current rate limiter options
125
158
  */
126
- getOptions(): Required<AsyncRateLimiterOptions<TFn>> {
127
- return this._options as Required<AsyncRateLimiterOptions<TFn>>
159
+ getOptions(): AsyncRateLimiterOptions<TFn> {
160
+ return this._options
161
+ }
162
+
163
+ /**
164
+ * Returns the current enabled state of the rate limiter
165
+ */
166
+ getEnabled(): boolean {
167
+ return !!parseFunctionOrValue(this._options.enabled, this)
168
+ }
169
+
170
+ /**
171
+ * Returns the current limit of executions allowed within the time window
172
+ */
173
+ getLimit(): number {
174
+ return parseFunctionOrValue(this._options.limit, this)
175
+ }
176
+
177
+ /**
178
+ * Returns the current time window in milliseconds
179
+ */
180
+ getWindow(): number {
181
+ return parseFunctionOrValue(this._options.window, this)
128
182
  }
129
183
 
130
184
  /**
@@ -132,6 +186,19 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
132
186
  * Will reject execution if the number of calls in the current window exceeds the limit.
133
187
  * If execution is allowed, waits for any previous execution to complete before proceeding.
134
188
  *
189
+ * Error Handling:
190
+ * - If the rate-limited function throws and no `onError` handler is configured,
191
+ * the error will be thrown from this method.
192
+ * - If an `onError` handler is configured, errors will be caught and passed to the handler,
193
+ * and this method will return undefined.
194
+ * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler
195
+ * will be called if configured.
196
+ * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
197
+ * - Rate limit rejections can be tracked using `getRejectionCount()`.
198
+ *
199
+ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
200
+ * @throws The error from the rate-limited function if no onError handler is configured
201
+ *
135
202
  * @example
136
203
  * ```ts
137
204
  * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });
@@ -148,9 +215,12 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
148
215
  ): Promise<ReturnType<TFn> | undefined> {
149
216
  this.cleanupOldExecutions()
150
217
 
218
+ const limit = this.getLimit()
219
+ const window = this.getWindow()
220
+
151
221
  if (this._options.windowType === 'sliding') {
152
222
  // For sliding window, we can execute if we have capacity in the current window
153
- if (this._executionTimes.length < this._options.limit) {
223
+ if (this._executionTimes.length < limit) {
154
224
  await this.executeFunction(...args)
155
225
  return this._lastResult
156
226
  }
@@ -158,9 +228,9 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
158
228
  // For fixed window, we need to check if we're in a new window
159
229
  const now = Date.now()
160
230
  const oldestExecution = Math.min(...this._executionTimes)
161
- const isNewWindow = oldestExecution + this._options.window <= now
231
+ const isNewWindow = oldestExecution + window <= now
162
232
 
163
- if (isNewWindow || this._executionTimes.length < this._options.limit) {
233
+ if (isNewWindow || this._executionTimes.length < limit) {
164
234
  await this.executeFunction(...args)
165
235
  return this._lastResult
166
236
  }
@@ -173,7 +243,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
173
243
  private async executeFunction(
174
244
  ...args: Parameters<TFn>
175
245
  ): Promise<ReturnType<TFn> | undefined> {
176
- if (!this._options.enabled) return
246
+ if (!this.getEnabled()) return
177
247
  this._isExecuting = true
178
248
  const now = Date.now()
179
249
  this._executionTimes.push(now)
@@ -185,6 +255,11 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
185
255
  } catch (error) {
186
256
  this._errorCount++
187
257
  this._options.onError?.(error, this)
258
+ if (this._options.throwOnError) {
259
+ throw error
260
+ } else {
261
+ console.error(error)
262
+ }
188
263
  } finally {
189
264
  this._isExecuting = false
190
265
  this._settleCount++
@@ -203,7 +278,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
203
278
 
204
279
  private cleanupOldExecutions(): void {
205
280
  const now = Date.now()
206
- const windowStart = now - this._options.window
281
+ const windowStart = now - this.getWindow()
207
282
  this._executionTimes = this._executionTimes.filter(
208
283
  (time) => time > windowStart,
209
284
  )
@@ -214,7 +289,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
214
289
  */
215
290
  getRemainingInWindow(): number {
216
291
  this.cleanupOldExecutions()
217
- return Math.max(0, this._options.limit - this._executionTimes.length)
292
+ return Math.max(0, this.getLimit() - this._executionTimes.length)
218
293
  }
219
294
 
220
295
  /**
@@ -227,7 +302,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
227
302
  return 0
228
303
  }
229
304
  const oldestExecution = Math.min(...this._executionTimes)
230
- return oldestExecution + this._options.window - Date.now()
305
+ return oldestExecution + this.getWindow() - Date.now()
231
306
  }
232
307
 
233
308
  /**
@@ -298,6 +373,14 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
298
373
  * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
299
374
  * need to enforce a hard limit on the number of executions within a time period.
300
375
  *
376
+ * Error Handling:
377
+ * - If an `onError` handler is provided, it will be called with the error and rate limiter instance
378
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
379
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
380
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
381
+ * - The error state can be checked using the underlying AsyncRateLimiter instance
382
+ * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler
383
+ *
301
384
  * @example
302
385
  * ```ts
303
386
  * // Rate limit to 5 calls per minute with a sliding window
@@ -305,6 +388,9 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
305
388
  * limit: 5,
306
389
  * window: 60000,
307
390
  * windowType: 'sliding',
391
+ * onError: (error) => {
392
+ * console.error('API call failed:', error);
393
+ * },
308
394
  * onReject: (rateLimiter) => {
309
395
  * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
310
396
  * }
@@ -321,7 +407,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
321
407
  */
322
408
  export function asyncRateLimit<TFn extends AnyAsyncFunction>(
323
409
  fn: TFn,
324
- initialOptions: Omit<AsyncRateLimiterOptions<TFn>, 'enabled'>,
410
+ initialOptions: AsyncRateLimiterOptions<TFn>,
325
411
  ) {
326
412
  const rateLimiter = new AsyncRateLimiter(fn, initialOptions)
327
413
  return rateLimiter.maybeExecute.bind(rateLimiter)
@@ -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 throttled function
@@ -6,16 +7,19 @@ import type { AnyAsyncFunction } from './types'
6
7
  export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
7
8
  /**
8
9
  * Whether the throttler 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 | ((throttler: AsyncThrottler<TFn>) => boolean)
12
14
  /**
13
15
  * Whether to execute the function immediately when called
14
16
  * Defaults to true
15
17
  */
16
18
  leading?: boolean
17
19
  /**
18
- * Optional error handler for when the throttled function throws
20
+ * Optional error handler for when the throttled function throws.
21
+ * If provided, the handler will be called with the error and throttler 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, asyncThrottler: AsyncThrottler<TFn>) => void
21
25
  /**
@@ -29,24 +33,33 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
29
33
  result: ReturnType<TFn>,
30
34
  asyncThrottler: AsyncThrottler<TFn>,
31
35
  ) => void
36
+ /**
37
+ * Whether to throw errors when they occur.
38
+ * Defaults to true if no onError handler is provided, false if an onError handler is provided.
39
+ * Can be explicitly set to override these defaults.
40
+ */
41
+ throwOnError?: boolean
32
42
  /**
33
43
  * Whether to execute the function on the trailing edge of the wait period
34
44
  * Defaults to true
35
45
  */
36
46
  trailing?: boolean
37
47
  /**
38
- * Time window in milliseconds during which the function can only be executed once
48
+ * Time window in milliseconds during which the function can only be executed once.
49
+ * Can be a number or a function that returns a number.
39
50
  * Defaults to 0ms
40
51
  */
41
- wait: number
52
+ wait: number | ((throttler: AsyncThrottler<TFn>) => number)
42
53
  }
43
54
 
44
- const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
55
+ type AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<
56
+ AsyncThrottlerOptions<any>,
57
+ 'onError' | 'onSettled' | 'onSuccess'
58
+ >
59
+
60
+ const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {
45
61
  enabled: true,
46
62
  leading: true,
47
- onError: () => {},
48
- onSettled: () => {},
49
- onSuccess: () => {},
50
63
  trailing: true,
51
64
  wait: 0,
52
65
  }
@@ -65,12 +78,24 @@ const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
65
78
  * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to
66
79
  * ensure a maximum execution frequency.
67
80
  *
81
+ * Error Handling:
82
+ * - If an `onError` handler is provided, it will be called with the error and throttler instance
83
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
84
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
85
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
86
+ * - The error state can be checked using the underlying AsyncThrottler instance
87
+ *
68
88
  * @example
69
89
  * ```ts
70
90
  * const throttler = new AsyncThrottler(async (value: string) => {
71
91
  * const result = await saveToAPI(value);
72
92
  * return result; // Return value is preserved
73
- * }, { wait: 1000 });
93
+ * }, {
94
+ * wait: 1000,
95
+ * onError: (error) => {
96
+ * console.error('API call failed:', error);
97
+ * }
98
+ * });
74
99
  *
75
100
  * // Will only execute once per second no matter how often called
76
101
  * // Returns the API response directly
@@ -78,7 +103,7 @@ const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
78
103
  * ```
79
104
  */
80
105
  export class AsyncThrottler<TFn extends AnyAsyncFunction> {
81
- private _options: Required<AsyncThrottlerOptions<TFn>>
106
+ private _options: AsyncThrottlerOptionsWithOptionalCallbacks
82
107
  private _abortController: AbortController | null = null
83
108
  private _errorCount = 0
84
109
  private _isExecuting = false
@@ -97,6 +122,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
97
122
  this._options = {
98
123
  ...defaultOptions,
99
124
  ...initialOptions,
125
+ throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
100
126
  }
101
127
  }
102
128
 
@@ -116,22 +142,47 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
116
142
  /**
117
143
  * Returns the current options
118
144
  */
119
- getOptions(): Required<AsyncThrottlerOptions<TFn>> {
145
+ getOptions(): AsyncThrottlerOptions<TFn> {
120
146
  return this._options
121
147
  }
122
148
 
123
149
  /**
124
- * Attempts to execute the throttled function
125
- * If a call is already in progress, it may be blocked or queued depending on the `wait` option
150
+ * Returns the current enabled state of the throttler
151
+ */
152
+ getEnabled(): boolean {
153
+ return !!parseFunctionOrValue(this._options.enabled, this)
154
+ }
155
+
156
+ /**
157
+ * Returns the current wait time in milliseconds
158
+ */
159
+ getWait(): number {
160
+ return parseFunctionOrValue(this._options.wait, this)
161
+ }
162
+
163
+ /**
164
+ * Attempts to execute the throttled function.
165
+ * If a call is already in progress, it may be blocked or queued depending on the `wait` option.
166
+ *
167
+ * Error Handling:
168
+ * - If the throttled function throws and no `onError` handler is configured,
169
+ * the error will be thrown from this method.
170
+ * - If an `onError` handler is configured, errors will be caught and passed to the handler,
171
+ * and this method will return undefined.
172
+ * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
173
+ *
174
+ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
175
+ * @throws The error from the throttled function if no onError handler is configured
126
176
  */
127
177
  async maybeExecute(
128
178
  ...args: Parameters<TFn>
129
179
  ): Promise<ReturnType<TFn> | undefined> {
130
180
  const now = Date.now()
131
181
  const timeSinceLastExecution = now - this._lastExecutionTime
182
+ const wait = this.getWait()
132
183
 
133
184
  // Handle leading execution
134
- if (this._options.leading && timeSinceLastExecution >= this._options.wait) {
185
+ if (this._options.leading && timeSinceLastExecution >= wait) {
135
186
  await this.executeFunction(...args)
136
187
  return this._lastResult
137
188
  } else {
@@ -149,7 +200,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
149
200
  const _timeSinceLastExecution = this._lastExecutionTime
150
201
  ? now - this._lastExecutionTime
151
202
  : 0
152
- const timeoutDuration = this._options.wait - _timeSinceLastExecution
203
+ const timeoutDuration = wait - _timeSinceLastExecution
153
204
  this._timeoutId = setTimeout(async () => {
154
205
  if (this._lastArgs !== undefined) {
155
206
  await this.executeFunction(...this._lastArgs)
@@ -164,23 +215,28 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
164
215
  private async executeFunction(
165
216
  ...args: Parameters<TFn>
166
217
  ): Promise<ReturnType<TFn> | undefined> {
167
- if (!this._options.enabled || this._isExecuting) return undefined
218
+ if (!this.getEnabled() || this._isExecuting) return undefined
168
219
  this._abortController = new AbortController()
169
220
  try {
170
221
  this._isExecuting = true
171
222
  this._lastResult = await this.fn(...args) // EXECUTE!
172
223
  this._successCount++
173
- this._options.onSuccess(this._lastResult!, this)
224
+ this._options.onSuccess?.(this._lastResult!, this)
174
225
  } catch (error) {
175
226
  this._errorCount++
176
- this._options.onError(error, this)
227
+ this._options.onError?.(error, this)
228
+ if (this._options.throwOnError) {
229
+ throw error
230
+ } else {
231
+ console.error(error)
232
+ }
177
233
  } finally {
178
234
  this._isExecuting = false
179
235
  this._settleCount++
180
236
  this._abortController = null
181
237
  this._lastExecutionTime = Date.now()
182
- this._nextExecutionTime = this._lastExecutionTime + this._options.wait
183
- this._options.onSettled(this)
238
+ this._nextExecutionTime = this._lastExecutionTime + this.getWait()
239
+ this._options.onSettled?.(this)
184
240
  }
185
241
  return this._lastResult
186
242
  }
@@ -246,7 +302,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
246
302
  * Returns the current pending state
247
303
  */
248
304
  getIsPending(): boolean {
249
- return this._options.enabled && !!this._timeoutId
305
+ return this.getEnabled() && !!this._timeoutId
250
306
  }
251
307
 
252
308
  /**
@@ -266,12 +322,24 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
266
322
  * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
267
323
  * instead of setting the result on a state variable from within the throttled function.
268
324
  *
325
+ * Error Handling:
326
+ * - If an `onError` handler is provided, it will be called with the error and throttler instance
327
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
328
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
329
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
330
+ * - The error state can be checked using the underlying AsyncThrottler instance
331
+ *
269
332
  * @example
270
333
  * ```ts
271
334
  * const throttled = asyncThrottle(async (value: string) => {
272
335
  * const result = await saveToAPI(value);
273
336
  * return result; // Return value is preserved
274
- * }, { wait: 1000 });
337
+ * }, {
338
+ * wait: 1000,
339
+ * onError: (error) => {
340
+ * console.error('API call failed:', error);
341
+ * }
342
+ * });
275
343
  *
276
344
  * // This will execute at most once per second
277
345
  * // Returns the API response directly
@@ -280,7 +348,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
280
348
  */
281
349
  export function asyncThrottle<TFn extends AnyAsyncFunction>(
282
350
  fn: TFn,
283
- initialOptions: Omit<AsyncThrottlerOptions<TFn>, 'enabled'>,
351
+ initialOptions: AsyncThrottlerOptions<TFn>,
284
352
  ) {
285
353
  const asyncThrottler = new AsyncThrottler(fn, initialOptions)
286
354
  return asyncThrottler.maybeExecute.bind(asyncThrottler)
package/src/debouncer.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseFunctionOrValue } from './utils'
1
2
  import type { AnyFunction } from './types'
2
3
 
3
4
  /**
@@ -6,9 +7,10 @@ import type { AnyFunction } from './types'
6
7
  export interface DebouncerOptions<TFn extends AnyFunction> {
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: Debouncer<TFn>) => boolean)
12
14
  /**
13
15
  * Whether to execute on the leading edge of the timeout.
14
16
  * The first call will execute immediately and the rest will wait the delay.
@@ -25,10 +27,11 @@ export interface DebouncerOptions<TFn extends AnyFunction> {
25
27
  */
26
28
  trailing?: boolean
27
29
  /**
28
- * Delay in milliseconds before executing the function
30
+ * Delay in milliseconds before executing the function.
31
+ * Can be a number or a function that returns a number.
29
32
  * Defaults to 0ms
30
33
  */
31
- wait: number
34
+ wait: number | ((debouncer: Debouncer<TFn>) => number)
32
35
  }
33
36
 
34
37
  const defaultOptions: Required<DebouncerOptions<any>> = {
@@ -99,6 +102,20 @@ export class Debouncer<TFn extends AnyFunction> {
99
102
  return this._options
100
103
  }
101
104
 
105
+ /**
106
+ * Returns the current enabled state of the debouncer
107
+ */
108
+ getEnabled(): boolean {
109
+ return parseFunctionOrValue(this._options.enabled, this)
110
+ }
111
+
112
+ /**
113
+ * Returns the current wait time in milliseconds
114
+ */
115
+ getWait(): number {
116
+ return parseFunctionOrValue(this._options.wait, this)
117
+ }
118
+
102
119
  /**
103
120
  * Attempts to execute the debounced function
104
121
  * If a call is already in progress, it will be queued
@@ -127,11 +144,11 @@ export class Debouncer<TFn extends AnyFunction> {
127
144
  if (this._options.trailing && !_didLeadingExecute) {
128
145
  this.executeFunction(...args)
129
146
  }
130
- }, this._options.wait)
147
+ }, this.getWait())
131
148
  }
132
149
 
133
150
  private executeFunction(...args: Parameters<TFn>): void {
134
- if (!this._options.enabled) return undefined
151
+ if (!this.getEnabled()) return undefined
135
152
  this.fn(...args) // EXECUTE!
136
153
  this._isPending = false
137
154
  this._executionCount++
@@ -160,7 +177,7 @@ export class Debouncer<TFn extends AnyFunction> {
160
177
  * Returns `true` if debouncing
161
178
  */
162
179
  getIsPending(): boolean {
163
- return this._options.enabled && this._isPending
180
+ return this.getEnabled() && this._isPending
164
181
  }
165
182
  }
166
183
 
@@ -186,7 +203,7 @@ export class Debouncer<TFn extends AnyFunction> {
186
203
  */
187
204
  export function debounce<TFn extends AnyFunction>(
188
205
  fn: TFn,
189
- initialOptions: Omit<DebouncerOptions<TFn>, 'enabled'>,
206
+ initialOptions: DebouncerOptions<TFn>,
190
207
  ): (...args: Parameters<TFn>) => void {
191
208
  const debouncer = new Debouncer(fn, initialOptions)
192
209
  return debouncer.maybeExecute.bind(debouncer)
package/src/queuer.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { parseFunctionOrValue } from './utils'
2
+
1
3
  /**
2
4
  * Options for configuring a Queuer instance
3
5
  */
@@ -60,9 +62,11 @@ export interface QueuerOptions<TValue> {
60
62
  */
61
63
  started?: boolean
62
64
  /**
63
- * Time in milliseconds to wait between processing items
65
+ * Time in milliseconds to wait between processing items.
66
+ * Can be a number or a function that returns a number.
67
+ * @default 0
64
68
  */
65
- wait?: number
69
+ wait?: number | ((queuer: Queuer<TValue>) => number)
66
70
  }
67
71
 
68
72
  const defaultOptions: Required<QueuerOptions<any>> = {
@@ -78,7 +82,7 @@ const defaultOptions: Required<QueuerOptions<any>> = {
78
82
  onItemsChange: () => {},
79
83
  onReject: () => {},
80
84
  onExpire: () => {},
81
- started: false,
85
+ started: true,
82
86
  wait: 0,
83
87
  }
84
88
 
@@ -178,6 +182,13 @@ export class Queuer<TValue> {
178
182
  return this._options
179
183
  }
180
184
 
185
+ /**
186
+ * Returns the current wait time in milliseconds
187
+ */
188
+ getWait(): number {
189
+ return parseFunctionOrValue(this._options.wait, this)
190
+ }
191
+
181
192
  /**
182
193
  * Processes items in the queuer
183
194
  */
@@ -197,9 +208,10 @@ export class Queuer<TValue> {
197
208
  }
198
209
  this._onItemsChanges.forEach((cb) => cb(nextItem))
199
210
 
200
- if (this._options.wait > 0) {
211
+ const wait = this.getWait()
212
+ if (wait > 0) {
201
213
  // Use setTimeout to wait before processing next item
202
- setTimeout(() => this.tick(), this._options.wait)
214
+ setTimeout(() => this.tick(), wait)
203
215
  return
204
216
  }
205
217
 
@@ -493,7 +505,7 @@ export class Queuer<TValue> {
493
505
  * processPriority(3) // Processed before 1
494
506
  * ```
495
507
  */
496
- export function queue<TValue>(options: QueuerOptions<TValue> = {}) {
497
- const queuer = new Queuer<TValue>({ ...options, started: true })
508
+ export function queue<TValue>(options: QueuerOptions<TValue>) {
509
+ const queuer = new Queuer<TValue>(options)
498
510
  return queuer.addItem.bind(queuer)
499
511
  }