@tanstack/pacer 0.5.0 → 0.7.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 (74) hide show
  1. package/dist/cjs/async-debouncer.cjs +32 -17
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +51 -9
  4. package/dist/cjs/async-queuer.cjs +189 -191
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +151 -94
  7. package/dist/cjs/async-rate-limiter.cjs +23 -13
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +54 -5
  10. package/dist/cjs/async-throttler.cjs +38 -17
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +51 -8
  13. package/dist/cjs/batcher.cjs +138 -0
  14. package/dist/cjs/batcher.cjs.map +1 -0
  15. package/dist/cjs/batcher.d.cts +149 -0
  16. package/dist/cjs/debouncer.cjs +3 -4
  17. package/dist/cjs/debouncer.cjs.map +1 -1
  18. package/dist/cjs/debouncer.d.cts +1 -2
  19. package/dist/cjs/index.cjs +3 -0
  20. package/dist/cjs/index.cjs.map +1 -1
  21. package/dist/cjs/index.d.cts +1 -0
  22. package/dist/cjs/queuer.cjs +68 -41
  23. package/dist/cjs/queuer.cjs.map +1 -1
  24. package/dist/cjs/queuer.d.cts +123 -92
  25. package/dist/cjs/rate-limiter.cjs +3 -4
  26. package/dist/cjs/rate-limiter.cjs.map +1 -1
  27. package/dist/cjs/rate-limiter.d.cts +1 -2
  28. package/dist/cjs/throttler.cjs +3 -4
  29. package/dist/cjs/throttler.cjs.map +1 -1
  30. package/dist/cjs/throttler.d.cts +1 -2
  31. package/dist/cjs/types.d.cts +1 -0
  32. package/dist/esm/async-debouncer.d.ts +51 -9
  33. package/dist/esm/async-debouncer.js +32 -17
  34. package/dist/esm/async-debouncer.js.map +1 -1
  35. package/dist/esm/async-queuer.d.ts +151 -94
  36. package/dist/esm/async-queuer.js +189 -191
  37. package/dist/esm/async-queuer.js.map +1 -1
  38. package/dist/esm/async-rate-limiter.d.ts +54 -5
  39. package/dist/esm/async-rate-limiter.js +23 -13
  40. package/dist/esm/async-rate-limiter.js.map +1 -1
  41. package/dist/esm/async-throttler.d.ts +51 -8
  42. package/dist/esm/async-throttler.js +38 -17
  43. package/dist/esm/async-throttler.js.map +1 -1
  44. package/dist/esm/batcher.d.ts +149 -0
  45. package/dist/esm/batcher.js +138 -0
  46. package/dist/esm/batcher.js.map +1 -0
  47. package/dist/esm/debouncer.d.ts +1 -2
  48. package/dist/esm/debouncer.js +3 -4
  49. package/dist/esm/debouncer.js.map +1 -1
  50. package/dist/esm/index.d.ts +1 -0
  51. package/dist/esm/index.js +3 -0
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/queuer.d.ts +123 -92
  54. package/dist/esm/queuer.js +68 -41
  55. package/dist/esm/queuer.js.map +1 -1
  56. package/dist/esm/rate-limiter.d.ts +1 -2
  57. package/dist/esm/rate-limiter.js +3 -4
  58. package/dist/esm/rate-limiter.js.map +1 -1
  59. package/dist/esm/throttler.d.ts +1 -2
  60. package/dist/esm/throttler.js +3 -4
  61. package/dist/esm/throttler.js.map +1 -1
  62. package/dist/esm/types.d.ts +1 -0
  63. package/package.json +11 -1
  64. package/src/async-debouncer.ts +76 -20
  65. package/src/async-queuer.ts +309 -275
  66. package/src/async-rate-limiter.ts +73 -16
  67. package/src/async-throttler.ts +84 -20
  68. package/src/batcher.ts +253 -0
  69. package/src/debouncer.ts +3 -4
  70. package/src/index.ts +1 -0
  71. package/src/queuer.ts +142 -98
  72. package/src/rate-limiter.ts +3 -4
  73. package/src/throttler.ts +3 -4
  74. package/src/types.ts +3 -0
@@ -15,7 +15,9 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
15
15
  */
16
16
  limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number);
17
17
  /**
18
- * Optional error handler for when the rate-limited function throws
18
+ * Optional error handler for when the rate-limited function throws.
19
+ * If provided, the handler will be called with the error and rate limiter instance.
20
+ * This can be used alongside throwOnError - the handler will be called before any error is thrown.
19
21
  */
20
22
  onError?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void;
21
23
  /**
@@ -30,6 +32,12 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
30
32
  * Optional function to call when the rate-limited function is executed
31
33
  */
