@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 rate-limited function
@@ -17,7 +17,9 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
17
17
  */
18
18
  limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)
19
19
  /**
20
- * 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.
21
23
  */
22
24
  onError?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void
23
25
  /**
@@ -35,6 +37,12 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
35
37
  result: ReturnType<TFn>,
36
38
  rateLimiter: AsyncRateLimiter<TFn>,
37
39
  ) => void
40
+ /**
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.
44
+ */
45
+ throwOnError?: boolean
38
46
  /**
39
47
  * Time window in milliseconds within which the limit applies.
40
48
  * Can be a number or a function that returns a number.
@@ -49,14 +57,16 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
49
57
  windowType?: 'fixed' | 'sliding'
50
58
  }
51
59
 
52
- const defaultOptions: Required<
53
- 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'
54
68
  > = {
55
69
  enabled: true,
56
- onError: () => {},
57
- onReject: () => {},
58
- onSettled: () => {},
59
- onSuccess: () => {},
60
70
  windowType: 'fixed',
61
71
  }
62
72
 
@@ -84,11 +94,29 @@ const defaultOptions: Required<
84
94
  * Rate limiting is best used for hard API limits or resource constraints. For UI updates or
85
95
  * smoothing out frequent events, throttling or debouncing usually provide better user experience.
86
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
+ *
87
105
  * @example
88
106
  * ```ts
89
107
  * const rateLimiter = new AsyncRateLimiter(
90
108
  * async (id: string) => await api.getData(id),
91
- * { 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
+ * }
92
120
  * );
93
121
  *
94
122
  * // Will execute immediately until limit reached, then block
@@ -97,7 +125,7 @@ const defaultOptions: Required<
97
125
  * ```
98
126
  */
99
127
  export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
100
- private _options: AsyncRateLimiterOptions<TFn>
128
+ private _options: AsyncRateLimiterOptionsWithOptionalCallbacks
101
129
  private _errorCount = 0
102
130
  private _executionTimes: Array<number> = []
103
131
  private _lastResult: ReturnType<TFn> | undefined
@@ -113,6 +141,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
113
141
  this._options = {
114
142
  ...defaultOptions,
115
143
  ...initialOptions,
144
+ throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
116
145
  }
117
146
  }
118
147
 
@@ -127,8 +156,8 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
127
156
  /**
128
157
  * Returns the current rate limiter options
129
158
  */
