@tanstack/pacer 0.22.0 → 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 +4 -4
- package/dist/async-batcher.d.ts +11 -13
- package/dist/async-batcher.js +185 -131
- package/dist/async-debouncer.d.ts +6 -8
- package/dist/async-debouncer.js +186 -142
- package/dist/async-queuer.d.ts +11 -13
- package/dist/async-queuer.js +383 -290
- package/dist/async-rate-limiter.d.ts +6 -8
- package/dist/async-rate-limiter.js +197 -143
- package/dist/async-retryer.d.ts +6 -8
- package/dist/async-retryer.js +209 -179
- package/dist/async-throttler.d.ts +6 -8
- package/dist/async-throttler.js +214 -157
- package/dist/batcher.d.ts +5 -7
- package/dist/batcher.js +107 -87
- package/dist/debouncer.d.ts +6 -8
- package/dist/debouncer.js +99 -86
- package/dist/event-client.d.ts +8 -10
- package/dist/event-client.js +1 -2
- package/dist/queuer.d.ts +7 -9
- package/dist/queuer.js +277 -212
- package/dist/rate-limiter.d.ts +6 -8
- package/dist/rate-limiter.js +127 -109
- package/dist/throttler.d.ts +6 -8
- package/dist/throttler.js +127 -90
- package/dist/types.d.ts +4 -6
- package/dist/utils.d.ts +3 -5
- package/dist/utils.js +1 -2
- package/package.json +21 -68
- package/dist/async-batcher.cjs +0 -337
- package/dist/async-batcher.cjs.map +0 -1
- package/dist/async-batcher.d.cts +0 -343
- package/dist/async-batcher.js.map +0 -1
- package/dist/async-debouncer.cjs +0 -327
- package/dist/async-debouncer.cjs.map +0 -1
- package/dist/async-debouncer.d.cts +0 -299
- package/dist/async-debouncer.js.map +0 -1
- package/dist/async-queuer.cjs +0 -516
- 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 -370
- package/dist/async-rate-limiter.cjs.map +0 -1
- package/dist/async-rate-limiter.d.cts +0 -356
- package/dist/async-rate-limiter.js.map +0 -1
- package/dist/async-retryer.cjs +0 -365
- package/dist/async-retryer.cjs.map +0 -1
- package/dist/async-retryer.d.cts +0 -321
- package/dist/async-retryer.js.map +0 -1
- package/dist/async-throttler.cjs +0 -344
- package/dist/async-throttler.cjs.map +0 -1
- package/dist/async-throttler.d.cts +0 -319
- 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 -179
- 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 -166
- 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 -65
- 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 -406
- 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 -214
- 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 -206
- 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 -13
- package/dist/utils.cjs.map +0 -1
- package/dist/utils.d.cts +0 -7
- package/dist/utils.js.map +0 -1
- package/src/async-batcher.ts +0 -594
- package/src/async-debouncer.ts +0 -566
- package/src/async-queuer.ts +0 -988
- package/src/async-rate-limiter.ts +0 -648
- package/src/async-retryer.ts +0 -673
- package/src/async-throttler.ts +0 -634
- 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 -751
- 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
|
@@ -2,7 +2,7 @@ import { AnyAsyncFunction } from "./types.js";
|
|
|
2
2
|
import { AsyncRetryer, AsyncRetryerOptions } from "./async-retryer.js";
|
|
3
3
|
import { Store } from "@tanstack/store";
|
|
4
4
|
//#region src/async-rate-limiter.d.ts
|
|
5
|
-
interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
5
|
+
export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
6
6
|
/**
|
|
7
7
|
* Number of function executions that have resulted in errors
|
|
8
8
|
*/
|
|
@@ -47,7 +47,7 @@ interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
|
47
47
|
/**
|
|
48
48
|
* Options for configuring an async rate-limited function
|
|
49
49
|
*/
|
|
50
|
-
interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
50
|
+
export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
51
51
|
/**
|
|
52
52
|
* Options for configuring the underlying async retryer
|
|
53
53
|
*/
|
|
@@ -112,7 +112,7 @@ interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
112
112
|
/**
|
|
113
113
|
* Utility function for sharing common `AsyncRateLimiterOptions` options between different `AsyncRateLimiter` instances.
|
|
114
114
|
*/
|
|
115
|
-
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;
|
|
116
116
|
/**
|
|
117
117
|
* A class that creates an async rate-limited function.
|
|
118
118
|
*
|
|
@@ -187,7 +187,7 @@ declare function asyncRateLimiterOptions<TFn extends AnyAsyncFunction = AnyAsync
|
|
|
187
187
|
* const data = await rateLimiter.maybeExecute('123');
|
|
188
188
|
* ```
|
|
189
189
|
*/
|
|
190
|
-
declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
190
|
+
export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
191
191
|
#private;
|
|
192
192
|
fn: TFn;
|
|
193
193
|
readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>>;
|
|
@@ -350,7 +350,5 @@ declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
350
350
|
* const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds
|
|
351
351
|
* ```
|
|
352
352
|
*/
|
|
353
|
-
declare function asyncRateLimit<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions: AsyncRateLimiterOptions<TFn>): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
|
|
354
|
-
//#endregion
|
|
355
|
-
export { AsyncRateLimiter, AsyncRateLimiterOptions, AsyncRateLimiterState, asyncRateLimit, asyncRateLimiterOptions };
|
|
356
|
-
//# 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
|
|
@@ -107,139 +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, this.options.asyncRetryerOptions);
|
|
168
|
-
this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer);
|
|
169
|
-
const result = await currentAsyncRetryer.execute(...args);
|
|
170
|
-
this.#setCleanupTimeout(now);
|
|
171
|
-
this.#setState({
|
|
172
|
-
successCount: this.store.state.successCount + 1,
|
|
173
|
-
lastResult: result
|
|
174
|
-
});
|
|
175
|
-
this.options.onSuccess?.(result, args, this);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
this.#setState({ errorCount: this.store.state.errorCount + 1 });
|
|
178
|
-
this.options.onError?.(error, args, this);
|
|
179
|
-
if (this.options.throwOnError) throw error;
|
|
180
|
-
} finally {
|
|
181
|
-
this.asyncRetryers.delete(currentMaybeExecute);
|
|
182
|
-
this.#setState({
|
|
183
|
-
isExecuting: this.asyncRetryers.size > 0,
|
|
184
|
-
settleCount: this.store.state.settleCount + 1
|
|
185
|
-
});
|
|
186
|
-
this.options.onSettled?.(args, this);
|
|
187
|
-
}
|
|
188
|
-
return this.store.state.lastResult;
|
|
189
|
-
};
|
|
190
|
-
this.#getExecutionTimesInWindow = () => {
|
|
191
|
-
if (this.options.windowType === "sliding") return this.store.state.executionTimes.filter((time) => time > Date.now() - this.#getWindow());
|
|
192
|
-
else {
|
|
193
|
-
if (this.store.state.executionTimes.length === 0) return [];
|
|
194
|
-
const windowStart = Math.min(...this.store.state.executionTimes);
|
|
195
|
-
const windowEnd = windowStart + this.#getWindow();
|
|
196
|
-
if (Date.now() > windowEnd) return [];
|
|
197
|
-
return this.store.state.executionTimes.filter((time) => time >= windowStart && time <= windowEnd);
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
this.#setCleanupTimeout = (executionTime) => {
|
|
201
|
-
if (this.options.windowType === "sliding" || this.#timeoutIds.size === 0) {
|
|
202
|
-
const timeUntilExpiration = executionTime - Date.now() + this.#getWindow() + 1;
|
|
203
|
-
const timeoutId = setTimeout(() => {
|
|
204
|
-
this.#cleanupOldExecutions();
|
|
205
|
-
this.#clearTimeout(timeoutId);
|
|
206
|
-
}, timeUntilExpiration);
|
|
207
|
-
this.#timeoutIds.add(timeoutId);
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
this.#clearTimeout = (timeoutId) => {
|
|
211
|
-
clearTimeout(timeoutId);
|
|
212
|
-
this.#timeoutIds.delete(timeoutId);
|
|
213
|
-
};
|
|
214
|
-
this.#clearTimeouts = () => {
|
|
215
|
-
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
|
|
216
|
-
this.#timeoutIds.clear();
|
|
217
|
-
};
|
|
218
|
-
this.#cleanupOldExecutions = () => {
|
|
219
|
-
this.#setState({ executionTimes: this.#getExecutionTimesInWindow() });
|
|
220
|
-
};
|
|
221
|
-
this.getRemainingInWindow = () => {
|
|
222
|
-
const relevantExecutionTimes = this.#getExecutionTimesInWindow();
|
|
223
|
-
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length);
|
|
224
|
-
};
|
|
225
|
-
this.getMsUntilNextWindow = () => {
|
|
226
|
-
if (this.getRemainingInWindow() > 0) return 0;
|
|
227
|
-
return (this.store.state.executionTimes[0] ?? Infinity) + this.#getWindow() - Date.now();
|
|
228
|
-
};
|
|
229
|
-
this.getAbortSignal = (maybeExecuteCount) => {
|
|
230
|
-
const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount;
|
|
231
|
-
return this.asyncRetryers.get(count)?.getAbortSignal() ?? null;
|
|
232
|
-
};
|
|
233
|
-
this.abort = () => {
|
|
234
|
-
this.asyncRetryers.forEach((retryer) => retryer.abort());
|
|
235
|
-
this.asyncRetryers.clear();
|
|
236
|
-
this.#setState({ isExecuting: false });
|
|
237
|
-
};
|
|
238
|
-
this.reset = () => {
|
|
239
|
-
this.#setState(getDefaultAsyncRateLimiterState());
|
|
240
|
-
this.#clearTimeouts();
|
|
241
|
-
this.asyncRetryers.forEach((retryer) => retryer.reset());
|
|
242
|
-
};
|
|
243
118
|
this.key = initialOptions.key;
|
|
244
119
|
this.options = {
|
|
245
120
|
...defaultOptions,
|
|
@@ -254,25 +129,205 @@ var AsyncRateLimiter = class {
|
|
|
254
129
|
this.setOptions(event.payload.options);
|
|
255
130
|
});
|
|
256
131
|
}
|
|
257
|
-
|
|
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
|
+
};
|
|
258
157
|
/**
|
|
259
158
|
* Returns the current enabled state of the async rate limiter
|
|
260
159
|
*/
|
|
261
|
-
#getEnabled
|
|
160
|
+
#getEnabled = () => {
|
|
161
|
+
return !!parseFunctionOrValue(this.options.enabled, this);
|
|
162
|
+
};
|
|
262
163
|
/**
|
|
263
164
|
* Returns the current limit of executions allowed within the time window
|
|
264
165
|
*/
|
|
265
|
-
#getLimit
|
|
166
|
+
#getLimit = () => {
|
|
167
|
+
return parseFunctionOrValue(this.options.limit, this);
|
|
168
|
+
};
|
|
266
169
|
/**
|
|
267
170
|
* Returns the current time window in milliseconds
|
|
268
171
|
*/
|
|
269
|
-
#getWindow
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
};
|
|
276
331
|
};
|
|
277
332
|
/**
|
|
278
333
|
* Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.
|
|
@@ -363,5 +418,4 @@ function asyncRateLimit(fn, initialOptions) {
|
|
|
363
418
|
}
|
|
364
419
|
|
|
365
420
|
//#endregion
|
|
366
|
-
export { AsyncRateLimiter, asyncRateLimit, asyncRateLimiterOptions };
|
|
367
|
-
//# sourceMappingURL=async-rate-limiter.js.map
|
|
421
|
+
export { AsyncRateLimiter, asyncRateLimit, asyncRateLimiterOptions };
|
package/dist/async-retryer.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AnyAsyncFunction } from "./types.js";
|
|
2
2
|
import { Store } from "@tanstack/store";
|
|
3
3
|
//#region src/async-retryer.d.ts
|
|
4
|
-
interface AsyncRetryerState<TFn extends AnyAsyncFunction> {
|
|
4
|
+
export interface AsyncRetryerState<TFn extends AnyAsyncFunction> {
|
|
5
5
|
/**
|
|
6
6
|
* The current retry attempt number (0 when not executing)
|
|
7
7
|
*/
|
|
@@ -35,7 +35,7 @@ interface AsyncRetryerState<TFn extends AnyAsyncFunction> {
|
|
|
35
35
|
*/
|
|
36
36
|
totalExecutionTime: number;
|
|
37
37
|
}
|
|
38
|
-
interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {
|
|
38
|
+
export interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {
|
|
39
39
|
/**
|
|
40
40
|
* The backoff strategy for retry delays:
|
|
41
41
|
* - 'exponential': Wait time doubles with each attempt (1s, 2s, 4s, ...)
|
|
@@ -135,7 +135,7 @@ interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {
|
|
|
135
135
|
/**
|
|
136
136
|
* Utility function for sharing common `AsyncRetryerOptions` options between different `AsyncRetryer` instances.
|
|
137
137
|
*/
|
|
138
|
-
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;
|
|
139
139
|
declare const defaultOptions: Omit<Required<AsyncRetryerOptions<any>>, 'initialState' | 'key' | 'onAbort' | 'onError' | 'onLastError' | 'onRetry' | 'onSettled' | 'onSuccess' | 'onExecutionTimeout' | 'onTotalExecutionTimeout'>;
|
|
140
140
|
/**
|
|
141
141
|
* Provides robust retry functionality for asynchronous functions, supporting configurable backoff strategies,
|
|
@@ -231,7 +231,7 @@ declare const defaultOptions: Omit<Required<AsyncRetryerOptions<any>>, 'initialS
|
|
|
231
231
|
*
|
|
232
232
|
* @template TFn The async function type to be retried.
|
|
233
233
|
*/
|
|
234
|
-
declare class AsyncRetryer<TFn extends AnyAsyncFunction> {
|
|
234
|
+
export declare class AsyncRetryer<TFn extends AnyAsyncFunction> {
|
|
235
235
|
#private;
|
|
236
236
|
fn: TFn;
|
|
237
237
|
readonly store: Store<Readonly<AsyncRetryerState<TFn>>>;
|
|
@@ -315,7 +315,5 @@ declare class AsyncRetryer<TFn extends AnyAsyncFunction> {
|
|
|
315
315
|
* const data2 = await fetchWithRetry('/api/data2')
|
|
316
316
|
* ```
|
|
317
317
|
*/
|
|
318
|
-
declare function asyncRetry<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions?: AsyncRetryerOptions<TFn>): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
|
|
319
|
-
//#endregion
|
|
320
|
-
export { AsyncRetryer, AsyncRetryerOptions, AsyncRetryerState, asyncRetry, asyncRetryerOptions };
|
|
321
|
-
//# 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
|