32
34
  onSuccess?: (result: ReturnType<TFn>, rateLimiter: AsyncRateLimiter<TFn>) => void;
35
+ /**
36
+ * Whether to throw errors when they occur.
37
+ * Defaults to true if no onError handler is provided, false if an onError handler is provided.
38
+ * Can be explicitly set to override these defaults.
39
+ */
40
+ throwOnError?: boolean;
33
41
  /**
34
42
  * Time window in milliseconds within which the limit applies.
35
43
  * Can be a number or a function that returns a number.
@@ -67,11 +75,29 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
67
75
  * Rate limiting is best used for hard API limits or resource constraints. For UI updates or
68
76
  * smoothing out frequent events, throttling or debouncing usually provide better user experience.
69
77
  *
78
+ * Error Handling:
79
+ * - If an `onError` handler is provided, it will be called with the error and rate limiter instance
80
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
81
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
82
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
83
+ * - The error state can be checked using the underlying AsyncRateLimiter instance
84
+ * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler
85
+ *
70
86
  * @example
71
87
  * ```ts
72
88
  * const rateLimiter = new AsyncRateLimiter(
73
89
  * async (id: string) => await api.getData(id),
74
- * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window
90
+ * {
91
+ * limit: 5,
92
+ * window: 1000,
93
+ * windowType: 'sliding',
94
+ * onError: (error) => {
95
+ * console.error('API call failed:', error);
96
+ * },
97
+ * onReject: (limiter) => {
98
+ * console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);
99
+ * }
100
+ * }
75
101
  * );
76
102
  *
77
103
  * // Will execute immediately until limit reached, then block
@@ -92,13 +118,12 @@ export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
92
118
  constructor(fn: TFn, initialOptions: AsyncRateLimiterOptions<TFn>);
93
119
  /**
94
120
  * Updates the rate limiter options
95
- * Returns the new options state
96
121
  */
97
122
  setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void;
98
123
  /**
99
124
  * Returns the current rate limiter options
100
125
  */
101
- getOptions(): Required<AsyncRateLimiterOptions<TFn>>;
126
+ getOptions(): AsyncRateLimiterOptions<TFn>;
102
127
  /**
103
128
  * Returns the current enabled state of the rate limiter
104
129
  */
@@ -116,6 +141,19 @@ export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
116
141
  * Will reject execution if the number of calls in the current window exceeds the limit.
117
142
  * If execution is allowed, waits for any previous execution to complete before proceeding.
118
143
  *
144
+ * Error Handling:
145
+ * - If the rate-limited function throws and no `onError` handler is configured,
146
+ * the error will be thrown from this method.
147
+ * - If an `onError` handler is configured, errors will be caught and passed to the handler,
148
+ * and this method will return undefined.
149
+ * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler
150
+ * will be called if configured.
151
+ * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
152
+ * - Rate limit rejections can be tracked using `getRejectionCount()`.
153
+ *
154
+ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
155
+ * @throws The error from the rate-limited function if no onError handler is configured
156
+ *
119
157
  * @example
120
158
  * ```ts
121
159
  * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });
@@ -128,7 +166,7 @@ export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
128
166
  * ```
129
167
  */
130
168
  maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>;
131
- private executeFunction;
169
+ private execute;
132
170
  private rejectFunction;
133
171
  private cleanupOldExecutions;
134
172
  /**
@@ -187,6 +225,14 @@ export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
187
225
  * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
188
226
  * need to enforce a hard limit on the number of executions within a time period.
189
227
  *
228
+ * Error Handling:
229
+ * - If an `onError` handler is provided, it will be called with the error and rate limiter instance
230
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
231
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
232
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
233
+ * - The error state can be checked using the underlying AsyncRateLimiter instance
234
+ * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler
235
+ *
190
236
  * @example
191
237
  * ```ts
192
238
  * // Rate limit to 5 calls per minute with a sliding window
@@ -194,6 +240,9 @@ export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
194
240
  * limit: 5,
195
241
  * window: 60000,
196
242
  * windowType: 'sliding',
243
+ * onError: (error) => {
244
+ * console.error('API call failed:', error);
245
+ * },
197
246
  * onReject: (rateLimiter) => {
198
247
  * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
199
248
  * }
@@ -4,12 +4,6 @@ const utils = require("./utils.cjs");
4
4
  const defaultOptions = {
5
5
  enabled: true,
6
6
  leading: true,
7
- onError: () => {
8
- },
9
- onSettled: () => {
10
- },
11
- onSuccess: () => {
12
- },
13
7
  trailing: true,
14
8
  wait: 0
15
9
  };
@@ -24,14 +18,15 @@ class AsyncThrottler {
24
18
  this._settleCount = 0;
25
19
  this._successCount = 0;
26
20
  this._timeoutId = null;
21
+ this._resolvePreviousPromise = null;
27
22
  this._options = {
28
23
  ...defaultOptions,
29
- ...initialOptions
24
+ ...initialOptions,
25
+ throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
30
26
  };
31
27
  }
32
28
  /**
33
29
  * Updates the throttler options
34
- * Returns the new options state
35
30
  */