130
- getOptions(): Required<AsyncRateLimiterOptions<TFn>> {
131
- return this._options as Required<AsyncRateLimiterOptions<TFn>>
159
+ getOptions(): AsyncRateLimiterOptions<TFn> {
160
+ return this._options
132
161
  }
133
162
 
134
163
  /**
@@ -157,6 +186,19 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
157
186
  * Will reject execution if the number of calls in the current window exceeds the limit.
158
187
  * If execution is allowed, waits for any previous execution to complete before proceeding.
159
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
+ *
160
202
  * @example
161
203
  * ```ts
162
204
  * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });
@@ -213,6 +255,11 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
213
255
  } catch (error) {
214
256
  this._errorCount++
215
257
  this._options.onError?.(error, this)
258
+ if (this._options.throwOnError) {
259
+ throw error
260
+ } else {
261
+ console.error(error)
262
+ }
216
263
  } finally {
217
264
  this._isExecuting = false
218
265
  this._settleCount++
@@ -326,6 +373,14 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
326
373
  * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
327
374
  * need to enforce a hard limit on the number of executions within a time period.
328
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
+ *
329
384
  * @example
330
385
  * ```ts
331
386
  * // Rate limit to 5 calls per minute with a sliding window
@@ -333,6 +388,9 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
333
388
  * limit: 5,
334
389
  * window: 60000,
335
390
  * windowType: 'sliding',
391
+ * onError: (error) => {
392
+ * console.error('API call failed:', error);
393
+ * },
336
394
  * onReject: (rateLimiter) => {
337
395
  * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
338
396
  * }
@@ -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 throttled function
@@ -17,7 +17,9 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
17
17
  */
18
18
  leading?: boolean
19
19
  /**
20
- * 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.
21
23
  */
22
24
  onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void
23
25
  /**
@@ -31,6 +33,12 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
31
33
  result: ReturnType<TFn>,
32
34
  asyncThrottler: AsyncThrottler<TFn>,
33
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
34
42
  /**
35
43
  * Whether to execute the function on the trailing edge of the wait period
36
44
  * Defaults to true
@@ -44,12 +52,14 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
44
52
  wait: number | ((throttler: AsyncThrottler<TFn>) => number)
45
53
  }
46
54
 
47
- const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
55
+ type AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<
56
+ AsyncThrottlerOptions<any>,
57
+ 'onError' | 'onSettled' | 'onSuccess'
58
+ >
59
+
60
+ const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {
48
61
  enabled: true,
49
62
  leading: true,
50
- onError: () => {},
51
- onSettled: () => {},
52
- onSuccess: () => {},
53
63
  trailing: true,
54
64
  wait: 0,
55
65
  }
@@ -68,12 +78,24 @@ const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
68
78
  * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to
69
79
  * ensure a maximum execution frequency.
70
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
+ *
71
88
  * @example
72
89
  * ```ts
73
90
  * const throttler = new AsyncThrottler(async (value: string) => {
74
91
  * const result = await saveToAPI(value);
75
92
  * return result; // Return value is preserved
76
- * }, { wait: 1000 });
93
+ * }, {
94
+ * wait: 1000,
95
+ * onError: (error) => {
96
+ * console.error('API call failed:', error);
97
+ * }
98
+ * });
77
99
  *
78
100
  * // Will only execute once per second no matter how often called
79
101
  * // Returns the API response directly
@@ -81,7 +103,7 @@ const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
81
103
  * ```
82
104
  */
83
105
  export class AsyncThrottler<TFn extends AnyAsyncFunction> {
84
- private _options: Required<AsyncThrottlerOptions<TFn>>
106
+ private _options: AsyncThrottlerOptionsWithOptionalCallbacks
85
107
  private _abortController: AbortController | null = null
86
108
  private _errorCount = 0
87
109
  private _isExecuting = false
@@ -100,6 +122,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
100
122
  this._options = {
101
123
  ...defaultOptions,
102
124
  ...initialOptions,
125
+ throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
103
126
  }
104
127
  }
105
128
 
@@ -119,7 +142,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
119
142
  /**
120
143
  * Returns the current options
121
144
  */
122
- getOptions(): Required<AsyncThrottlerOptions<TFn>> {
145
+ getOptions(): AsyncThrottlerOptions<TFn> {
123
146
  return this._options
124
147
  }
125
148
 
@@ -127,7 +150,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
127
150
  * Returns the current enabled state of the throttler
128
151
  */
129
152
  getEnabled(): boolean {
130
- return parseFunctionOrValue(this._options.enabled, this)
153
+ return !!parseFunctionOrValue(this._options.enabled, this)
131
154
  }
132
155
 
133
156
  /**
@@ -138,8 +161,18 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
138
161
  }
139
162
 
140
163
  /**
141
- * Attempts to execute the throttled function
142
- * If a call is already in progress, it may be blocked or queued depending on the `wait` option
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
143
176
  */
144
177
  async maybeExecute(
145
178
  ...args: Parameters<TFn>
@@ -188,17 +221,22 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
188
221
  this._isExecuting = true
189
222
  this._lastResult = await this.fn(...args) // EXECUTE!
190
223
  this._successCount++
191
- this._options.onSuccess(this._lastResult!, this)
224
+ this._options.onSuccess?.(this._lastResult!, this)
192
225
  } catch (error) {
193
226
  this._errorCount++
194
- 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
+ }
195
233
  } finally {
196
234
  this._isExecuting = false
197
235
  this._settleCount++
198
236
  this._abortController = null
199
237
  this._lastExecutionTime = Date.now()
200
238
  this._nextExecutionTime = this._lastExecutionTime + this.getWait()
201
- this._options.onSettled(this)
239
+ this._options.onSettled?.(this)
202
240
  }
203
241
  return this._lastResult
204
242
  }
@@ -284,12 +322,24 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
284
322
  * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
285
323
  * instead of setting the result on a state variable from within the throttled function.
286
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
+ *
287
332
  * @example
288
333
  * ```ts
289
334
  * const throttled = asyncThrottle(async (value: string) => {
290
335
  * const result = await saveToAPI(value);
291
336
  * return result; // Return value is preserved
292
- * }, { wait: 1000 });
337
+ * }, {
338
+ * wait: 1000,
339
+ * onError: (error) => {
340
+ * console.error('API call failed:', error);
341
+ * }
342
+ * });
293
343
  *
294
344
  * // This will execute at most once per second
295
345
  * // Returns the API response directly
package/src/types.ts CHANGED
@@ -7,3 +7,6 @@ export type AnyFunction = (...args: Array<any>) => any
7
7
  * Represents an asynchronous function that can be called with any arguments and returns a promise.
8
8
  */
9
9
  export type AnyAsyncFunction = (...args: Array<any>) => Promise<any>
10
+
11
+ export type OptionalKeys<T, TKey extends keyof T> = Omit<T, TKey> &
12
+ Partial<Pick<T, TKey>>