@tanstack/pacer 0.9.0 → 0.10.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.
- package/dist/cjs/async-batcher.cjs +1 -12
- package/dist/cjs/async-batcher.cjs.map +1 -1
- package/dist/cjs/async-batcher.d.cts +4 -16
- package/dist/cjs/async-debouncer.cjs +25 -8
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +1 -1
- package/dist/cjs/async-queuer.cjs +20 -9
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +10 -5
- package/dist/cjs/async-rate-limiter.cjs +53 -12
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +8 -0
- package/dist/cjs/async-throttler.cjs +17 -6
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +1 -1
- package/dist/cjs/batcher.cjs +1 -12
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +4 -16
- package/dist/cjs/queuer.cjs +16 -4
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +13 -8
- package/dist/cjs/rate-limiter.cjs +53 -12
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +8 -0
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +4 -4
- package/dist/esm/async-batcher.d.ts +4 -16
- package/dist/esm/async-batcher.js +1 -12
- package/dist/esm/async-batcher.js.map +1 -1
- package/dist/esm/async-debouncer.d.ts +1 -1
- package/dist/esm/async-debouncer.js +25 -8
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +10 -5
- package/dist/esm/async-queuer.js +20 -9
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +8 -0
- package/dist/esm/async-rate-limiter.js +53 -12
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +1 -1
- package/dist/esm/async-throttler.js +17 -6
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +4 -16
- package/dist/esm/batcher.js +1 -12
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/queuer.d.ts +13 -8
- package/dist/esm/queuer.js +16 -4
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +8 -0
- package/dist/esm/rate-limiter.js +53 -12
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +4 -4
- package/dist/esm/throttler.js.map +1 -1
- package/package.json +1 -1
- package/src/async-batcher.ts +5 -28
- package/src/async-debouncer.ts +27 -8
- package/src/async-queuer.ts +32 -14
- package/src/async-rate-limiter.ts +72 -12
- package/src/async-throttler.ts +19 -6
- package/src/batcher.ts +5 -28
- package/src/queuer.ts +29 -12
- package/src/rate-limiter.ts +71 -11
- package/src/throttler.ts +4 -4
|
@@ -4,11 +4,13 @@ function getDefaultAsyncRateLimiterState() {
|
|
|
4
4
|
return {
|
|
5
5
|
errorCount: 0,
|
|
6
6
|
executionTimes: [],
|
|
7
|
+
isExceeded: false,
|
|
7
8
|
isExecuting: false,
|
|
8
9
|
lastResult: void 0,
|
|
9
10
|
rejectionCount: 0,
|
|
10
11
|
settleCount: 0,
|
|
11
|
-
successCount: 0
|
|
12
|
+
successCount: 0,
|
|
13
|
+
status: "idle"
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
const defaultOptions = {
|
|
@@ -22,6 +24,7 @@ class AsyncRateLimiter {
|
|
|
22
24
|
constructor(fn, initialOptions) {
|
|
23
25
|
this.fn = fn;
|
|
24
26
|
this.store = new Store(getDefaultAsyncRateLimiterState());
|
|
27
|
+
this.#timeoutIds = /* @__PURE__ */ new Set();
|
|
25
28
|
this.setOptions = (newOptions) => {
|
|
26
29
|
this.options = { ...this.options, ...newOptions };
|
|
27
30
|
};
|
|
@@ -31,7 +34,13 @@ class AsyncRateLimiter {
|
|
|
31
34
|
...state,
|
|
32
35
|
...newState
|
|
33
36
|
};
|
|
34
|
-
|
|
37
|
+
const isExceeded = combinedState.executionTimes.length >= this.#getLimit();
|
|
38
|
+
const status = !this.#getEnabled() ? "disabled" : combinedState.isExecuting ? "executing" : isExceeded ? "exceeded" : "idle";
|
|
39
|
+
return {
|
|
40
|
+
...combinedState,
|
|
41
|
+
isExceeded,
|
|
42
|
+
status
|
|
43
|
+
};
|
|
35
44
|
});
|
|
36
45
|
};
|
|
37
46
|
this.#getEnabled = () => {
|
|
@@ -45,7 +54,7 @@ class AsyncRateLimiter {
|
|
|
45
54
|
};
|
|
46
55
|
this.maybeExecute = async (...args) => {
|
|
47
56
|
this.#cleanupOldExecutions();
|
|
48
|
-
const relevantExecutionTimes = this.#
|
|
57
|
+
const relevantExecutionTimes = this.#getExecutionTimesInWindow();
|
|
49
58
|
if (relevantExecutionTimes.length < this.#getLimit()) {
|
|
50
59
|
await this.#execute(...args);
|
|
51
60
|
return this.store.state.lastResult;
|
|
@@ -66,6 +75,7 @@ class AsyncRateLimiter {
|
|
|
66
75
|
});
|
|
67
76
|
try {
|
|
68
77
|
const result = await this.fn(...args);
|
|
78
|
+
this.#setCleanupTimeout(now);
|
|
69
79
|
this.#setState({
|
|
70
80
|
successCount: this.store.state.successCount + 1,
|
|
71
81
|
lastResult: result
|
|
@@ -88,30 +98,53 @@ class AsyncRateLimiter {
|
|
|
88
98
|
}
|
|
89
99
|
return this.store.state.lastResult;
|
|
90
100
|
};
|
|
91
|
-
this.#
|
|
101
|
+
this.#getExecutionTimesInWindow = () => {
|
|
92
102
|
if (this.options.windowType === "sliding") {
|
|
93
103
|
return this.store.state.executionTimes.filter(
|
|
94
104
|
(time) => time > Date.now() - this.#getWindow()
|
|
95
105
|
);
|
|
96
106
|
} else {
|
|
107
|
+
if (this.store.state.executionTimes.length === 0) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
97
110
|
const oldestExecution = Math.min(...this.store.state.executionTimes);
|
|
98
111
|
const windowStart = oldestExecution;
|
|
112
|
+
const windowEnd = windowStart + this.#getWindow();
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
if (now > windowEnd) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
99
117
|
return this.store.state.executionTimes.filter(
|
|
100
|
-
(time) => time >= windowStart && time <=
|
|
118
|
+
(time) => time >= windowStart && time <= windowEnd
|
|
101
119
|
);
|
|
102
120
|
}
|
|
103
121
|
};
|
|
122
|
+
this.#setCleanupTimeout = (executionTime) => {
|
|
123
|
+
if (this.options.windowType === "sliding" || this.#timeoutIds.size === 0) {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const timeUntilExpiration = executionTime - now + this.#getWindow() + 1;
|
|
126
|
+
const timeoutId = setTimeout(() => {
|
|
127
|
+
this.#cleanupOldExecutions();
|
|
128
|
+
this.#clearTimeout(timeoutId);
|
|
129
|
+
}, timeUntilExpiration);
|
|
130
|
+
this.#timeoutIds.add(timeoutId);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
this.#clearTimeout = (timeoutId) => {
|
|
134
|
+
clearTimeout(timeoutId);
|
|
135
|
+
this.#timeoutIds.delete(timeoutId);
|
|
136
|
+
};
|
|
137
|
+
this.#clearTimeouts = () => {
|
|
138
|
+
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
|
|
139
|
+
this.#timeoutIds.clear();
|
|
140
|
+
};
|
|
104
141
|
this.#cleanupOldExecutions = () => {
|
|
105
|
-
const now = Date.now();
|
|
106
|
-
const windowStart = now - this.#getWindow();
|
|
107
142
|
this.#setState({
|
|
108
|
-
executionTimes: this
|
|
109
|
-
(time) => time > windowStart
|
|
110
|
-
)
|
|
143
|
+
executionTimes: this.#getExecutionTimesInWindow()
|
|
111
144
|
});
|
|
112
145
|
};
|
|
113
146
|
this.getRemainingInWindow = () => {
|
|
114
|
-
const relevantExecutionTimes = this.#
|
|
147
|
+
const relevantExecutionTimes = this.#getExecutionTimesInWindow();
|
|
115
148
|
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length);
|
|
116
149
|
};
|
|
117
150
|
this.getMsUntilNextWindow = () => {
|
|
@@ -123,6 +156,7 @@ class AsyncRateLimiter {
|
|
|
123
156
|
};
|
|
124
157
|
this.reset = () => {
|
|
125
158
|
this.#setState(getDefaultAsyncRateLimiterState());
|
|
159
|
+
this.#clearTimeouts();
|
|
126
160
|
};
|
|
127
161
|
this.options = {
|
|
128
162
|
...defaultOptions,
|
|
@@ -130,13 +164,20 @@ class AsyncRateLimiter {
|
|
|
130
164
|
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
|
|
131
165
|
};
|
|
132
166
|
this.#setState(this.options.initialState ?? {});
|
|
167
|
+
for (const executionTime of this.#getExecutionTimesInWindow()) {
|
|
168
|
+
this.#setCleanupTimeout(executionTime);
|
|
169
|
+
}
|
|
133
170
|
}
|
|
171
|
+
#timeoutIds;
|
|
134
172
|
#setState;
|
|
135
173
|
#getEnabled;
|
|
136
174
|
#getLimit;
|
|
137
175
|
#getWindow;
|
|
138
176
|
#execute;
|
|
139
|
-
#
|
|
177
|
+
#getExecutionTimesInWindow;
|
|
178
|
+
#setCleanupTimeout;
|
|
179
|
+
#clearTimeout;
|
|
180
|
+
#clearTimeouts;
|
|
140
181
|
#cleanupOldExecutions;
|
|
141
182
|
}
|
|
142
183
|
function asyncRateLimit(fn, initialOptions) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-rate-limiter.js","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate-limited function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExecuting: false,\n lastResult: undefined,\n rejectionCount: 0,\n settleCount: 0,\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the rate limiter 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 | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<AsyncRateLimiterState<TFn>>\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a function that returns a number.\n */\n limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Optional error handler for when the rate-limited function throws.\n * If provided, the handler will be called with the error and rate limiter instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n rateLimiter: AsyncRateLimiter<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 * Time window in milliseconds within which the limit applies.\n * Can be a number or a function that returns a number.\n */\n window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess'\n> = {\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n throwOnError: true,\n}\n\n/**\n * A class that creates an async rate-limited function.\n *\n * Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited 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 rate-limited function.\n *\n * For smoother execution patterns, consider using:\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(\n * async (id: string) => await api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (limiter) => {\n * console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);\n * }\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * // Returns the API response directly\n * const data = await rateLimiter.maybeExecute('123');\n * ```\n */\nexport class AsyncRateLimiter<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<\n AsyncRateLimiterState<TFn>\n >(getDefaultAsyncRateLimiterState<TFn>())\n options: AsyncRateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the async rate limiter options\n */\n setOptions = (newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRateLimiterState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n return combinedState\n })\n }\n\n /**\n * Returns the current enabled state of the async rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * Error Handling:\n * - If the rate-limited 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 rate-limited function if no onError handler is configured\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return a promise that resolves with the result\n * const result = await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will return undefined\n * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(this)\n return undefined\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return\n\n const now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n const result = await this.fn(...args)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(this)\n }\n\n return this.store.state.lastResult\n }\n\n #getRelevantExecutionTimes = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n return this.store.state.executionTimes.filter(\n (time) =>\n time >= windowStart && time <= windowStart + this.#getWindow(),\n )\n }\n }\n\n #cleanupOldExecutions = (): void => {\n const now = Date.now()\n const windowStart = now - this.#getWindow()\n this.#setState({\n executionTimes: this.store.state.executionTimes.filter(\n (time) => time > windowStart,\n ),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n * For fixed windows, this is the time until the current window resets\n * For sliding windows, this is the time until the oldest execution expires\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\n }\n}\n\n/**\n * Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited 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 rate-limited function.\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = asyncRateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * // Returns the API response directly\n * const result = await rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function asyncRateLimit<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n) {\n const rateLimiter = new AsyncRateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"names":[],"mappings":";;AAmCA,SAAS,kCAEuB;AAC9B,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,gBAAgB,CAAA;AAAA,IAChB,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,cAAc;AAAA,EAAA;AAElB;AA8DA,MAAM,iBAGF;AAAA,EACF,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,cAAc;AAChB;AAoEO,MAAM,iBAA+C;AAAA,EAM1D,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AANV,SAAS,QAAqD,IAAI,MAEhE,gCAAA,CAAsC;AAkBxC,SAAA,aAAa,CAAC,eAA4D;AACxE,WAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,WAAA;AAAA,IAAW;AAGlD,SAAA,YAAY,CAAC,aAAwD;AACnE,WAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,cAAM,gBAAgB;AAAA,UACpB,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAEL,eAAO;AAAA,MAAA,CACR;AAAA,IAAA;AAMH,SAAA,cAAc,MAAe;AAC3B,aAAO,CAAC,CAAC,qBAAqB,KAAK,QAAQ,SAAS,IAAI;AAAA,IAAA;AAM1D,SAAA,YAAY,MAAc;AACxB,aAAO,qBAAqB,KAAK,QAAQ,OAAO,IAAI;AAAA,IAAA;AAMtD,SAAA,aAAa,MAAc;AACzB,aAAO,qBAAqB,KAAK,QAAQ,QAAQ,IAAI;AAAA,IAAA;AA4BvD,SAAA,eAAe,UACV,SACsC;AACzC,WAAK,sBAAA;AAEL,YAAM,yBAAyB,KAAK,2BAAA;AAEpC,UAAI,uBAAuB,SAAS,KAAK,UAAA,GAAa;AACpD,cAAM,KAAK,SAAS,GAAG,IAAI;AAC3B,eAAO,KAAK,MAAM,MAAM;AAAA,MAAA;AAG1B,WAAK,UAAU;AAAA,QACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;AAAA,MAAA,CACnD;AACD,WAAK,QAAQ,WAAW,IAAI;AAC5B,aAAO;AAAA,IAAA;AAGT,SAAA,WAAW,UACN,SACsC;AACzC,UAAI,CAAC,KAAK,cAAe;AAEzB,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,iBAAiB,CAAC,GAAG,KAAK,MAAM,MAAM,gBAAgB,GAAG;AAC/D,WAAK,UAAU;AAAA,QACb,aAAa;AAAA,QACb;AAAA,MAAA,CACD;AAED,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,GAAG,GAAG,IAAI;AACpC,aAAK,UAAU;AAAA,UACb,cAAc,KAAK,MAAM,MAAM,eAAe;AAAA,UAC9C,YAAY;AAAA,QAAA,CACb;AACD,aAAK,QAAQ,YAAY,QAAQ,IAAI;AAAA,MAAA,SAC9B,OAAO;AACd,aAAK,UAAU;AAAA,UACb,YAAY,KAAK,MAAM,MAAM,aAAa;AAAA,QAAA,CAC3C;AACD,aAAK,QAAQ,UAAU,OAAO,IAAI;AAClC,YAAI,KAAK,QAAQ,cAAc;AAC7B,gBAAM;AAAA,QAAA;AAAA,MACR,UACF;AACE,aAAK,UAAU;AAAA,UACb,aAAa;AAAA,UACb,aAAa,KAAK,MAAM,MAAM,cAAc;AAAA,QAAA,CAC7C;AACD,aAAK,QAAQ,YAAY,IAAI;AAAA,MAAA;AAG/B,aAAO,KAAK,MAAM,MAAM;AAAA,IAAA;AAG1B,SAAA,6BAA6B,MAAqB;AAChD,UAAI,KAAK,QAAQ,eAAe,WAAW;AAEzC,eAAO,KAAK,MAAM,MAAM,eAAe;AAAA,UACrC,CAAC,SAAS,OAAO,KAAK,IAAA,IAAQ,KAAK,WAAA;AAAA,QAAW;AAAA,MAChD,OACK;AAGL,cAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,cAAc;AACnE,cAAM,cAAc;AACpB,eAAO,KAAK,MAAM,MAAM,eAAe;AAAA,UACrC,CAAC,SACC,QAAQ,eAAe,QAAQ,cAAc,KAAK,WAAA;AAAA,QAAW;AAAA,MACjE;AAAA,IACF;AAGF,SAAA,wBAAwB,MAAY;AAClC,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,cAAc,MAAM,KAAK,WAAA;AAC/B,WAAK,UAAU;AAAA,QACb,gBAAgB,KAAK,MAAM,MAAM,eAAe;AAAA,UAC9C,CAAC,SAAS,OAAO;AAAA,QAAA;AAAA,MACnB,CACD;AAAA,IAAA;AAMH,SAAA,uBAAuB,MAAc;AACnC,YAAM,yBAAyB,KAAK,2BAAA;AACpC,aAAO,KAAK,IAAI,GAAG,KAAK,UAAA,IAAc,uBAAuB,MAAM;AAAA,IAAA;AAQrE,SAAA,uBAAuB,MAAc;AACnC,UAAI,KAAK,qBAAA,IAAyB,GAAG;AACnC,eAAO;AAAA,MAAA;AAET,YAAM,kBAAkB,KAAK,MAAM,MAAM,eAAe,CAAC,KAAK;AAC9D,aAAO,kBAAkB,KAAK,WAAA,IAAe,KAAK,IAAA;AAAA,IAAI;AAMxD,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,iCAAiC;AAAA,IAAA;AArLhD,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG;AAAA,MACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;AAAA,IAAA;AAE/D,SAAK,UAAU,KAAK,QAAQ,gBAAgB,CAAA,CAAE;AAAA,EAAA;AAAA,EAUhD;AAAA,EAaA;AAAA,EAOA;AAAA,EAOA;AAAA,EAgDA;AAAA,EAsCA;AAAA,EAkBA;AAqCF;AAmEO,SAAS,eACd,IACA,gBACA;AACA,QAAM,cAAc,IAAI,iBAAiB,IAAI,cAAc;AAC3D,SAAO,YAAY;AACrB;"}
|
|
1
|
+
{"version":3,"file":"async-rate-limiter.js","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Whether the rate-limited function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'executing' | 'exceeded' | 'idle'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExceeded: false,\n isExecuting: false,\n lastResult: undefined,\n rejectionCount: 0,\n settleCount: 0,\n successCount: 0,\n status: 'idle',\n }\n}\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the rate limiter 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 | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<AsyncRateLimiterState<TFn>>\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a function that returns a number.\n */\n limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Optional error handler for when the rate-limited function throws.\n * If provided, the handler will be called with the error and rate limiter instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n rateLimiter: AsyncRateLimiter<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 * Time window in milliseconds within which the limit applies.\n * Can be a number or a function that returns a number.\n */\n window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess'\n> = {\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n throwOnError: true,\n}\n\n/**\n * A class that creates an async rate-limited function.\n *\n * Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited 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 rate-limited function.\n *\n * For smoother execution patterns, consider using:\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(\n * async (id: string) => await api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (limiter) => {\n * console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);\n * }\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * // Returns the API response directly\n * const data = await rateLimiter.maybeExecute('123');\n * ```\n */\nexport class AsyncRateLimiter<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<\n AsyncRateLimiterState<TFn>\n >(getDefaultAsyncRateLimiterState<TFn>())\n options: AsyncRateLimiterOptions<TFn>\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n }\n\n /**\n * Updates the async rate limiter options\n */\n setOptions = (newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRateLimiterState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : combinedState.isExecuting\n ? 'executing'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n }\n\n /**\n * Returns the current enabled state of the async rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * Error Handling:\n * - If the rate-limited 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 rate-limited function if no onError handler is configured\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return a promise that resolves with the result\n * const result = await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will return undefined\n * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(this)\n return undefined\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return\n\n const now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n const result = await this.fn(...args) // EXECUTE!\n this.#setCleanupTimeout(now)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(this)\n }\n\n return this.store.state.lastResult\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: NodeJS.Timeout): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n * For fixed windows, this is the time until the current window resets\n * For sliding windows, this is the time until the oldest execution expires\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\n this.#clearTimeouts()\n }\n}\n\n/**\n * Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited 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 rate-limited function.\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = asyncRateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * // Returns the API response directly\n * const result = await rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function asyncRateLimit<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n) {\n const rateLimiter = new AsyncRateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -177,7 +177,7 @@ export declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
177
177
|
/**
|
|
178
178
|
* Processes the current pending execution immediately
|
|
179
179
|
*/
|
|
180
|
-
flush: () =>
|
|
180
|
+
flush: () => Promise<ReturnType<TFn> | undefined>;
|
|
181
181
|
/**
|
|
182
182
|
* Cancels any pending execution or aborts any execution in progress
|
|
183
183
|
*/
|
|
@@ -27,6 +27,7 @@ class AsyncThrottler {
|
|
|
27
27
|
this.#abortController = null;
|
|
28
28
|
this.#timeoutId = null;
|
|
29
29
|
this.#resolvePreviousPromise = null;
|
|
30
|
+
this.#rejectPreviousPromise = null;
|
|
30
31
|
this.setOptions = (newOptions) => {
|
|
31
32
|
this.options = { ...this.options, ...newOptions };
|
|
32
33
|
if (!this.#getEnabled()) {
|
|
@@ -63,8 +64,9 @@ class AsyncThrottler {
|
|
|
63
64
|
await this.#execute(...args);
|
|
64
65
|
return this.store.state.lastResult;
|
|
65
66
|
} else {
|
|
66
|
-
return new Promise((resolve) => {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
67
68
|
this.#resolvePreviousPromise = resolve;
|
|
69
|
+
this.#rejectPreviousPromise = reject;
|
|
68
70
|
this.#clearTimeout();
|
|
69
71
|
if (this.options.trailing) {
|
|
70
72
|
const _timeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0;
|
|
@@ -98,9 +100,7 @@ class AsyncThrottler {
|
|
|
98
100
|
});
|
|
99
101
|
this.options.onError?.(error, this);
|
|
100
102
|
if (this.options.throwOnError) {
|
|
101
|
-
|
|
102
|
-
} else {
|
|
103
|
-
console.error(error);
|
|
103
|
+
this.#rejectPreviousPromiseInternal(error);
|
|
104
104
|
}
|
|
105
105
|
} finally {
|
|
106
106
|
const lastExecutionTime = Date.now();
|
|
@@ -117,12 +117,15 @@ class AsyncThrottler {
|
|
|
117
117
|
}
|
|
118
118
|
return this.store.state.lastResult;
|
|
119
119
|
};
|
|
120
|
-
this.flush = () => {
|
|
120
|
+
this.flush = async () => {
|
|
121
121
|
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
122
122
|
this.#abortExecution();
|
|
123
123
|
this.#clearTimeout();
|
|
124
|
-
this.#execute(...this.store.state.lastArgs);
|
|
124
|
+
const result = await this.#execute(...this.store.state.lastArgs);
|
|
125
|
+
this.#resolvePreviousPromiseInternal();
|
|
126
|
+
return result;
|
|
125
127
|
}
|
|
128
|
+
return void 0;
|
|
126
129
|
};
|
|
127
130
|
this.#resolvePreviousPromiseInternal = () => {
|
|
128
131
|
if (this.#resolvePreviousPromise) {
|
|
@@ -130,6 +133,12 @@ class AsyncThrottler {
|
|
|
130
133
|
this.#resolvePreviousPromise = null;
|
|
131
134
|
}
|
|
132
135
|
};
|
|
136
|
+
this.#rejectPreviousPromiseInternal = (error) => {
|
|
137
|
+
if (this.#rejectPreviousPromise) {
|
|
138
|
+
this.#rejectPreviousPromise(error);
|
|
139
|
+
this.#rejectPreviousPromise = null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
133
142
|
this.#clearTimeout = () => {
|
|
134
143
|
if (this.#timeoutId) {
|
|
135
144
|
clearTimeout(this.#timeoutId);
|
|
@@ -171,11 +180,13 @@ class AsyncThrottler {
|
|
|
171
180
|
#abortController;
|
|
172
181
|
#timeoutId;
|
|
173
182
|
#resolvePreviousPromise;
|
|
183
|
+
#rejectPreviousPromise;
|
|
174
184
|
#setState;
|
|
175
185
|
#getEnabled;
|
|
176
186
|
#getWait;
|
|
177
187
|
#execute;
|
|
178
188
|
#resolvePreviousPromiseInternal;
|
|
189
|
+
#rejectPreviousPromiseInternal;
|
|
179
190
|
#clearTimeout;
|
|
180
191
|
#cancelPendingExecution;
|
|
181
192
|
#abortExecution;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-throttler.js","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the throttled function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return structuredClone({\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n nextExecutionTime: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n })\n}\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 * Initial state for the async throttler\n */\n initialState?: Partial<AsyncThrottlerState<TFn>>\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 'initialState' | '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 * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via `asyncThrottler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`\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 readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<\n AsyncThrottlerState<TFn>\n >(getDefaultAsyncThrottlerState<TFn>())\n options: AsyncThrottlerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #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 this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the async throttler options\n */\n setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // End the pending state if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n }\n\n /**\n * Returns the current enabled state of the async 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. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new AsyncThrottler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * await throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * await throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n // Store the most recent arguments for potential trailing execution\n this.#setState({ lastArgs: args })\n\n this.#resolvePreviousPromiseInternal()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n await this.#execute(...args)\n return this.store.state.lastResult\n } else {\n return new Promise((resolve) => {\n this.#resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n this.#clearTimeout()\n\n // Set up trailing execution if enabled\n if (this.options.trailing) {\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(async () => {\n if (this.store.state.lastArgs !== undefined) {\n await this.#execute(...this.store.state.lastArgs)\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled() || this.store.state.isExecuting) return undefined\n this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\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 * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via the underlying AsyncThrottler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\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\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"async-throttler.js","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the throttled function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return structuredClone({\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n nextExecutionTime: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n })\n}\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 * Initial state for the async throttler\n */\n initialState?: Partial<AsyncThrottlerState<TFn>>\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 'initialState' | '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 * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via `asyncThrottler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`\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 readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<\n AsyncThrottlerState<TFn>\n >(getDefaultAsyncThrottlerState<TFn>())\n options: AsyncThrottlerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n #rejectPreviousPromise: ((reason?: unknown) => void) | 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 this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the async throttler options\n */\n setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // End the pending state if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n }\n\n /**\n * Returns the current enabled state of the async 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. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new AsyncThrottler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * await throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * await throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n // Store the most recent arguments for potential trailing execution\n this.#setState({ lastArgs: args })\n\n this.#resolvePreviousPromiseInternal()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n await this.#execute(...args)\n return this.store.state.lastResult\n } else {\n return new Promise((resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n this.#rejectPreviousPromise = reject\n // Clear any existing timeout to ensure we use the latest arguments\n this.#clearTimeout()\n\n // Set up trailing execution if enabled\n if (this.options.trailing) {\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(async () => {\n if (this.store.state.lastArgs !== undefined) {\n await this.#execute(...this.store.state.lastArgs)\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled() || this.store.state.isExecuting) return undefined\n this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n this.#rejectPreviousPromiseInternal(error)\n }\n } finally {\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = async (): Promise<ReturnType<TFn> | undefined> => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n const result = await this.#execute(...this.store.state.lastArgs)\n\n // Resolve any pending promise from maybeExecute\n this.#resolvePreviousPromiseInternal()\n\n return result\n }\n return undefined\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #rejectPreviousPromiseInternal = (error: unknown): void => {\n if (this.#rejectPreviousPromise) {\n this.#rejectPreviousPromise(error)\n this.#rejectPreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\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 * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via the underlying AsyncThrottler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\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\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/batcher.d.ts
CHANGED
|
@@ -13,14 +13,6 @@ export interface BatcherState<TValue> {
|
|
|
13
13
|
* Whether the batcher is waiting for the timeout to trigger batch processing
|
|
14
14
|
*/
|
|
15
15
|
isPending: boolean;
|
|
16
|
-
/**
|
|
17
|
-
* Whether the batcher is active and will process items automatically
|
|
18
|
-
*/
|
|
19
|
-
isRunning: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Total number of items that have been processed across all batches
|
|
22
|
-
*/
|
|
23
|
-
totalItemsProcessed: number;
|
|
24
16
|
/**
|
|
25
17
|
* Array of items currently queued for batch processing
|
|
26
18
|
*/
|
|
@@ -33,6 +25,10 @@ export interface BatcherState<TValue> {
|
|
|
33
25
|
* Current processing status - 'idle' when not processing, 'pending' when waiting for timeout
|
|
34
26
|
*/
|
|
35
27
|
status: 'idle' | 'pending';
|
|
28
|
+
/**
|
|
29
|
+
* Total number of items that have been processed across all batches
|
|
30
|
+
*/
|
|
31
|
+
totalItemsProcessed: number;
|
|
36
32
|
}
|
|
37
33
|
/**
|
|
38
34
|
* Options for configuring a Batcher instance
|
|
@@ -131,14 +127,6 @@ export declare class Batcher<TValue> {
|
|
|
131
127
|
* Processes the current batch of items immediately
|
|
132
128
|
*/
|
|
133
129
|
flush: () => void;
|
|
134
|
-
/**
|
|
135
|
-
* Stops the batcher from processing batches
|
|
136
|
-
*/
|
|
137
|
-
stop: () => void;
|
|
138
|
-
/**
|
|
139
|
-
* Starts the batcher and processes any pending items
|
|
140
|
-
*/
|
|
141
|
-
start: () => void;
|
|
142
130
|
/**
|
|
143
131
|
* Returns a copy of all items in the batcher
|
|
144
132
|
*/
|
package/dist/esm/batcher.js
CHANGED
|
@@ -5,7 +5,6 @@ function getDefaultBatcherState() {
|
|
|
5
5
|
executionCount: 0,
|
|
6
6
|
isEmpty: true,
|
|
7
7
|
isPending: false,
|
|
8
|
-
isRunning: true,
|
|
9
8
|
totalItemsProcessed: 0,
|
|
10
9
|
items: [],
|
|
11
10
|
size: 0,
|
|
@@ -57,7 +56,7 @@ class Batcher {
|
|
|
57
56
|
const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this);
|
|
58
57
|
if (shouldProcess) {
|
|
59
58
|
this.#execute();
|
|
60
|
-
} else if (this.
|
|
59
|
+
} else if (this.options.wait !== Infinity) {
|
|
61
60
|
this.#clearTimeout();
|
|
62
61
|
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait());
|
|
63
62
|
}
|
|
@@ -80,16 +79,6 @@ class Batcher {
|
|
|
80
79
|
this.#clearTimeout();
|
|
81
80
|
this.#execute();
|
|
82
81
|
};
|
|
83
|
-
this.stop = () => {
|
|
84
|
-
this.#setState({ isRunning: false });
|
|
85
|
-
this.#clearTimeout();
|
|
86
|
-
};
|
|
87
|
-
this.start = () => {
|
|
88
|
-
this.#setState({ isRunning: true });
|
|
89
|
-
if (this.store.state.items.length > 0) {
|
|
90
|
-
this.#execute();
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
82
|
this.peekAllItems = () => {
|
|
94
83
|
return [...this.store.state.items];
|
|
95
84
|
};
|
package/dist/esm/batcher.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batcher.js","sources":["../../src/batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\n\nexport interface BatcherState<TValue> {\n /**\n * Number of batch executions that have been completed\n */\n executionCount: number\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Whether the batcher is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout\n */\n status: 'idle' | 'pending'\n}\n\nfunction getDefaultBatcherState<TValue>(): BatcherState<TValue> {\n return {\n executionCount: 0,\n isEmpty: true,\n isPending: false,\n isRunning: true,\n totalItemsProcessed: 0,\n items: [],\n size: 0,\n status: 'idle',\n }\n}\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 * Initial state for the batcher\n */\n initialState?: Partial<BatcherState<TValue>>\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 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 | ((batcher: Batcher<TValue>) => number)\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'initialState' | 'onExecute' | 'onItemsChange'\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 * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the batcher\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes batch execution count, total items processed, items, and running status\n * - State can be accessed via `batcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `batcher.state`\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 * onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems())\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.flush() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(\n getDefaultBatcherState<TValue>(),\n )\n options: BatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\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 #setState = (newState: Partial<BatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isPending ? 'pending' : 'idle',\n }\n })\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\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.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n this.#execute()\n } else if (this.store.state.isRunning && this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\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.store.state.items.length === 0) {\n return\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // 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) // EXECUTE\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,\n })\n this.options.onExecute?.(this)\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = (): void => {\n this.#clearTimeout() // clear any pending timeout\n this.#execute() // execute immediately\n }\n\n /**\n * Stops the batcher from processing batches\n */\n stop = (): void => {\n this.#setState({ isRunning: false })\n this.#clearTimeout()\n }\n\n /**\n * Starts the batcher and processes any pending items\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (this.store.state.items.length > 0) {\n this.#execute()\n }\n }\n\n /**\n * Returns a copy of all items in the batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the batcher\n */\n clear = (): void => {\n this.#setState({ items: [], isPending: false })\n }\n\n /**\n * Resets the batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a batcher that processes items in batches\n *\n * @example\n * ```ts\n * const batchItems = batch<number>(\n * (items) => console.log('Processing:', items),\n * {\n * maxSize: 3,\n * onExecute: (batcher) => console.log('Batch executed')\n * }\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\n}\n"],"names":["batch"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"batcher.js","sources":["../../src/batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\n\nexport interface BatcherState<TValue> {\n /**\n * Number of batch executions that have been completed\n */\n executionCount: number\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout\n */\n status: 'idle' | 'pending'\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultBatcherState<TValue>(): BatcherState<TValue> {\n return {\n executionCount: 0,\n isEmpty: true,\n isPending: false,\n totalItemsProcessed: 0,\n items: [],\n size: 0,\n status: 'idle',\n }\n}\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 * Initial state for the batcher\n */\n initialState?: Partial<BatcherState<TValue>>\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 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 | ((batcher: Batcher<TValue>) => number)\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'initialState' | 'onExecute' | 'onItemsChange'\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 * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the batcher\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes batch execution count, total items processed, items, and running status\n * - State can be accessed via `batcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `batcher.state`\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 * onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems())\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.flush() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(\n getDefaultBatcherState<TValue>(),\n )\n options: BatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\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 #setState = (newState: Partial<BatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isPending ? 'pending' : 'idle',\n }\n })\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\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.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\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.store.state.items.length === 0) {\n return\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // 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) // EXECUTE\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,\n })\n this.options.onExecute?.(this)\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = (): void => {\n this.#clearTimeout() // clear any pending timeout\n this.#execute() // execute immediately\n }\n\n /**\n * Returns a copy of all items in the batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the batcher\n */\n clear = (): void => {\n this.#setState({ items: [], isPending: false })\n }\n\n /**\n * Resets the batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a batcher that processes items in batches\n *\n * @example\n * ```ts\n * const batchItems = batch<number>(\n * (items) => console.log('Processing:', items),\n * {\n * maxSize: 3,\n * onExecute: (batcher) => console.log('Batch executed')\n * }\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\n}\n"],"names":["batch"],"mappings":"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"}
|
package/dist/esm/queuer.d.ts
CHANGED
|
@@ -24,14 +24,14 @@ export interface QueuerState<TValue> {
|
|
|
24
24
|
* Whether the queuer is active and will process items automatically
|
|
25
25
|
*/
|
|
26
26
|
isRunning: boolean;
|
|
27
|
-
/**
|
|
28
|
-
* Timestamps when items were added to the queue for expiration tracking
|
|
29
|
-
*/
|
|
30
|
-
itemTimestamps: Array<number>;
|
|
31
27
|
/**
|
|
32
28
|
* Array of items currently waiting to be processed
|
|
33
29
|
*/
|
|
34
30
|
items: Array<TValue>;
|
|
31
|
+
/**
|
|
32
|
+
* Timestamps when items were added to the queue for expiration tracking
|
|
33
|
+
*/
|
|
34
|
+
itemTimestamps: Array<number>;
|
|
35
35
|
/**
|
|
36
36
|
* Whether the queuer has a pending timeout for processing the next item
|
|
37
37
|
*/
|
|
@@ -92,14 +92,14 @@ export interface QueuerOptions<TValue> {
|
|
|
92
92
|
* Maximum number of items allowed in the queuer
|
|
93
93
|
*/
|
|
94
94
|
maxSize?: number;
|
|
95
|
-
/**
|
|
96
|
-
* Callback fired whenever an item expires in the queuer
|
|
97
|
-
*/
|
|
98
|
-
onExpire?: (item: TValue, queuer: Queuer<TValue>) => void;
|
|
99
95
|
/**
|
|
100
96
|
* Callback fired whenever an item is removed from the queuer
|
|
101
97
|
*/
|
|
102
98
|
onExecute?: (item: TValue, queuer: Queuer<TValue>) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Callback fired whenever an item expires in the queuer
|
|
101
|
+
*/
|
|
102
|
+
onExpire?: (item: TValue, queuer: Queuer<TValue>) => void;
|
|
103
103
|
/**
|
|
104
104
|
* Callback fired whenever an item is added or removed from the queuer
|
|
105
105
|
*/
|
|
@@ -250,6 +250,11 @@ export declare class Queuer<TValue> {
|
|
|
250
250
|
* If no numberOfItems is provided, all items will be processed
|
|
251
251
|
*/
|
|
252
252
|
flush: (numberOfItems?: number, position?: QueuePosition) => void;
|
|
253
|
+
/**
|
|
254
|
+
* Processes all items in the queue as a batch using the provided function
|
|
255
|
+
* The queue is cleared after processing
|
|
256
|
+
*/
|
|
257
|
+
flushAsBatch: (batchFunction: (items: Array<TValue>) => void) => void;
|
|
253
258
|
/**
|
|
254
259
|
* Returns the next item in the queue without removing it.
|
|
255
260
|
*
|