36
31
  setOptions(newOptions) {
37
32
  this._options = { ...this._options, ...newOptions };
@@ -49,7 +44,7 @@ class AsyncThrottler {
49
44
  * Returns the current enabled state of the throttler
50
45
  */
51
46
  getEnabled() {
52
- return utils.parseFunctionOrValue(this._options.enabled, this);
47
+ return !!utils.parseFunctionOrValue(this._options.enabled, this);
53
48
  }
54
49
  /**
55
50
  * Returns the current wait time in milliseconds
@@ -58,19 +53,31 @@ class AsyncThrottler {
58
53
  return utils.parseFunctionOrValue(this._options.wait, this);
59
54
  }
60
55
  /**
61
- * Attempts to execute the throttled function
62
- * If a call is already in progress, it may be blocked or queued depending on the `wait` option
56
+ * Attempts to execute the throttled function.
57
+ * If a call is already in progress, it may be blocked or queued depending on the `wait` option.
58
+ *
59
+ * Error Handling:
60
+ * - If the throttled function throws and no `onError` handler is configured,
61
+ * the error will be thrown from this method.
62
+ * - If an `onError` handler is configured, errors will be caught and passed to the handler,
63
+ * and this method will return undefined.
64
+ * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
65
+ *
66
+ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
67
+ * @throws The error from the throttled function if no onError handler is configured
63
68
  */
64
69
  async maybeExecute(...args) {
65
70
  const now = Date.now();
66
71
  const timeSinceLastExecution = now - this._lastExecutionTime;
67
72
  const wait = this.getWait();
73
+ this.resolvePreviousPromise();
68
74
  if (this._options.leading && timeSinceLastExecution >= wait) {
69
- await this.executeFunction(...args);
75
+ await this.execute(...args);
70
76
  return this._lastResult;
71
77
  } else {
72
78
  this._lastArgs = args;
73
79
  return new Promise((resolve) => {
80
+ this._resolvePreviousPromise = resolve;
74
81
  if (this._timeoutId) {
75
82
  clearTimeout(this._timeoutId);
76
83
  }
@@ -79,35 +86,48 @@ class AsyncThrottler {
79
86
  const timeoutDuration = wait - _timeSinceLastExecution;
80
87
  this._timeoutId = setTimeout(async () => {
81
88
  if (this._lastArgs !== void 0) {
82
- await this.executeFunction(...this._lastArgs);
89
+ await this.execute(...this._lastArgs);
83
90
  }
91
+ this._resolvePreviousPromise = null;
84
92
  resolve(this._lastResult);
85
93
  }, timeoutDuration);
86
94
  }
87
95
  });
88
96
  }
89
97
  }
90
- async executeFunction(...args) {
98
+ async execute(...args) {
99
+ var _a, _b, _c, _d, _e, _f;
91
100
  if (!this.getEnabled() || this._isExecuting) return void 0;
92
101
  this._abortController = new AbortController();
93
102
  try {
94
103
  this._isExecuting = true;
95
104
  this._lastResult = await this.fn(...args);
96
105
  this._successCount++;
97
- this._options.onSuccess(this._lastResult, this);
106
+ (_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this);
98
107
  } catch (error) {
99
108
  this._errorCount++;
100
- this._options.onError(error, this);
109
+ (_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
110
+ if (this._options.throwOnError) {
111
+ throw error;
112
+ } else {
113
+ console.error(error);
114
+ }
101
115
  } finally {
102
116
  this._isExecuting = false;
103
117
  this._settleCount++;
104
118
  this._abortController = null;
105
119
  this._lastExecutionTime = Date.now();
106
120
  this._nextExecutionTime = this._lastExecutionTime + this.getWait();
107
- this._options.onSettled(this);
121
+ (_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this);
108
122
  }
109
123
  return this._lastResult;
110
124
  }
125
+ resolvePreviousPromise() {
126
+ if (this._resolvePreviousPromise) {
127
+ this._resolvePreviousPromise(this._lastResult);
128
+ this._resolvePreviousPromise = null;
129
+ }
130
+ }
111
131
  /**
112
132
  * Cancels any pending execution or aborts any execution in progress
113
133
  */
@@ -120,6 +140,7 @@ class AsyncThrottler {
120
140
  this._abortController.abort();
121
141
  this._abortController = null;
122
142
  }
143
+ this.resolvePreviousPromise();
123
144
  this._lastArgs = void 0;
124
145
  }
125
146
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"async-throttler.cjs","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction } from './types'\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((throttler: AsyncThrottler<TFn>) => boolean)\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws\n */\n onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Whether to execute the function on the trailing edge of the wait period\n * Defaults to true\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: AsyncThrottler<TFn>) => number)\n}\n\nconst defaultOptions: Required<AsyncThrottlerOptions<any>> = {\n enabled: true,\n leading: true,\n onError: () => {},\n onSettled: () => {},\n onSuccess: () => {},\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, { wait: 1000 });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n private _options: Required<AsyncThrottlerOptions<TFn>>\n private _abortController: AbortController | null = null\n private _errorCount = 0\n private _isExecuting = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _lastResult: ReturnType<TFn> | undefined\n private _nextExecutionTime = 0\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the throttler options\n * Returns the new options state\n */\n setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current options\n */\n getOptions(): Required<AsyncThrottlerOptions<TFn>> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the throttler\n */\n getEnabled(): boolean {\n return parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function\n * If a call is already in progress, it may be blocked or queued depending on the `wait` option\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n const wait = this.getWait()\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= wait) {\n await this.executeFunction(...args)\n return this._lastResult\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n return new Promise((resolve) => {\n // Clear any existing timeout to ensure we use the latest arguments\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n }\n\n // Set up trailing execution if enabled\n if (this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(async () => {\n if (this._lastArgs !== undefined) {\n await this.executeFunction(...this._lastArgs)\n }\n resolve(this._lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n private async executeFunction(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled() || this._isExecuting) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError(error, this)\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._abortController = null\n this._lastExecutionTime = Date.now()\n this._nextExecutionTime = this._lastExecutionTime + this.getWait()\n this._options.onSettled(this)\n }\n return this._lastResult\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n this._lastArgs = undefined\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._nextExecutionTime\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the current pending state\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n\n /**\n * Returns the current executing state\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * @example\n * ```ts\n * const throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, { wait: 1000 });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute.bind(asyncThrottler)\n}\n"],"names":["parseFunctionOrValue"],"mappings":";;;AA8CA,MAAM,iBAAuD;AAAA,EAC3D,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS,MAAM;AAAA,EAAC;AAAA,EAChB,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AA4BO,MAAM,eAA6C;AAAA,EAaxD,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAZV,SAAQ,mBAA2C;AACnD,SAAQ,cAAc;AACtB,SAAQ,eAAe;AAEvB,SAAQ,qBAAqB;AAE7B,SAAQ,qBAAqB;AAC7B,SAAQ,eAAe;AACvB,SAAQ,gBAAgB;AACxB,SAAQ,aAAoC;AAM1C,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,WAAW,YAAuD;AAChE,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,OAAO;AAAA,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMF,aAAmD;AACjD,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAsB;AACpB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,SAAS,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,UAAkB;AAChB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtD,MAAM,gBACD,MACmC;AAChC,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,yBAAyB,MAAM,KAAK;AACpC,UAAA,OAAO,KAAK,QAAQ;AAG1B,QAAI,KAAK,SAAS,WAAW,0BAA0B,MAAM;AACrD,YAAA,KAAK,gBAAgB,GAAG,IAAI;AAClC,aAAO,KAAK;AAAA,IAAA,OACP;AAEL,WAAK,YAAY;AAEV,aAAA,IAAI,QAAQ,CAAC,YAAY;AAE9B,YAAI,KAAK,YAAY;AACnB,uBAAa,KAAK,UAAU;AAAA,QAAA;AAI1B,YAAA,KAAK,SAAS,UAAU;AAC1B,gBAAM,0BAA0B,KAAK,qBACjC,MAAM,KAAK,qBACX;AACJ,gBAAM,kBAAkB,OAAO;AAC1B,eAAA,aAAa,WAAW,YAAY;AACnC,gBAAA,KAAK,cAAc,QAAW;AAChC,oBAAM,KAAK,gBAAgB,GAAG,KAAK,SAAS;AAAA,YAAA;AAE9C,oBAAQ,KAAK,WAAW;AAAA,aACvB,eAAe;AAAA,QAAA;AAAA,MACpB,CACD;AAAA,IAAA;AAAA,EACH;AAAA,EAGF,MAAc,mBACT,MACmC;AACtC,QAAI,CAAC,KAAK,WAAA,KAAgB,KAAK,aAAqB,QAAA;AAC/C,SAAA,mBAAmB,IAAI,gBAAgB;AACxC,QAAA;AACF,WAAK,eAAe;AACpB,WAAK,cAAc,MAAM,KAAK,GAAG,GAAG,IAAI;AACnC,WAAA;AACL,WAAK,SAAS,UAAU,KAAK,aAAc,IAAI;AAAA,aACxC,OAAO;AACT,WAAA;AACA,WAAA,SAAS,QAAQ,OAAO,IAAI;AAAA,IAAA,UACjC;AACA,WAAK,eAAe;AACf,WAAA;AACL,WAAK,mBAAmB;AACnB,WAAA,qBAAqB,KAAK,IAAI;AACnC,WAAK,qBAAqB,KAAK,qBAAqB,KAAK,QAAQ;AAC5D,WAAA,SAAS,UAAU,IAAI;AAAA,IAAA;AAE9B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAAA,IAAA;AAEpB,QAAI,KAAK,kBAAkB;AACzB,WAAK,iBAAiB,MAAM;AAC5B,WAAK,mBAAmB;AAAA,IAAA;AAE1B,SAAK,YAAY;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMnB,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,gBAA6C;AAC3C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,kBAA0B;AACxB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,iBAAyB;AACvB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,gBAAwB;AACtB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACtB,WAAO,KAAK,WAAA,KAAgB,CAAC,CAAC,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMrC,iBAA0B;AACxB,WAAO,KAAK;AAAA,EAAA;AAEhB;AAuBgB,SAAA,cACd,IACA,gBACA;AACA,QAAM,iBAAiB,IAAI,eAAe,IAAI,cAAc;AACrD,SAAA,eAAe,aAAa,KAAK,cAAc;AACxD;;;"}
1
+ {"version":3,"file":"async-throttler.cjs","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((throttler: AsyncThrottler<TFn>) => boolean)\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Whether to execute the function on the trailing edge of the wait period\n * Defaults to true\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: AsyncThrottler<TFn>) => number)\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncThrottler instance\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n private _options: AsyncThrottlerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _errorCount = 0\n private _isExecuting = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _lastResult: ReturnType<TFn> | undefined\n private _nextExecutionTime = 0\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current options\n */\n getOptions(): AsyncThrottlerOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the throttler\n */\n getEnabled(): boolean {\n return !!parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function.\n * If a call is already in progress, it may be blocked or queued depending on the `wait` option.\n *\n * Error Handling:\n * - If the throttled function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the throttled function if no onError handler is configured\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n const wait = this.getWait()\n\n this.resolvePreviousPromise()\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= wait) {\n await this.execute(...args)\n return this._lastResult\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n }\n\n // Set up trailing execution if enabled\n if (this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(async () => {\n if (this._lastArgs !== undefined) {\n await this.execute(...this._lastArgs)\n }\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled() || this._isExecuting) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._abortController = null\n this._lastExecutionTime = Date.now()\n this._nextExecutionTime = this._lastExecutionTime + this.getWait()\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n private resolvePreviousPromise(): void {\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n this.resolvePreviousPromise()\n this._lastArgs = undefined\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._nextExecutionTime\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the current pending state\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n\n /**\n * Returns the current executing state\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncThrottler instance\n *\n * @example\n * ```ts\n * const throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute.bind(asyncThrottler)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"}
@@ -15,7 +15,9 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
15
15
  */
16
16
  leading?: boolean;
17
17
  /**
18
- * Optional error handler for when the throttled function throws
18
+ * Optional error handler for when the throttled function throws.
19
+ * If provided, the handler will be called with the error and throttler instance.
20
+ * This can be used alongside throwOnError - the handler will be called before any error is thrown.
19
21
  */
20
22
  onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void;
21
23
  /**
@@ -26,6 +28,12 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
26
28
  * Optional function to call when the throttled function is executed
27
29
  */
28
30
  onSuccess?: (result: ReturnType<TFn>, asyncThrottler: AsyncThrottler<TFn>) => void;
31
+ /**
32
+ * Whether to throw errors when they occur.
33
+ * Defaults to true if no onError handler is provided, false if an onError handler is provided.
34
+ * Can be explicitly set to override these defaults.
35
+ */
36
+ throwOnError?: boolean;
29
37
  /**
30
38
  * Whether to execute the function on the trailing edge of the wait period
31
39
  * Defaults to true
@@ -52,12 +60,24 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
52
60
  * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to
53
61
  * ensure a maximum execution frequency.
54
62
  *
63
+ * Error Handling:
64
+ * - If an `onError` handler is provided, it will be called with the error and throttler instance
65
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
66
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
67
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
68
+ * - The error state can be checked using the underlying AsyncThrottler instance
69
+ *
55
70
  * @example
56
71
  * ```ts
57
72
  * const throttler = new AsyncThrottler(async (value: string) => {
58
73
  * const result = await saveToAPI(value);
59
74
  * return result; // Return value is preserved
60
- * }, { wait: 1000 });
75
+ * }, {
76
+ * wait: 1000,
77
+ * onError: (error) => {
78
+ * console.error('API call failed:', error);
79
+ * }
80
+ * });
61
81
  *
62
82
  * // Will only execute once per second no matter how often called
63
83
  * // Returns the API response directly
@@ -77,16 +97,16 @@ export declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
77
97
  private _settleCount;
78
98
  private _successCount;
79
99
  private _timeoutId;
100
+ private _resolvePreviousPromise;
80
101
  constructor(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>);
81
102
  /**
82
103
  * Updates the throttler options
83
- * Returns the new options state
84
104
  */
85
105
  setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void;
86
106
  /**
87
107
  * Returns the current options
88
108
  */
89
- getOptions(): Required<AsyncThrottlerOptions<TFn>>;
109
+ getOptions(): AsyncThrottlerOptions<TFn>;
90
110
  /**
91
111
  * Returns the current enabled state of the throttler
92
112
  */
@@ -96,11 +116,22 @@ export declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
96
116
  */
97
117
  getWait(): number;
98
118
  /**
99
- * Attempts to execute the throttled function
100
- * If a call is already in progress, it may be blocked or queued depending on the `wait` option
119
+ * Attempts to execute the throttled function.
120
+ * If a call is already in progress, it may be blocked or queued depending on the `wait` option.
121
+ *
122
+ * Error Handling:
123
+ * - If the throttled function throws and no `onError` handler is configured,
124
+ * the error will be thrown from this method.
125
+ * - If an `onError` handler is configured, errors will be caught and passed to the handler,
126
+ * and this method will return undefined.
127
+ * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
128
+ *
129
+ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
130
+ * @throws The error from the throttled function if no onError handler is configured
101
131
  */
102
132
  maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>;
103
- private executeFunction;
133
+ private execute;
134
+ private resolvePreviousPromise;
104
135
  /**
105
136
  * Cancels any pending execution or aborts any execution in progress
106
137
  */
@@ -147,12 +178,24 @@ export declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
147
178
  * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
148
179
  * instead of setting the result on a state variable from within the throttled function.
149
180
  *
181
+ * Error Handling:
182
+ * - If an `onError` handler is provided, it will be called with the error and throttler instance
183
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
184
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
185
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
186
+ * - The error state can be checked using the underlying AsyncThrottler instance
187
+ *
150
188
  * @example
151
189
  * ```ts
152
190
  * const throttled = asyncThrottle(async (value: string) => {
153
191
  * const result = await saveToAPI(value);
154
192
  * return result; // Return value is preserved
155
- * }, { wait: 1000 });
193
+ * }, {
194
+ * wait: 1000,
195
+ * onError: (error) => {
196
+ * console.error('API call failed:', error);
197
+ * }
198
+ * });
156
199
  *
157
200
  * // This will execute at most once per second
158
201
  * // Returns the API response directly
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const defaultOptions = {
4
+ getShouldExecute: () => false,
5
+ maxSize: Infinity,
6
+ started: true,
7
+ wait: Infinity
8
+ };
9
+ class Batcher {
10
+ constructor(fn, initialOptions) {
11
+ this.fn = fn;
12
+ this._batchExecutionCount = 0;
13
+ this._itemExecutionCount = 0;
14
+ this._items = [];
15
+ this._timeoutId = null;
16
+ this._options = { ...defaultOptions, ...initialOptions };
17
+ this._running = this._options.started;
18
+ }
19
+ /**
20
+ * Updates the batcher options
21
+ */
22
+ setOptions(newOptions) {
23
+ this._options = { ...this._options, ...newOptions };
24
+ }
25
+ /**
26
+ * Returns the current batcher options
27
+ */
28
+ getOptions() {
29
+ return this._options;
30
+ }
31
+ /**
32
+ * Adds an item to the batcher
33
+ * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
34
+ */
35
+ addItem(item) {
36
+ var _a, _b;
37
+ this._items.push(item);
38
+ (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
39
+ const shouldProcess = this._items.length >= this._options.maxSize || this._options.getShouldExecute(this._items, this);
40
+ if (shouldProcess) {
41
+ this.execute();
42
+ } else if (this._running && !this._timeoutId && this._options.wait !== Infinity) {
43
+ this._timeoutId = setTimeout(() => this.execute(), this._options.wait);
44
+ }
45
+ }
46
+ /**
47
+ * Processes the current batch of items.
48
+ * This method will automatically be triggered if the batcher is running and any of these conditions are met:
49
+ * - The number of items reaches batchSize
50
+ * - The wait duration has elapsed
51
+ * - The getShouldExecute function returns true upon adding an item
52
+ *
53
+ * You can also call this method manually to process the current batch at any time.
54
+ */
55
+ execute() {
56
+ var _a, _b, _c, _d;
57
+ if (this._timeoutId) {
58
+ clearTimeout(this._timeoutId);
59
+ this._timeoutId = null;
60
+ }
61
+ if (this._items.length === 0) {
62
+ return;
63
+ }
64
+ const batch2 = this.getAllItems();
65
+ this._items = [];
66
+ (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
67
+ this.fn(batch2);
68
+ this._batchExecutionCount++;
69
+ this._itemExecutionCount += batch2.length;
70
+ (_d = (_c = this._options).onExecute) == null ? void 0 : _d.call(_c, this);
71
+ }
72
+ /**
73
+ * Stops the batcher from processing batches
74
+ */
75
+ stop() {
76
+ var _a, _b;
77
+ this._running = false;
78
+ (_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
79
+ if (this._timeoutId) {
80
+ clearTimeout(this._timeoutId);
81
+ this._timeoutId = null;
82
+ }
83
+ }
84
+ /**
85
+ * Starts the batcher and processes any pending items
86
+ */
87
+ start() {
88
+ var _a, _b;
89
+ this._running = true;
90
+ (_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
91
+ if (this._items.length > 0 && !this._timeoutId) {
92
+ this._timeoutId = setTimeout(() => this.execute(), this._options.wait);
93
+ }
94
+ }
95
+ /**
96
+ * Returns the current number of items in the batcher
97
+ */
98
+ getSize() {
99
+ return this._items.length;
100
+ }
101
+ /**
102
+ * Returns true if the batcher is empty
103
+ */
104
+ getIsEmpty() {
105
+ return this._items.length === 0;
106
+ }
107
+ /**
108
+ * Returns true if the batcher is running
109
+ */
110
+ getIsRunning() {
111
+ return this._running;
112
+ }
113
+ /**
114
+ * Returns a copy of all items currently in the batcher
115
+ */
116
+ getAllItems() {
117
+ return [...this._items];
118
+ }
119
+ /**
120
+ * Returns the number of times batches have been processed
121
+ */
122
+ getBatchExecutionCount() {
123
+ return this._batchExecutionCount;
124
+ }
125
+ /**
126
+ * Returns the total number of individual items that have been processed
127
+ */
128
+ getItemExecutionCount() {
129
+ return this._itemExecutionCount;
130
+ }
131
+ }
132
+ function batch(fn, options) {
133
+ const batcher = new Batcher(fn, options);
134
+ return batcher.addItem.bind(batcher);
135
+ }
136
+ exports.Batcher = Batcher;
137
+ exports.batch = batch;
138
+ //# sourceMappingURL=batcher.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batcher.cjs","sources":["../../src/batcher.ts"],"sourcesContent":["import type { OptionalKeys } from './types'\n\n/**\n * Options for configuring a Batcher instance\n */\nexport interface BatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Callback fired after a batch is processed\n */\n onExecute?: (batcher: Batcher<TValue>) => void\n /**\n * Callback fired when the batcher's running state changes\n */\n onIsRunningChange?: (batcher: Batcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: Batcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'onExecute' | 'onItemsChange' | 'onIsRunningChange'\n>\n\nconst defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches.\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The Batcher provides a flexible way to implement batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n *\n * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecuteBatch: (items) => console.log('Batch executed:', items)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n private _options: BatcherOptionsWithOptionalCallbacks<TValue>\n private _batchExecutionCount = 0\n private _itemExecutionCount = 0\n private _items: Array<TValue> = []\n private _running: boolean\n private _timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\n }\n\n /**\n * Updates the batcher options\n */\n setOptions(newOptions: Partial<BatcherOptions<TValue>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current batcher options\n */\n getOptions(): BatcherOptions<TValue> {\n return this._options\n }\n\n /**\n * Adds an item to the batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n */\n addItem(item: TValue): void {\n this._items.push(item)\n this._options.onItemsChange?.(this)\n\n const shouldProcess =\n this._items.length >= this._options.maxSize ||\n this._options.getShouldExecute(this._items, this)\n\n if (shouldProcess) {\n this.execute()\n } else if (\n this._running &&\n !this._timeoutId &&\n this._options.wait !== Infinity\n ) {\n this._timeoutId = setTimeout(() => this.execute(), this._options.wait)\n }\n }\n\n /**\n * Processes the current batch of items.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches batchSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n */\n execute(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n\n if (this._items.length === 0) {\n return\n }\n\n const batch = this.getAllItems() // copy of the items to be processed (to prevent race conditions)\n this._items = [] // Clear items before processing to prevent race conditions\n this._options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed\n\n this.fn(batch)\n this._batchExecutionCount++\n this._itemExecutionCount += batch.length\n this._options.onExecute?.(this)\n }\n\n /**\n * Stops the batcher from processing batches\n */\n stop(): void {\n this._running = false\n this._options.onIsRunningChange?.(this)\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n }\n\n /**\n * Starts the batcher and processes any pending items\n */\n start(): void {\n this._running = true\n this._options.onIsRunningChange?.(this)\n if (this._items.length > 0 && !this._timeoutId) {\n this._timeoutId = setTimeout(() => this.execute(), this._options.wait)\n }\n }\n\n /**\n * Returns the current number of items in the batcher\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns true if the batcher is empty\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the batcher is running\n */\n getIsRunning(): boolean {\n return this._running\n }\n\n /**\n * Returns a copy of all items currently in the batcher\n */\n getAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of times batches have been processed\n */\n getBatchExecutionCount(): number {\n return this._batchExecutionCount\n }\n\n /**\n * Returns the total number of individual items that have been processed\n */\n getItemExecutionCount(): number {\n return this._itemExecutionCount\n }\n}\n\n/**\n * Creates a batcher that processes items in batches\n *\n * @example\n * ```ts\n * const batchItems = batch<number>({\n * batchSize: 3,\n * processBatch: (items) => console.log('Processing:', items)\n * });\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function batch<TValue>(\n fn: (items: Array<TValue>) => void,\n options: BatcherOptions<TValue>,\n) {\n const batcher = new Batcher<TValue>(fn, options)\n return batcher.addItem.bind(batcher)\n}\n"],"names":["batch"],"mappings":";;AA+CA,MAAM,iBAA2D;AAAA,EAC/D,kBAAkB,MAAM;AAAA,EACxB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AACR;AA+BO,MAAM,QAAgB;AAAA,EAQ3B,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,uBAAuB;AAC/B,SAAQ,sBAAsB;AAC9B,SAAQ,SAAwB,CAAC;AAEjC,SAAQ,aAAoC;AAM1C,SAAK,WAAW,EAAE,GAAG,gBAAgB,GAAG,eAAe;AAClD,SAAA,WAAW,KAAK,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhC,WAAW,YAAmD;AAC5D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMpD,aAAqC;AACnC,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd,QAAQ,MAAoB;;AACrB,SAAA,OAAO,KAAK,IAAI;AAChB,qBAAA,UAAS,kBAAT,4BAAyB;AAE9B,UAAM,gBACJ,KAAK,OAAO,UAAU,KAAK,SAAS,WACpC,KAAK,SAAS,iBAAiB,KAAK,QAAQ,IAAI;AAElD,QAAI,eAAe;AACjB,WAAK,QAAQ;AAAA,IAAA,WAEb,KAAK,YACL,CAAC,KAAK,cACN,KAAK,SAAS,SAAS,UACvB;AACK,WAAA,aAAa,WAAW,MAAM,KAAK,WAAW,KAAK,SAAS,IAAI;AAAA,IAAA;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYF,UAAgB;;AACd,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAAA,IAAA;AAGhB,QAAA,KAAK,OAAO,WAAW,GAAG;AAC5B;AAAA,IAAA;AAGIA,UAAAA,SAAQ,KAAK,YAAY;AAC/B,SAAK,SAAS,CAAC;AACV,qBAAA,UAAS,kBAAT,4BAAyB;AAE9B,SAAK,GAAGA,MAAK;AACR,SAAA;AACL,SAAK,uBAAuBA,OAAM;AAC7B,qBAAA,UAAS,cAAT,4BAAqB;AAAA,EAAI;AAAA;AAAA;AAAA;AAAA,EAMhC,OAAa;;AACX,SAAK,WAAW;AACX,qBAAA,UAAS,sBAAT,4BAA6B;AAClC,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,QAAc;;AACZ,SAAK,WAAW;AACX,qBAAA,UAAS,sBAAT,4BAA6B;AAClC,QAAI,KAAK,OAAO,SAAS,KAAK,CAAC,KAAK,YAAY;AACzC,WAAA,aAAa,WAAW,MAAM,KAAK,WAAW,KAAK,SAAS,IAAI;AAAA,IAAA;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA,EAMF,UAAkB;AAChB,WAAO,KAAK,OAAO;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMrB,aAAsB;AACb,WAAA,KAAK,OAAO,WAAW;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhC,eAAwB;AACtB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,cAA6B;AACpB,WAAA,CAAC,GAAG,KAAK,MAAM;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMxB,yBAAiC;AAC/B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,wBAAgC;AAC9B,WAAO,KAAK;AAAA,EAAA;AAEhB;AAiBgB,SAAA,MACd,IACA,SACA;AACA,QAAM,UAAU,IAAI,QAAgB,IAAI,OAAO;AACxC,SAAA,QAAQ,QAAQ,KAAK,OAAO;AACrC;;;"}