@tanstack/pacer 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/cjs/async-batcher.cjs +1 -12
  2. package/dist/cjs/async-batcher.cjs.map +1 -1
  3. package/dist/cjs/async-batcher.d.cts +4 -16
  4. package/dist/cjs/async-debouncer.cjs +26 -8
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +1 -1
  7. package/dist/cjs/async-queuer.cjs +15 -4
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +10 -5
  10. package/dist/cjs/async-rate-limiter.cjs +53 -12
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +8 -0
  13. package/dist/cjs/async-throttler.cjs +23 -7
  14. package/dist/cjs/async-throttler.cjs.map +1 -1
  15. package/dist/cjs/async-throttler.d.cts +2 -2
  16. package/dist/cjs/batcher.cjs +1 -12
  17. package/dist/cjs/batcher.cjs.map +1 -1
  18. package/dist/cjs/batcher.d.cts +4 -16
  19. package/dist/cjs/debouncer.cjs +2 -1
  20. package/dist/cjs/debouncer.cjs.map +1 -1
  21. package/dist/cjs/queuer.cjs +11 -0
  22. package/dist/cjs/queuer.cjs.map +1 -1
  23. package/dist/cjs/queuer.d.cts +13 -8
  24. package/dist/cjs/rate-limiter.cjs +53 -12
  25. package/dist/cjs/rate-limiter.cjs.map +1 -1
  26. package/dist/cjs/rate-limiter.d.cts +8 -0
  27. package/dist/cjs/throttler.cjs +5 -0
  28. package/dist/cjs/throttler.cjs.map +1 -1
  29. package/dist/cjs/throttler.d.cts +5 -5
  30. package/dist/esm/async-batcher.d.ts +4 -16
  31. package/dist/esm/async-batcher.js +1 -12
  32. package/dist/esm/async-batcher.js.map +1 -1
  33. package/dist/esm/async-debouncer.d.ts +1 -1
  34. package/dist/esm/async-debouncer.js +26 -8
  35. package/dist/esm/async-debouncer.js.map +1 -1
  36. package/dist/esm/async-queuer.d.ts +10 -5
  37. package/dist/esm/async-queuer.js +15 -4
  38. package/dist/esm/async-queuer.js.map +1 -1
  39. package/dist/esm/async-rate-limiter.d.ts +8 -0
  40. package/dist/esm/async-rate-limiter.js +53 -12
  41. package/dist/esm/async-rate-limiter.js.map +1 -1
  42. package/dist/esm/async-throttler.d.ts +2 -2
  43. package/dist/esm/async-throttler.js +23 -7
  44. package/dist/esm/async-throttler.js.map +1 -1
  45. package/dist/esm/batcher.d.ts +4 -16
  46. package/dist/esm/batcher.js +1 -12
  47. package/dist/esm/batcher.js.map +1 -1
  48. package/dist/esm/debouncer.js +2 -1
  49. package/dist/esm/debouncer.js.map +1 -1
  50. package/dist/esm/queuer.d.ts +13 -8
  51. package/dist/esm/queuer.js +11 -0
  52. package/dist/esm/queuer.js.map +1 -1
  53. package/dist/esm/rate-limiter.d.ts +8 -0
  54. package/dist/esm/rate-limiter.js +53 -12
  55. package/dist/esm/rate-limiter.js.map +1 -1
  56. package/dist/esm/throttler.d.ts +5 -5
  57. package/dist/esm/throttler.js +5 -0
  58. package/dist/esm/throttler.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/async-batcher.ts +5 -28
  61. package/src/async-debouncer.ts +28 -8
  62. package/src/async-queuer.ts +27 -9
  63. package/src/async-rate-limiter.ts +72 -12
  64. package/src/async-throttler.ts +26 -8
  65. package/src/batcher.ts +5 -28
  66. package/src/debouncer.ts +2 -1
  67. package/src/queuer.ts +24 -8
  68. package/src/rate-limiter.ts +71 -11
  69. package/src/throttler.ts +10 -5
@@ -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
- return combinedState
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.#getRelevantExecutionTimes()
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
- #getRelevantExecutionTimes = (): Array<number> => {
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.store.state.executionTimes.filter(
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.#getRelevantExecutionTimes()
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
  */
@@ -18,11 +22,7 @@ export interface ThrottlerState<TFn extends AnyFunction> {
18
22
  /**
19
23
  * Timestamp when the next execution can occur in milliseconds
20
24
  */
21
- nextExecutionTime: number
22
- /**
23
- * Whether the throttler is waiting for the timeout to trigger execution
24
- */
25
- isPending: boolean
25
+ nextExecutionTime: number | undefined
26
26
  /**
27
27
  * Current execution status - 'idle' when not active, 'pending' when waiting for timeout
28
28
  */
@@ -246,6 +246,11 @@ export class Throttler<TFn extends AnyFunction> {
246
246
  lastArgs: undefined,
247
247
  })
248
248
  this.options.onExecute?.(this)
249
+ setTimeout(() => {
250
+ if (!this.store.state.isPending) {
251
+ this.#setState({ nextExecutionTime: undefined })
252
+ }
253
+ }, this.#getWait())
249
254
  }
250
255
 
251
256
  /**