@tanstack/pacer 0.21.1 → 0.23.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/README.md +21 -6
- package/dist/async-batcher.d.ts +11 -14
- package/dist/async-batcher.js +186 -132
- package/dist/async-debouncer.d.ts +10 -13
- package/dist/async-debouncer.js +187 -146
- package/dist/async-queuer.d.ts +12 -14
- package/dist/async-queuer.js +384 -259
- package/dist/async-rate-limiter.d.ts +9 -12
- package/dist/async-rate-limiter.js +198 -147
- package/dist/async-retryer.d.ts +10 -10
- package/dist/async-retryer.js +209 -188
- package/dist/async-throttler.d.ts +10 -13
- package/dist/async-throttler.js +215 -161
- package/dist/batcher.d.ts +5 -8
- package/dist/batcher.js +107 -87
- package/dist/debouncer.d.ts +6 -9
- package/dist/debouncer.js +99 -86
- package/dist/event-client.d.ts +8 -11
- package/dist/event-client.js +1 -2
- package/dist/index.js +1 -1
- package/dist/queuer.d.ts +8 -10
- package/dist/queuer.js +277 -207
- package/dist/rate-limiter.d.ts +6 -9
- package/dist/rate-limiter.js +127 -109
- package/dist/throttler.d.ts +6 -9
- package/dist/throttler.js +127 -90
- package/dist/types.d.ts +4 -6
- package/dist/utils.d.ts +3 -6
- package/dist/utils.js +1 -2
- package/package.json +23 -70
- package/dist/async-batcher.cjs +0 -337
- package/dist/async-batcher.cjs.map +0 -1
- package/dist/async-batcher.d.cts +0 -344
- package/dist/async-batcher.js.map +0 -1
- package/dist/async-debouncer.cjs +0 -330
- package/dist/async-debouncer.cjs.map +0 -1
- package/dist/async-debouncer.d.cts +0 -300
- package/dist/async-debouncer.js.map +0 -1
- package/dist/async-queuer.cjs +0 -484
- package/dist/async-queuer.cjs.map +0 -1
- package/dist/async-queuer.d.cts +0 -440
- package/dist/async-queuer.js.map +0 -1
- package/dist/async-rate-limiter.cjs +0 -373
- package/dist/async-rate-limiter.cjs.map +0 -1
- package/dist/async-rate-limiter.d.cts +0 -357
- package/dist/async-rate-limiter.js.map +0 -1
- package/dist/async-retryer.cjs +0 -374
- package/dist/async-retryer.cjs.map +0 -1
- package/dist/async-retryer.d.cts +0 -319
- package/dist/async-retryer.js.map +0 -1
- package/dist/async-throttler.cjs +0 -347
- package/dist/async-throttler.cjs.map +0 -1
- package/dist/async-throttler.d.cts +0 -320
- package/dist/async-throttler.js.map +0 -1
- package/dist/batcher.cjs +0 -200
- package/dist/batcher.cjs.map +0 -1
- package/dist/batcher.d.cts +0 -180
- package/dist/batcher.js.map +0 -1
- package/dist/debouncer.cjs +0 -203
- package/dist/debouncer.cjs.map +0 -1
- package/dist/debouncer.d.cts +0 -167
- package/dist/debouncer.js.map +0 -1
- package/dist/event-client.cjs +0 -64
- package/dist/event-client.cjs.map +0 -1
- package/dist/event-client.d.cts +0 -66
- package/dist/event-client.js.map +0 -1
- package/dist/index.cjs +0 -52
- package/dist/index.d.cts +0 -15
- package/dist/queuer.cjs +0 -401
- package/dist/queuer.cjs.map +0 -1
- package/dist/queuer.d.cts +0 -345
- package/dist/queuer.js.map +0 -1
- package/dist/rate-limiter.cjs +0 -263
- package/dist/rate-limiter.cjs.map +0 -1
- package/dist/rate-limiter.d.cts +0 -215
- package/dist/rate-limiter.js.map +0 -1
- package/dist/throttler.cjs +0 -215
- package/dist/throttler.cjs.map +0 -1
- package/dist/throttler.d.cts +0 -207
- package/dist/throttler.js.map +0 -1
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +0 -13
- package/dist/utils.cjs +0 -14
- package/dist/utils.cjs.map +0 -1
- package/dist/utils.d.cts +0 -8
- package/dist/utils.js.map +0 -1
- package/src/async-batcher.ts +0 -594
- package/src/async-debouncer.ts +0 -565
- package/src/async-queuer.ts +0 -925
- package/src/async-rate-limiter.ts +0 -647
- package/src/async-retryer.ts +0 -684
- package/src/async-throttler.ts +0 -633
- package/src/batcher.ts +0 -329
- package/src/debouncer.ts +0 -334
- package/src/event-client.ts +0 -129
- package/src/index.ts +0 -24
- package/src/queuer.ts +0 -740
- package/src/rate-limiter.ts +0 -429
- package/src/throttler.ts +0 -380
- package/src/types.ts +0 -12
- package/src/utils.ts +0 -12
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { AnyAsyncFunction } from "./types.js";
|
|
2
2
|
import { AsyncRetryer, AsyncRetryerOptions } from "./async-retryer.js";
|
|
3
3
|
import { Store } from "@tanstack/store";
|
|
4
|
-
|
|
5
4
|
//#region src/async-rate-limiter.d.ts
|
|
6
|
-
interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
5
|
+
export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
7
6
|
/**
|
|
8
7
|
* Number of function executions that have resulted in errors
|
|
9
8
|
*/
|
|
@@ -23,7 +22,7 @@ interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
|
23
22
|
/**
|
|
24
23
|
* The result from the most recent successful function execution
|
|
25
24
|
*/
|
|
26
|
-
lastResult: ReturnType<TFn
|
|
25
|
+
lastResult: Awaited<ReturnType<TFn>> | undefined;
|
|
27
26
|
/**
|
|
28
27
|
* Number of function executions that have been rejected due to rate limiting
|
|
29
28
|
*/
|
|
@@ -48,7 +47,7 @@ interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
|
48
47
|
/**
|
|
49
48
|
* Options for configuring an async rate-limited function
|
|
50
49
|
*/
|
|
51
|
-
interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
50
|
+
export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
52
51
|
/**
|
|
53
52
|
* Options for configuring the underlying async retryer
|
|
54
53
|
*/
|
|
@@ -90,7 +89,7 @@ interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
90
89
|
/**
|
|
91
90
|
* Optional function to call when the rate-limited function is executed
|
|
92
91
|
*/
|
|
93
|
-
onSuccess?: (result: ReturnType<TFn
|
|
92
|
+
onSuccess?: (result: Awaited<ReturnType<TFn>>, args: Parameters<TFn>, rateLimiter: AsyncRateLimiter<TFn>) => void;
|
|
94
93
|
/**
|
|
95
94
|
* Whether to throw errors when they occur.
|
|
96
95
|
* Defaults to true if no onError handler is provided, false if an onError handler is provided.
|
|
@@ -113,7 +112,7 @@ interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
113
112
|
/**
|
|
114
113
|
* Utility function for sharing common `AsyncRateLimiterOptions` options between different `AsyncRateLimiter` instances.
|
|
115
114
|
*/
|
|
116
|
-
declare function asyncRateLimiterOptions<TFn extends AnyAsyncFunction = AnyAsyncFunction, TOptions extends Partial<AsyncRateLimiterOptions<TFn>> = Partial<AsyncRateLimiterOptions<TFn>>>(options: TOptions): TOptions;
|
|
115
|
+
export declare function asyncRateLimiterOptions<TFn extends AnyAsyncFunction = AnyAsyncFunction, TOptions extends Partial<AsyncRateLimiterOptions<TFn>> = Partial<AsyncRateLimiterOptions<TFn>>>(options: TOptions): TOptions;
|
|
117
116
|
/**
|
|
118
117
|
* A class that creates an async rate-limited function.
|
|
119
118
|
*
|
|
@@ -188,7 +187,7 @@ declare function asyncRateLimiterOptions<TFn extends AnyAsyncFunction = AnyAsync
|
|
|
188
187
|
* const data = await rateLimiter.maybeExecute('123');
|
|
189
188
|
* ```
|
|
190
189
|
*/
|
|
191
|
-
declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
190
|
+
export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
192
191
|
#private;
|
|
193
192
|
fn: TFn;
|
|
194
193
|
readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>>;
|
|
@@ -225,7 +224,7 @@ declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
225
224
|
* const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined
|
|
226
225
|
* ```
|
|
227
226
|
*/
|
|
228
|
-
maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn
|
|
227
|
+
maybeExecute: (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
|
|
229
228
|
/**
|
|
230
229
|
* Returns the number of remaining executions allowed in the current window
|
|
231
230
|
*/
|
|
@@ -351,7 +350,5 @@ declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
351
350
|
* const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds
|
|
352
351
|
* ```
|
|
353
352
|
*/
|
|
354
|
-
declare function asyncRateLimit<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions: AsyncRateLimiterOptions<TFn>): (...args: Parameters<TFn>) => Promise<ReturnType<TFn
|
|
355
|
-
//#endregion
|
|
356
|
-
export { AsyncRateLimiter, AsyncRateLimiterOptions, AsyncRateLimiterState, asyncRateLimit, asyncRateLimiterOptions };
|
|
357
|
-
//# sourceMappingURL=async-rate-limiter.d.ts.map
|
|
353
|
+
export declare function asyncRateLimit<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions: AsyncRateLimiterOptions<TFn>): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
|
|
354
|
+
//#endregion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseFunctionOrValue } from "./utils.js";
|
|
2
|
-
import { emitChange, pacerEventClient } from "./event-client.js";
|
|
3
2
|
import { AsyncRetryer } from "./async-retryer.js";
|
|
3
|
+
import { emitChange, pacerEventClient } from "./event-client.js";
|
|
4
4
|
import { Store } from "@tanstack/store";
|
|
5
5
|
|
|
6
6
|
//#region src/async-rate-limiter.ts
|
|
@@ -107,142 +107,14 @@ const defaultOptions = {
|
|
|
107
107
|
* ```
|
|
108
108
|
*/
|
|
109
109
|
var AsyncRateLimiter = class {
|
|
110
|
-
|
|
110
|
+
fn;
|
|
111
|
+
store = new Store(getDefaultAsyncRateLimiterState());
|
|
112
|
+
key;
|
|
113
|
+
options;
|
|
114
|
+
asyncRetryers = /* @__PURE__ */ new Map();
|
|
115
|
+
#timeoutIds = /* @__PURE__ */ new Set();
|
|
111
116
|
constructor(fn, initialOptions) {
|
|
112
117
|
this.fn = fn;
|
|
113
|
-
this.store = new Store(getDefaultAsyncRateLimiterState());
|
|
114
|
-
this.asyncRetryers = /* @__PURE__ */ new Map();
|
|
115
|
-
this.#timeoutIds = /* @__PURE__ */ new Set();
|
|
116
|
-
this.setOptions = (newOptions) => {
|
|
117
|
-
this.options = {
|
|
118
|
-
...this.options,
|
|
119
|
-
...newOptions
|
|
120
|
-
};
|
|
121
|
-
};
|
|
122
|
-
this.#setState = (newState) => {
|
|
123
|
-
this.store.setState((state) => {
|
|
124
|
-
const combinedState = {
|
|
125
|
-
...state,
|
|
126
|
-
...newState
|
|
127
|
-
};
|
|
128
|
-
const isExceeded = combinedState.executionTimes.length >= this.#getLimit();
|
|
129
|
-
const status = !this.#getEnabled() ? "disabled" : combinedState.isExecuting ? "executing" : isExceeded ? "exceeded" : "idle";
|
|
130
|
-
return {
|
|
131
|
-
...combinedState,
|
|
132
|
-
isExceeded,
|
|
133
|
-
status
|
|
134
|
-
};
|
|
135
|
-
});
|
|
136
|
-
emitChange("AsyncRateLimiter", this);
|
|
137
|
-
};
|
|
138
|
-
this.#getEnabled = () => {
|
|
139
|
-
return !!parseFunctionOrValue(this.options.enabled, this);
|
|
140
|
-
};
|
|
141
|
-
this.#getLimit = () => {
|
|
142
|
-
return parseFunctionOrValue(this.options.limit, this);
|
|
143
|
-
};
|
|
144
|
-
this.#getWindow = () => {
|
|
145
|
-
return parseFunctionOrValue(this.options.window, this);
|
|
146
|
-
};
|
|
147
|
-
this.maybeExecute = async (...args) => {
|
|
148
|
-
this.#setState({ maybeExecuteCount: this.store.state.maybeExecuteCount + 1 });
|
|
149
|
-
this.#cleanupOldExecutions();
|
|
150
|
-
if (this.#getExecutionTimesInWindow().length < this.#getLimit()) {
|
|
151
|
-
await this.#execute(...args);
|
|
152
|
-
return this.store.state.lastResult;
|
|
153
|
-
}
|
|
154
|
-
this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
|
|
155
|
-
this.options.onReject?.(args, this);
|
|
156
|
-
};
|
|
157
|
-
this.#execute = async (...args) => {
|
|
158
|
-
if (!this.#getEnabled()) return;
|
|
159
|
-
const currentMaybeExecute = this.store.state.maybeExecuteCount;
|
|
160
|
-
const now = Date.now();
|
|
161
|
-
const executionTimes = [...this.store.state.executionTimes, now];
|
|
162
|
-
this.#setState({
|
|
163
|
-
isExecuting: true,
|
|
164
|
-
executionTimes
|
|
165
|
-
});
|
|
166
|
-
try {
|
|
167
|
-
const currentAsyncRetryer = new AsyncRetryer(this.fn, {
|
|
168
|
-
...this.options.asyncRetryerOptions,
|
|
169
|
-
key: `${this.key}-retryer-${currentMaybeExecute}`
|
|
170
|
-
});
|
|
171
|
-
this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer);
|
|
172
|
-
const result = await currentAsyncRetryer.execute(...args);
|
|
173
|
-
this.#setCleanupTimeout(now);
|
|
174
|
-
this.#setState({
|
|
175
|
-
successCount: this.store.state.successCount + 1,
|
|
176
|
-
lastResult: result
|
|
177
|
-
});
|
|
178
|
-
this.options.onSuccess?.(result, args, this);
|
|
179
|
-
} catch (error) {
|
|
180
|
-
this.#setState({ errorCount: this.store.state.errorCount + 1 });
|
|
181
|
-
this.options.onError?.(error, args, this);
|
|
182
|
-
if (this.options.throwOnError) throw error;
|
|
183
|
-
} finally {
|
|
184
|
-
this.asyncRetryers.delete(currentMaybeExecute);
|
|
185
|
-
this.#setState({
|
|
186
|
-
isExecuting: false,
|
|
187
|
-
settleCount: this.store.state.settleCount + 1
|
|
188
|
-
});
|
|
189
|
-
this.options.onSettled?.(args, this);
|
|
190
|
-
}
|
|
191
|
-
return this.store.state.lastResult;
|
|
192
|
-
};
|
|
193
|
-
this.#getExecutionTimesInWindow = () => {
|
|
194
|
-
if (this.options.windowType === "sliding") return this.store.state.executionTimes.filter((time) => time > Date.now() - this.#getWindow());
|
|
195
|
-
else {
|
|
196
|
-
if (this.store.state.executionTimes.length === 0) return [];
|
|
197
|
-
const windowStart = Math.min(...this.store.state.executionTimes);
|
|
198
|
-
const windowEnd = windowStart + this.#getWindow();
|
|
199
|
-
if (Date.now() > windowEnd) return [];
|
|
200
|
-
return this.store.state.executionTimes.filter((time) => time >= windowStart && time <= windowEnd);
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
this.#setCleanupTimeout = (executionTime) => {
|
|
204
|
-
if (this.options.windowType === "sliding" || this.#timeoutIds.size === 0) {
|
|
205
|
-
const timeUntilExpiration = executionTime - Date.now() + this.#getWindow() + 1;
|
|
206
|
-
const timeoutId = setTimeout(() => {
|
|
207
|
-
this.#cleanupOldExecutions();
|
|
208
|
-
this.#clearTimeout(timeoutId);
|
|
209
|
-
}, timeUntilExpiration);
|
|
210
|
-
this.#timeoutIds.add(timeoutId);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
this.#clearTimeout = (timeoutId) => {
|
|
214
|
-
clearTimeout(timeoutId);
|
|
215
|
-
this.#timeoutIds.delete(timeoutId);
|
|
216
|
-
};
|
|
217
|
-
this.#clearTimeouts = () => {
|
|
218
|
-
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
|
|
219
|
-
this.#timeoutIds.clear();
|
|
220
|
-
};
|
|
221
|
-
this.#cleanupOldExecutions = () => {
|
|
222
|
-
this.#setState({ executionTimes: this.#getExecutionTimesInWindow() });
|
|
223
|
-
};
|
|
224
|
-
this.getRemainingInWindow = () => {
|
|
225
|
-
const relevantExecutionTimes = this.#getExecutionTimesInWindow();
|
|
226
|
-
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length);
|
|
227
|
-
};
|
|
228
|
-
this.getMsUntilNextWindow = () => {
|
|
229
|
-
if (this.getRemainingInWindow() > 0) return 0;
|
|
230
|
-
return (this.store.state.executionTimes[0] ?? Infinity) + this.#getWindow() - Date.now();
|
|
231
|
-
};
|
|
232
|
-
this.getAbortSignal = (maybeExecuteCount) => {
|
|
233
|
-
const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount;
|
|
234
|
-
return this.asyncRetryers.get(count)?.getAbortSignal() ?? null;
|
|
235
|
-
};
|
|
236
|
-
this.abort = () => {
|
|
237
|
-
this.asyncRetryers.forEach((retryer) => retryer.abort());
|
|
238
|
-
this.asyncRetryers.clear();
|
|
239
|
-
this.#setState({ isExecuting: false });
|
|
240
|
-
};
|
|
241
|
-
this.reset = () => {
|
|
242
|
-
this.#setState(getDefaultAsyncRateLimiterState());
|
|
243
|
-
this.#clearTimeouts();
|
|
244
|
-
this.asyncRetryers.forEach((retryer) => retryer.reset());
|
|
245
|
-
};
|
|
246
118
|
this.key = initialOptions.key;
|
|
247
119
|
this.options = {
|
|
248
120
|
...defaultOptions,
|
|
@@ -257,25 +129,205 @@ var AsyncRateLimiter = class {
|
|
|
257
129
|
this.setOptions(event.payload.options);
|
|
258
130
|
});
|
|
259
131
|
}
|
|
260
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Updates the async rate limiter options
|
|
134
|
+
*/
|
|
135
|
+
setOptions = (newOptions) => {
|
|
136
|
+
this.options = {
|
|
137
|
+
...this.options,
|
|
138
|
+
...newOptions
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
#setState = (newState) => {
|
|
142
|
+
this.store.setState((state) => {
|
|
143
|
+
const combinedState = {
|
|
144
|
+
...state,
|
|
145
|
+
...newState
|
|
146
|
+
};
|
|
147
|
+
const isExceeded = combinedState.executionTimes.length >= this.#getLimit();
|
|
148
|
+
const status = !this.#getEnabled() ? "disabled" : combinedState.isExecuting ? "executing" : isExceeded ? "exceeded" : "idle";
|
|
149
|
+
return {
|
|
150
|
+
...combinedState,
|
|
151
|
+
isExceeded,
|
|
152
|
+
status
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
emitChange("AsyncRateLimiter", this);
|
|
156
|
+
};
|
|
261
157
|
/**
|
|
262
158
|
* Returns the current enabled state of the async rate limiter
|
|
263
159
|
*/
|
|
264
|
-
#getEnabled
|
|
160
|
+
#getEnabled = () => {
|
|
161
|
+
return !!parseFunctionOrValue(this.options.enabled, this);
|
|
162
|
+
};
|
|
265
163
|
/**
|
|
266
164
|
* Returns the current limit of executions allowed within the time window
|
|
267
165
|
*/
|
|
268
|
-
#getLimit
|
|
166
|
+
#getLimit = () => {
|
|
167
|
+
return parseFunctionOrValue(this.options.limit, this);
|
|
168
|
+
};
|
|
269
169
|
/**
|
|
270
170
|
* Returns the current time window in milliseconds
|
|
271
171
|
*/
|
|
272
|
-
#getWindow
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
172
|
+
#getWindow = () => {
|
|
173
|
+
return parseFunctionOrValue(this.options.window, this);
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Attempts to execute the rate-limited function if within the configured limits.
|
|
177
|
+
* Will reject execution if the number of calls in the current window exceeds the limit.
|
|
178
|
+
*
|
|
179
|
+
* Error Handling:
|
|
180
|
+
* - If the rate-limited function throws and no `onError` handler is configured,
|
|
181
|
+
* the error will be thrown from this method.
|
|
182
|
+
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
|
|
183
|
+
* and this method will return undefined.
|
|
184
|
+
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
|
|
185
|
+
*
|
|
186
|
+
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
|
|
187
|
+
* @throws The error from the rate-limited function if no onError handler is configured
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });
|
|
192
|
+
*
|
|
193
|
+
* // First 5 calls will return a promise that resolves with the result
|
|
194
|
+
* const result = await rateLimiter.maybeExecute('arg1', 'arg2');
|
|
195
|
+
*
|
|
196
|
+
* // Additional calls within the window will return undefined
|
|
197
|
+
* const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
maybeExecute = async (...args) => {
|
|
201
|
+
this.#setState({ maybeExecuteCount: this.store.state.maybeExecuteCount + 1 });
|
|
202
|
+
this.#cleanupOldExecutions();
|
|
203
|
+
if (this.#getExecutionTimesInWindow().length < this.#getLimit()) {
|
|
204
|
+
await this.#execute(...args);
|
|
205
|
+
return this.store.state.lastResult;
|
|
206
|
+
}
|
|
207
|
+
this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
|
|
208
|
+
this.options.onReject?.(args, this);
|
|
209
|
+
};
|
|
210
|
+
#execute = async (...args) => {
|
|
211
|
+
if (!this.#getEnabled()) return;
|
|
212
|
+
const currentMaybeExecute = this.store.state.maybeExecuteCount;
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const executionTimes = [...this.store.state.executionTimes, now];
|
|
215
|
+
this.#setState({
|
|
216
|
+
isExecuting: true,
|
|
217
|
+
executionTimes
|
|
218
|
+
});
|
|
219
|
+
try {
|
|
220
|
+
const currentAsyncRetryer = new AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
|
|
221
|
+
this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer);
|
|
222
|
+
const result = await currentAsyncRetryer.execute(...args);
|
|
223
|
+
this.#setCleanupTimeout(now);
|
|
224
|
+
this.#setState({
|
|
225
|
+
successCount: this.store.state.successCount + 1,
|
|
226
|
+
lastResult: result
|
|
227
|
+
});
|
|
228
|
+
this.options.onSuccess?.(result, args, this);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
this.#setState({ errorCount: this.store.state.errorCount + 1 });
|
|
231
|
+
this.options.onError?.(error, args, this);
|
|
232
|
+
if (this.options.throwOnError) throw error;
|
|
233
|
+
} finally {
|
|
234
|
+
this.asyncRetryers.delete(currentMaybeExecute);
|
|
235
|
+
this.#setState({
|
|
236
|
+
isExecuting: this.asyncRetryers.size > 0,
|
|
237
|
+
settleCount: this.store.state.settleCount + 1
|
|
238
|
+
});
|
|
239
|
+
this.options.onSettled?.(args, this);
|
|
240
|
+
}
|
|
241
|
+
return this.store.state.lastResult;
|
|
242
|
+
};
|
|
243
|
+
#getExecutionTimesInWindow = () => {
|
|
244
|
+
if (this.options.windowType === "sliding") return this.store.state.executionTimes.filter((time) => time > Date.now() - this.#getWindow());
|
|
245
|
+
else {
|
|
246
|
+
if (this.store.state.executionTimes.length === 0) return [];
|
|
247
|
+
const windowStart = Math.min(...this.store.state.executionTimes);
|
|
248
|
+
const windowEnd = windowStart + this.#getWindow();
|
|
249
|
+
if (Date.now() > windowEnd) return [];
|
|
250
|
+
return this.store.state.executionTimes.filter((time) => time >= windowStart && time <= windowEnd);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
#setCleanupTimeout = (executionTime) => {
|
|
254
|
+
if (this.options.windowType === "sliding" || this.#timeoutIds.size === 0) {
|
|
255
|
+
const timeUntilExpiration = executionTime - Date.now() + this.#getWindow() + 1;
|
|
256
|
+
const timeoutId = setTimeout(() => {
|
|
257
|
+
this.#cleanupOldExecutions();
|
|
258
|
+
this.#clearTimeout(timeoutId);
|
|
259
|
+
}, timeUntilExpiration);
|
|
260
|
+
this.#timeoutIds.add(timeoutId);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
#clearTimeout = (timeoutId) => {
|
|
264
|
+
clearTimeout(timeoutId);
|
|
265
|
+
this.#timeoutIds.delete(timeoutId);
|
|
266
|
+
};
|
|
267
|
+
#clearTimeouts = () => {
|
|
268
|
+
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
|
|
269
|
+
this.#timeoutIds.clear();
|
|
270
|
+
};
|
|
271
|
+
#cleanupOldExecutions = () => {
|
|
272
|
+
this.#setState({ executionTimes: this.#getExecutionTimesInWindow() });
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Returns the number of remaining executions allowed in the current window
|
|
276
|
+
*/
|
|
277
|
+
getRemainingInWindow = () => {
|
|
278
|
+
const relevantExecutionTimes = this.#getExecutionTimesInWindow();
|
|
279
|
+
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length);
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Returns the number of milliseconds until the next execution will be possible
|
|
283
|
+
* For fixed windows, this is the time until the current window resets
|
|
284
|
+
* For sliding windows, this is the time until the oldest execution expires
|
|
285
|
+
*/
|
|
286
|
+
getMsUntilNextWindow = () => {
|
|
287
|
+
if (this.getRemainingInWindow() > 0) return 0;
|
|
288
|
+
return (this.store.state.executionTimes[0] ?? Infinity) + this.#getWindow() - Date.now();
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Returns the AbortSignal for a specific execution.
|
|
292
|
+
* If no maybeExecuteCount is provided, returns the signal for the most recent execution.
|
|
293
|
+
* Returns null if no execution is found or not currently executing.
|
|
294
|
+
*
|
|
295
|
+
* @param maybeExecuteCount - Optional specific execution to get signal for
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* const rateLimiter = new AsyncRateLimiter(
|
|
299
|
+
* async (userId: string) => {
|
|
300
|
+
* const signal = rateLimiter.getAbortSignal()
|
|
301
|
+
* if (signal) {
|
|
302
|
+
* const response = await fetch(`/api/users/${userId}`, { signal })
|
|
303
|
+
* return response.json()
|
|
304
|
+
* }
|
|
305
|
+
* },
|
|
306
|
+
* { limit: 5, window: 1000 }
|
|
307
|
+
* )
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
getAbortSignal = (maybeExecuteCount) => {
|
|
311
|
+
const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount;
|
|
312
|
+
return this.asyncRetryers.get(count)?.getAbortSignal() ?? null;
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Aborts all ongoing executions with the internal abort controllers.
|
|
316
|
+
* Does NOT clear out the execution times or reset the rate limiter.
|
|
317
|
+
*/
|
|
318
|
+
abort = () => {
|
|
319
|
+
this.asyncRetryers.forEach((retryer) => retryer.abort());
|
|
320
|
+
this.asyncRetryers.clear();
|
|
321
|
+
this.#setState({ isExecuting: false });
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* Resets the rate limiter state
|
|
325
|
+
*/
|
|
326
|
+
reset = () => {
|
|
327
|
+
this.#setState(getDefaultAsyncRateLimiterState());
|
|
328
|
+
this.#clearTimeouts();
|
|
329
|
+
this.asyncRetryers.forEach((retryer) => retryer.reset());
|
|
330
|
+
};
|
|
279
331
|
};
|
|
280
332
|
/**
|
|
281
333
|
* Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.
|
|
@@ -366,5 +418,4 @@ function asyncRateLimit(fn, initialOptions) {
|
|
|
366
418
|
}
|
|
367
419
|
|
|
368
420
|
//#endregion
|
|
369
|
-
export { AsyncRateLimiter, asyncRateLimit, asyncRateLimiterOptions };
|
|
370
|
-
//# sourceMappingURL=async-rate-limiter.js.map
|
|
421
|
+
export { AsyncRateLimiter, asyncRateLimit, asyncRateLimiterOptions };
|
package/dist/async-retryer.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { AnyAsyncFunction } from "./types.js";
|
|
2
2
|
import { Store } from "@tanstack/store";
|
|
3
|
-
|
|
4
3
|
//#region src/async-retryer.d.ts
|
|
5
|
-
interface AsyncRetryerState<TFn extends AnyAsyncFunction> {
|
|
4
|
+
export interface AsyncRetryerState<TFn extends AnyAsyncFunction> {
|
|
6
5
|
/**
|
|
7
6
|
* The current retry attempt number (0 when not executing)
|
|
8
7
|
*/
|
|
@@ -36,7 +35,7 @@ interface AsyncRetryerState<TFn extends AnyAsyncFunction> {
|
|
|
36
35
|
*/
|
|
37
36
|
totalExecutionTime: number;
|
|
38
37
|
}
|
|
39
|
-
interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {
|
|
38
|
+
export interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {
|
|
40
39
|
/**
|
|
41
40
|
* The backoff strategy for retry delays:
|
|
42
41
|
* - 'exponential': Wait time doubles with each attempt (1s, 2s, 4s, ...)
|
|
@@ -66,7 +65,10 @@ interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {
|
|
|
66
65
|
jitter?: number;
|
|
67
66
|
/**
|
|
68
67
|
* Optional key to identify this async retryer instance.
|
|
69
|
-
*
|
|
68
|
+
* Note: async retryers are not currently surfaced in the devtools, so this key
|
|
69
|
+
* is only a plain identifier. Retryer instances are often created per-execution
|
|
70
|
+
* (including internally by the other async utilities), so they intentionally do
|
|
71
|
+
* not register with the devtools event bus.
|
|
70
72
|
*/
|
|
71
73
|
key?: string;
|
|
72
74
|
/**
|
|
@@ -133,7 +135,7 @@ interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {
|
|
|
133
135
|
/**
|
|
134
136
|
* Utility function for sharing common `AsyncRetryerOptions` options between different `AsyncRetryer` instances.
|
|
135
137
|
*/
|
|
136
|
-
declare function asyncRetryerOptions<TFn extends AnyAsyncFunction = AnyAsyncFunction, TOptions extends Partial<AsyncRetryerOptions<TFn>> = Partial<AsyncRetryerOptions<TFn>>>(options: TOptions): TOptions;
|
|
138
|
+
export declare function asyncRetryerOptions<TFn extends AnyAsyncFunction = AnyAsyncFunction, TOptions extends Partial<AsyncRetryerOptions<TFn>> = Partial<AsyncRetryerOptions<TFn>>>(options: TOptions): TOptions;
|
|
137
139
|
declare const defaultOptions: Omit<Required<AsyncRetryerOptions<any>>, 'initialState' | 'key' | 'onAbort' | 'onError' | 'onLastError' | 'onRetry' | 'onSettled' | 'onSuccess' | 'onExecutionTimeout' | 'onTotalExecutionTimeout'>;
|
|
138
140
|
/**
|
|
139
141
|
* Provides robust retry functionality for asynchronous functions, supporting configurable backoff strategies,
|
|
@@ -229,7 +231,7 @@ declare const defaultOptions: Omit<Required<AsyncRetryerOptions<any>>, 'initialS
|
|
|
229
231
|
*
|
|
230
232
|
* @template TFn The async function type to be retried.
|
|
231
233
|
*/
|
|
232
|
-
declare class AsyncRetryer<TFn extends AnyAsyncFunction> {
|
|
234
|
+
export declare class AsyncRetryer<TFn extends AnyAsyncFunction> {
|
|
233
235
|
#private;
|
|
234
236
|
fn: TFn;
|
|
235
237
|
readonly store: Store<Readonly<AsyncRetryerState<TFn>>>;
|
|
@@ -313,7 +315,5 @@ declare class AsyncRetryer<TFn extends AnyAsyncFunction> {
|
|
|
313
315
|
* const data2 = await fetchWithRetry('/api/data2')
|
|
314
316
|
* ```
|
|
315
317
|
*/
|
|
316
|
-
declare function asyncRetry<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions?: AsyncRetryerOptions<TFn>): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
|
|
317
|
-
//#endregion
|
|
318
|
-
export { AsyncRetryer, AsyncRetryerOptions, AsyncRetryerState, asyncRetry, asyncRetryerOptions };
|
|
319
|
-
//# sourceMappingURL=async-retryer.d.ts.map
|
|
318
|
+
export declare function asyncRetry<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions?: AsyncRetryerOptions<TFn>): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
|
|
319
|
+
//#endregion
|