@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
package/src/rate-limiter.ts
CHANGED
|
@@ -11,17 +11,27 @@ export interface RateLimiterState {
|
|
|
11
11
|
* Array of timestamps when executions occurred for rate limiting calculations
|
|
12
12
|
*/
|
|
13
13
|
executionTimes: Array<number>
|
|
14
|
+
/**
|
|
15
|
+
* Whether the rate limiter has exceeded the limit
|
|
16
|
+
*/
|
|
17
|
+
isExceeded: boolean
|
|
14
18
|
/**
|
|
15
19
|
* Number of function executions that have been rejected due to rate limiting
|
|
16
20
|
*/
|
|
17
21
|
rejectionCount: number
|
|
22
|
+
/**
|
|
23
|
+
* Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded
|
|
24
|
+
*/
|
|
25
|
+
status: 'disabled' | 'exceeded' | 'idle'
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
function getDefaultRateLimiterState(): RateLimiterState {
|
|
21
29
|
return structuredClone({
|
|
22
30
|
executionCount: 0,
|
|
23
31
|
executionTimes: [],
|
|
32
|
+
isExceeded: false,
|
|
24
33
|
rejectionCount: 0,
|
|
34
|
+
status: 'idle',
|
|
25
35
|
})
|
|
26
36
|
}
|
|
27
37
|
|
|
@@ -123,6 +133,7 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
123
133
|
readonly store: Store<Readonly<RateLimiterState>> =
|
|
124
134
|
new Store<RateLimiterState>(getDefaultRateLimiterState())
|
|
125
135
|
options: RateLimiterOptions<TFn>
|
|
136
|
+
#timeoutIds: Set<NodeJS.Timeout> = new Set()
|
|
126
137
|
|
|
127
138
|
constructor(
|
|
128
139
|
private fn: TFn,
|
|
@@ -133,6 +144,9 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
133
144
|
...initialOptions,
|
|
134
145
|
}
|
|
135
146
|
this.#setState(this.options.initialState ?? {})
|
|
147
|
+
for (const executionTime of this.#getExecutionTimesInWindow()) {
|
|
148
|
+
this.#setCleanupTimeout(executionTime)
|
|
149
|
+
}
|
|
136
150
|
}
|
|
137
151
|
|
|
138
152
|
/**
|
|
@@ -148,7 +162,17 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
148
162
|
...state,
|
|
149
163
|
...newState,
|
|
150
164
|
}
|
|
151
|
-
|
|
165
|
+
const isExceeded = combinedState.executionTimes.length >= this.#getLimit()
|
|
166
|
+
const status = !this.#getEnabled()
|
|
167
|
+
? 'disabled'
|
|
168
|
+
: isExceeded
|
|
169
|
+
? 'exceeded'
|
|
170
|
+
: 'idle'
|
|
171
|
+
return {
|
|
172
|
+
...combinedState,
|
|
173
|
+
isExceeded,
|
|
174
|
+
status,
|
|
175
|
+
}
|
|
152
176
|
})
|
|
153
177
|
}
|
|
154
178
|
|
|
@@ -191,7 +215,7 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
191
215
|
maybeExecute = (...args: Parameters<TFn>): boolean => {
|
|
192
216
|
this.#cleanupOldExecutions()
|
|
193
217
|
|
|
194
|
-
const relevantExecutionTimes = this.#
|
|
218
|
+
const relevantExecutionTimes = this.#getExecutionTimesInWindow()
|
|
195
219
|
|
|
196
220
|
if (relevantExecutionTimes.length < this.#getLimit()) {
|
|
197
221
|
this.#execute(...args)
|
|
@@ -210,13 +234,16 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
210
234
|
const now = Date.now()
|
|
211
235
|
this.fn(...args) // EXECUTE!
|
|
212
236
|
this.store.state.executionTimes.push(now) // mutate state directly for performance
|
|
237
|
+
|
|
238
|
+
this.#setCleanupTimeout(now)
|
|
239
|
+
|
|
213
240
|
this.#setState({
|
|
214
241
|
executionCount: this.store.state.executionCount + 1,
|
|
215
242
|
})
|
|
216
243
|
this.options.onExecute?.(this)
|
|
217
244
|
}
|
|
218
245
|
|
|
219
|
-
#
|
|
246
|
+
#getExecutionTimesInWindow = (): Array<number> => {
|
|
220
247
|
if (this.options.windowType === 'sliding') {
|
|
221
248
|
// For sliding window, return all executions within the current window
|
|
222
249
|
return this.store.state.executionTimes.filter(
|
|
@@ -225,22 +252,54 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
225
252
|
} else {
|
|
226
253
|
// For fixed window, return all executions in the current window
|
|
227
254
|
// The window starts from the oldest execution time
|
|
255
|
+
if (this.store.state.executionTimes.length === 0) {
|
|
256
|
+
return []
|
|
257
|
+
}
|
|
228
258
|
const oldestExecution = Math.min(...this.store.state.executionTimes)
|
|
229
259
|
const windowStart = oldestExecution
|
|
260
|
+
const windowEnd = windowStart + this.#getWindow()
|
|
261
|
+
const now = Date.now()
|
|
262
|
+
|
|
263
|
+
// If the window has expired, return empty array
|
|
264
|
+
if (now > windowEnd) {
|
|
265
|
+
return []
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Otherwise, return all executions in the current window
|
|
230
269
|
return this.store.state.executionTimes.filter(
|
|
231
|
-
(time) =>
|
|
232
|
-
time >= windowStart && time <= windowStart + this.#getWindow(),
|
|
270
|
+
(time) => time >= windowStart && time <= windowEnd,
|
|
233
271
|
)
|
|
234
272
|
}
|
|
235
273
|
}
|
|
236
274
|
|
|
275
|
+
#setCleanupTimeout = (executionTime: number): void => {
|
|
276
|
+
if (
|
|
277
|
+
this.options.windowType === 'sliding' ||
|
|
278
|
+
this.#timeoutIds.size === 0 // new fixed window
|
|
279
|
+
) {
|
|
280
|
+
const now = Date.now()
|
|
281
|
+
const timeUntilExpiration = executionTime - now + this.#getWindow() + 1
|
|
282
|
+
const timeoutId = setTimeout(() => {
|
|
283
|
+
this.#cleanupOldExecutions()
|
|
284
|
+
this.#clearTimeout(timeoutId)
|
|
285
|
+
}, timeUntilExpiration)
|
|
286
|
+
this.#timeoutIds.add(timeoutId)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#clearTimeout = (timeoutId: NodeJS.Timeout): void => {
|
|
291
|
+
clearTimeout(timeoutId)
|
|
292
|
+
this.#timeoutIds.delete(timeoutId)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#clearTimeouts = (): void => {
|
|
296
|
+
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))
|
|
297
|
+
this.#timeoutIds.clear()
|
|
298
|
+
}
|
|
299
|
+
|
|
237
300
|
#cleanupOldExecutions = (): void => {
|
|
238
|
-
const now = Date.now()
|
|
239
|
-
const windowStart = now - this.#getWindow()
|
|
240
301
|
this.#setState({
|
|
241
|
-
executionTimes: this
|
|
242
|
-
(time) => time > windowStart,
|
|
243
|
-
),
|
|
302
|
+
executionTimes: this.#getExecutionTimesInWindow(),
|
|
244
303
|
})
|
|
245
304
|
}
|
|
246
305
|
|
|
@@ -248,7 +307,7 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
248
307
|
* Returns the number of remaining executions allowed in the current window
|
|
249
308
|
*/
|
|
250
309
|
getRemainingInWindow = (): number => {
|
|
251
|
-
const relevantExecutionTimes = this.#
|
|
310
|
+
const relevantExecutionTimes = this.#getExecutionTimesInWindow()
|
|
252
311
|
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)
|
|
253
312
|
}
|
|
254
313
|
|
|
@@ -268,6 +327,7 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
268
327
|
*/
|
|
269
328
|
reset = (): void => {
|
|
270
329
|
this.#setState(getDefaultRateLimiterState())
|
|
330
|
+
this.#clearTimeouts()
|
|
271
331
|
}
|
|
272
332
|
}
|
|
273
333
|
|
package/src/throttler.ts
CHANGED
|
@@ -7,6 +7,10 @@ export interface ThrottlerState<TFn extends AnyFunction> {
|
|
|
7
7
|
* Number of function executions that have been completed
|
|
8
8
|
*/
|
|
9
9
|
executionCount: number
|
|
10
|
+
/**
|
|
11
|
+
* Whether the throttler is waiting for the timeout to trigger execution
|
|
12
|
+
*/
|
|
13
|
+
isPending: boolean
|
|
10
14
|
/**
|
|
11
15
|
* The arguments from the most recent call to maybeExecute
|
|
12
16
|
*/
|
|
@@ -19,10 +23,6 @@ export interface ThrottlerState<TFn extends AnyFunction> {
|
|
|
19
23
|
* Timestamp when the next execution can occur in milliseconds
|
|
20
24
|
*/
|
|
21
25
|
nextExecutionTime: number
|
|
22
|
-
/**
|
|
23
|
-
* Whether the throttler is waiting for the timeout to trigger execution
|
|
24
|
-
*/
|
|
25
|
-
isPending: boolean
|
|
26
26
|
/**
|
|
27
27
|
* Current execution status - 'idle' when not active, 'pending' when waiting for timeout
|
|
28
28
|
*/
|