@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
@@ -23,10 +23,6 @@ export interface AsyncBatcherState<TValue> {
23
23
  * Whether the batcher is waiting for the timeout to trigger batch processing
24
24
  */
25
25
  isPending: boolean
26
- /**
27
- * Whether the batcher is active and will process items automatically
28
- */
29
- isRunning: boolean
30
26
  /**
31
27
  * Array of items currently queued for batch processing
32
28
  */
@@ -51,14 +47,14 @@ export interface AsyncBatcherState<TValue> {
51
47
  * Number of batch executions that have completed successfully
52
48
  */
53
49
  successCount: number
54
- /**
55
- * Total number of items that have been processed across all batches
56
- */
57
- totalItemsProcessed: number
58
50
  /**
59
51
  * Total number of items that have failed processing across all batches
60
52
  */
61
53
  totalItemsFailed: number
54
+ /**
55
+ * Total number of items that have been processed across all batches
56
+ */
57
+ totalItemsProcessed: number
62
58
  }
63
59
 
64
60
  function getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {
@@ -68,7 +64,6 @@ function getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {
68
64
  isEmpty: true,
69
65
  isExecuting: false,
70
66
  isPending: false,
71
- isRunning: true,
72
67
  items: [],
73
68
  lastResult: undefined,
74
69
  settleCount: 0,
@@ -297,7 +292,7 @@ export class AsyncBatcher<TValue> {
297
292
 
298
293
  if (shouldProcess) {
299
294
  this.#execute()
300
- } else if (this.store.state.isRunning && this.options.wait !== Infinity) {
295
+ } else if (this.options.wait !== Infinity) {
301
296
  this.#clearTimeout() // clear any pending timeout to replace it with a new one
302
297
  this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
303
298
  }
@@ -365,24 +360,6 @@ export class AsyncBatcher<TValue> {
365
360
  return await this.#execute()
366
361
  }
367
362
 
368
- /**
369
- * Stops the async batcher from processing batches
370
- */
371
- stop = (): void => {
372
- this.#setState({ isRunning: false })
373
- this.#clearTimeout()
374
- }
375
-
376
- /**
377
- * Starts the async batcher and processes any pending items
378
- */
379
- start = (): void => {
380
- this.#setState({ isRunning: true })
381
- if (this.store.state.items.length > 0 && !this.#timeoutId) {
382
- this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
383
- }
384
- }
385
-
386
363
  /**
387
364
  * Returns a copy of all items in the async batcher
388
365
  */
@@ -176,6 +176,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
176
176
  #resolvePreviousPromise:
177
177
  | ((value?: ReturnType<TFn> | undefined) => void)
178
178
  | null = null
179
+ #rejectPreviousPromise: ((reason?: unknown) => void) | null = null
179
180
 
180
181
  constructor(
181
182
  private fn: TFn,
@@ -270,8 +271,9 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
270
271
  this.#setState({ isPending: true })
271
272
  }
272
273
 
273
- return new Promise((resolve) => {
274
+ return new Promise((resolve, reject) => {
274
275
  this.#resolvePreviousPromise = resolve
276
+ this.#rejectPreviousPromise = reject
275
277
  this.#timeoutId = setTimeout(async () => {
276
278
  // Execute trailing if enabled
277
279
  if (this.options.trailing && this.store.state.lastArgs) {
@@ -305,12 +307,13 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
305
307
  })
306
308
  this.options.onError?.(error, this)
307
309
  if (this.options.throwOnError) {
308
- throw error
310
+ this.#rejectPreviousPromiseInternal(error)
309
311
  }
310
312
  } finally {
311
313
  this.#setState({
312
314
  isExecuting: false,
313
315
  isPending: false,
316
+ lastArgs: undefined,
314
317
  settleCount: this.store.state.settleCount + 1,
315
318
  })
316
319
  this.#abortController = null
@@ -322,11 +325,31 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
322
325
  /**
323
326
  * Processes the current pending execution immediately
324
327
  */
325
- flush = (): void => {
328
+ flush = async (): Promise<ReturnType<TFn> | undefined> => {
326
329
  if (this.store.state.isPending && this.store.state.lastArgs) {
327
330
  this.#abortExecution() // abort any current execution
328
331
  this.#clearTimeout() // clear any existing timeout
329
- this.#execute(...this.store.state.lastArgs)
332
+ const result = await this.#execute(...this.store.state.lastArgs)
333
+
334
+ // Resolve any pending promise from maybeExecute
335
+ this.#resolvePreviousPromiseInternal()
336
+
337
+ return result
338
+ }
339
+ return undefined
340
+ }
341
+
342
+ #resolvePreviousPromiseInternal = (): void => {
343
+ if (this.#resolvePreviousPromise) {
344
+ this.#resolvePreviousPromise(this.store.state.lastResult)
345
+ this.#resolvePreviousPromise = null
346
+ }
347
+ }
348
+
349
+ #rejectPreviousPromiseInternal = (error: unknown): void => {
350
+ if (this.#rejectPreviousPromise) {
351
+ this.#rejectPreviousPromise(error)
352
+ this.#rejectPreviousPromise = null
330
353
  }
331
354
  }
332
355
 
@@ -339,10 +362,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
339
362
 
340
363
  #cancelPendingExecution = (): void => {
341
364
  this.#clearTimeout()
342
- if (this.#resolvePreviousPromise) {
343
- this.#resolvePreviousPromise(this.store.state.lastResult)
344
- this.#resolvePreviousPromise = null
345
- }
365
+ this.#resolvePreviousPromiseInternal()
346
366
  this.#setState({
347
367
  isPending: false,
348
368
  isExecuting: false,
@@ -32,14 +32,14 @@ export interface AsyncQueuerState<TValue> {
32
32
  * Whether the queuer is active and will process items automatically
33
33
  */
34
34
  isRunning: boolean
35
- /**
36
- * Timestamps when items were added to the queue for expiration tracking
37
- */
38
- itemTimestamps: Array<number>
39
35
  /**
40
36
  * Array of items currently waiting to be processed
41
37
  */
42
38
  items: Array<TValue>
39
+ /**
40
+ * Timestamps when items were added to the queue for expiration tracking
41
+ */
42
+ itemTimestamps: Array<number>
43
43
  /**
44
44
  * The result from the most recent task execution
45
45
  */
@@ -507,6 +507,12 @@ export class AsyncQueuer<TValue> {
507
507
  return item
508
508
  }
509
509
 
510
+ #getAllItems = (): Array<TValue> => {
511
+ const items = this.peekAllItems()
512
+ this.clear()
513
+ return items
514
+ }
515
+
510
516
  /**
511
517
  * Removes and returns the next item from the queue and executes the task function with it.
512
518
  *
@@ -552,14 +558,26 @@ export class AsyncQueuer<TValue> {
552
558
  * Processes a specified number of items to execute immediately with no wait time
553
559
  * If no numberOfItems is provided, all items will be processed
554
560
  */
555
- flush = (
561
+ flush = async (
556
562
  numberOfItems: number = this.store.state.items.length,
557
563
  position?: QueuePosition,
558
- ): void => {
564
+ ): Promise<void> => {
559
565
  this.#clearTimeouts() // clear any pending timeouts
560
- for (let i = 0; i < numberOfItems; i++) {
561
- this.execute(position)
562
- }
566
+ await Promise.all(
567
+ Array.from({ length: numberOfItems }, () => this.execute(position)),
568
+ )
569
+ }
570
+
571
+ /**
572
+ * Processes all items in the queue as a batch using the provided function
573
+ * The queue is cleared after processing
574
+ */
575
+ flushAsBatch = async (
576
+ batchFunction: (items: Array<TValue>) => Promise<any>,
577
+ ): Promise<void> => {
578
+ this.#clearTimeouts() // clear any pending timeouts
579
+ const items = this.#getAllItems()
580
+ await batchFunction(items)
563
581
  }
564
582
 
565
583
  /**
@@ -11,6 +11,10 @@ export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
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
  * Whether the rate-limited function is currently executing asynchronously
16
20
  */
@@ -27,6 +31,10 @@ export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
27
31
  * Number of function executions that have completed (either successfully or with errors)
28
32
  */
29
33
  settleCount: number
34
+ /**
35
+ * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded
36
+ */
37
+ status: 'disabled' | 'executing' | 'exceeded' | 'idle'
30
38
  /**
31
39
  * Number of function executions that have completed successfully
32
40
  */
@@ -39,11 +47,13 @@ function getDefaultAsyncRateLimiterState<
39
47
  return {
40
48
  errorCount: 0,
41
49
  executionTimes: [],
50
+ isExceeded: false,
42
51
  isExecuting: false,
43
52
  lastResult: undefined,
44
53
  rejectionCount: 0,
45
54
  settleCount: 0,
46
55
  successCount: 0,
56
+ status: 'idle',
47
57
  }
48
58
  }
49
59
 
@@ -189,6 +199,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
189
199
  AsyncRateLimiterState<TFn>
190
200
  >(getDefaultAsyncRateLimiterState<TFn>())
191
201
  options: AsyncRateLimiterOptions<TFn>
202
+ #timeoutIds: Set<NodeJS.Timeout> = new Set()
192
203
 
193
204
  constructor(
194
205
  private fn: TFn,
@@ -200,6 +211,9 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
200
211
  throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
201
212
  }
202
213
  this.#setState(this.options.initialState ?? {})
214
+ for (const executionTime of this.#getExecutionTimesInWindow()) {
215
+ this.#setCleanupTimeout(executionTime)
216
+ }
203
217
  }
204
218
 
205
219
  /**
@@ -215,7 +229,19 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
215
229
  ...state,
216
230
  ...newState,
217
231
  }
218
- return combinedState
232
+ const isExceeded = combinedState.executionTimes.length >= this.#getLimit()
233
+ const status = !this.#getEnabled()
234
+ ? 'disabled'
235
+ : combinedState.isExecuting
236
+ ? 'executing'
237
+ : isExceeded
238
+ ? 'exceeded'
239
+ : 'idle'
240
+ return {
241
+ ...combinedState,
242
+ isExceeded,
243
+ status,
244
+ }
219
245
  })
220
246
  }
221
247
 
@@ -270,7 +296,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
270
296
  ): Promise<ReturnType<TFn> | undefined> => {
271
297
  this.#cleanupOldExecutions()
272
298
 
273
- const relevantExecutionTimes = this.#getRelevantExecutionTimes()
299
+ const relevantExecutionTimes = this.#getExecutionTimesInWindow()
274
300
 
275
301
  if (relevantExecutionTimes.length < this.#getLimit()) {
276
302
  await this.#execute(...args)
@@ -297,7 +323,8 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
297
323
  })
298
324
 
299
325
  try {
300
- const result = await this.fn(...args)
326
+ const result = await this.fn(...args) // EXECUTE!
327
+ this.#setCleanupTimeout(now)
301
328
  this.#setState({
302
329
  successCount: this.store.state.successCount + 1,
303
330
  lastResult: result,
@@ -322,7 +349,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
322
349
  return this.store.state.lastResult
323
350
  }
324
351
 
325
- #getRelevantExecutionTimes = (): Array<number> => {
352
+ #getExecutionTimesInWindow = (): Array<number> => {
326
353
  if (this.options.windowType === 'sliding') {
327
354
  // For sliding window, return all executions within the current window
328
355
  return this.store.state.executionTimes.filter(
@@ -331,22 +358,54 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
331
358
  } else {
332
359
  // For fixed window, return all executions in the current window
333
360
  // The window starts from the oldest execution time
361
+ if (this.store.state.executionTimes.length === 0) {
362
+ return []
363
+ }
334
364
  const oldestExecution = Math.min(...this.store.state.executionTimes)
335
365
  const windowStart = oldestExecution
366
+ const windowEnd = windowStart + this.#getWindow()
367
+ const now = Date.now()
368
+
369
+ // If the window has expired, return empty array
370
+ if (now > windowEnd) {
371
+ return []
372
+ }
373
+
374
+ // Otherwise, return all executions in the current window
336
375
  return this.store.state.executionTimes.filter(
337
- (time) =>
338
- time >= windowStart && time <= windowStart + this.#getWindow(),
376
+ (time) => time >= windowStart && time <= windowEnd,
339
377
  )
340
378
  }
341
379
  }
342
380
 
381
+ #setCleanupTimeout = (executionTime: number): void => {
382
+ if (
383
+ this.options.windowType === 'sliding' ||
384
+ this.#timeoutIds.size === 0 // new fixed window
385
+ ) {
386
+ const now = Date.now()
387
+ const timeUntilExpiration = executionTime - now + this.#getWindow() + 1
388
+ const timeoutId = setTimeout(() => {
389
+ this.#cleanupOldExecutions()
390
+ this.#clearTimeout(timeoutId)
391
+ }, timeUntilExpiration)
392
+ this.#timeoutIds.add(timeoutId)
393
+ }
394
+ }
395
+
396
+ #clearTimeout = (timeoutId: NodeJS.Timeout): void => {
397
+ clearTimeout(timeoutId)
398
+ this.#timeoutIds.delete(timeoutId)
399
+ }
400
+
401
+ #clearTimeouts = (): void => {
402
+ this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))
403
+ this.#timeoutIds.clear()
404
+ }
405
+
343
406
  #cleanupOldExecutions = (): void => {
344
- const now = Date.now()
345
- const windowStart = now - this.#getWindow()
346
407
  this.#setState({
347
- executionTimes: this.store.state.executionTimes.filter(
348
- (time) => time > windowStart,
349
- ),
408
+ executionTimes: this.#getExecutionTimesInWindow(),
350
409
  })
351
410
  }
352
411
 
@@ -354,7 +413,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
354
413
  * Returns the number of remaining executions allowed in the current window
355
414
  */
356
415
  getRemainingInWindow = (): number => {
357
- const relevantExecutionTimes = this.#getRelevantExecutionTimes()
416
+ const relevantExecutionTimes = this.#getExecutionTimesInWindow()
358
417
  return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)
359
418
  }
360
419
 
@@ -376,6 +435,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
376
435
  */
377
436
  reset = (): void => {
378
437
  this.#setState(getDefaultAsyncRateLimiterState())
438
+ this.#clearTimeouts()
379
439
  }
380
440
  }
381
441
 
@@ -30,7 +30,7 @@ export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
30
30
  /**
31
31
  * Timestamp when the next execution can occur in milliseconds
32
32
  */
33
- nextExecutionTime: number
33
+ nextExecutionTime: number | undefined
34
34
  /**
35
35
  * Number of function executions that have completed (either successfully or with errors)
36
36
  */
@@ -55,7 +55,7 @@ function getDefaultAsyncThrottlerState<
55
55
  lastArgs: undefined,
56
56
  lastExecutionTime: 0,
57
57
  lastResult: undefined,
58
- nextExecutionTime: 0,
58
+ nextExecutionTime: undefined,
59
59
  settleCount: 0,
60
60
  status: 'idle',
61
61
  successCount: 0,
@@ -187,6 +187,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
187
187
  #resolvePreviousPromise:
188
188
  | ((value?: ReturnType<TFn> | undefined) => void)
189
189
  | null = null
190
+ #rejectPreviousPromise: ((reason?: unknown) => void) | null = null
190
191
 
191
192
  constructor(
192
193
  private fn: TFn,
@@ -287,8 +288,9 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
287
288
  await this.#execute(...args)
288
289
  return this.store.state.lastResult
289
290
  } else {
290
- return new Promise((resolve) => {
291
+ return new Promise((resolve, reject) => {
291
292
  this.#resolvePreviousPromise = resolve
293
+ this.#rejectPreviousPromise = reject
292
294
  // Clear any existing timeout to ensure we use the latest arguments
293
295
  this.#clearTimeout()
294
296
 
@@ -330,9 +332,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
330
332
  })
331
333
  this.options.onError?.(error, this)
332
334
  if (this.options.throwOnError) {
333
- throw error
334
- } else {
335
- console.error(error)
335
+ this.#rejectPreviousPromiseInternal(error)
336
336
  }
337
337
  } finally {
338
338
  const lastExecutionTime = Date.now()
@@ -346,6 +346,11 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
346
346
  })
347
347
  this.#abortController = null
348
348
  this.options.onSettled?.(this)
349
+ setTimeout(() => {
350
+ if (!this.store.state.isPending) {
351
+ this.#setState({ nextExecutionTime: undefined })
352
+ }
353
+ }, this.#getWait())
349
354
  }
350
355
  return this.store.state.lastResult
351
356
  }
@@ -353,12 +358,18 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
353
358
  /**
354
359
  * Processes the current pending execution immediately
355
360
  */
356
- flush = (): void => {
361
+ flush = async (): Promise<ReturnType<TFn> | undefined> => {
357
362
  if (this.store.state.isPending && this.store.state.lastArgs) {
358
363
  this.#abortExecution() // abort any current execution
359
364
  this.#clearTimeout() // clear any existing timeout
360
- this.#execute(...this.store.state.lastArgs)
365
+ const result = await this.#execute(...this.store.state.lastArgs)
366
+
367
+ // Resolve any pending promise from maybeExecute
368
+ this.#resolvePreviousPromiseInternal()
369
+
370
+ return result
361
371
  }
372
+ return undefined
362
373
  }
363
374
 
364
375
  #resolvePreviousPromiseInternal = (): void => {
@@ -368,6 +379,13 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
368
379
  }
369
380
  }
370
381
 
382
+ #rejectPreviousPromiseInternal = (error: unknown): void => {
383
+ if (this.#rejectPreviousPromise) {
384
+ this.#rejectPreviousPromise(error)
385
+ this.#rejectPreviousPromise = null
386
+ }
387
+ }
388
+
371
389
  #clearTimeout = (): void => {
372
390
  if (this.#timeoutId) {
373
391
  clearTimeout(this.#timeoutId)
package/src/batcher.ts CHANGED
@@ -15,14 +15,6 @@ export interface BatcherState<TValue> {
15
15
  * Whether the batcher is waiting for the timeout to trigger batch processing
16
16
  */
17
17
  isPending: boolean
18
- /**
19
- * Whether the batcher is active and will process items automatically
20
- */
21
- isRunning: boolean
22
- /**
23
- * Total number of items that have been processed across all batches
24
- */
25
- totalItemsProcessed: number
26
18
  /**
27
19
  * Array of items currently queued for batch processing
28
20
  */
@@ -35,6 +27,10 @@ export interface BatcherState<TValue> {
35
27
  * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout
36
28
  */
37
29
  status: 'idle' | 'pending'
30
+ /**
31
+ * Total number of items that have been processed across all batches
32
+ */
33
+ totalItemsProcessed: number
38
34
  }
39
35
 
40
36
  function getDefaultBatcherState<TValue>(): BatcherState<TValue> {
@@ -42,7 +38,6 @@ function getDefaultBatcherState<TValue>(): BatcherState<TValue> {
42
38
  executionCount: 0,
43
39
  isEmpty: true,
44
40
  isPending: false,
45
- isRunning: true,
46
41
  totalItemsProcessed: 0,
47
42
  items: [],
48
43
  size: 0,
@@ -204,7 +199,7 @@ export class Batcher<TValue> {
204
199
 
205
200
  if (shouldProcess) {
206
201
  this.#execute()
207
- } else if (this.store.state.isRunning && this.options.wait !== Infinity) {
202
+ } else if (this.options.wait !== Infinity) {
208
203
  this.#clearTimeout() // clear any pending timeout to replace it with a new one
209
204
  this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
210
205
  }
@@ -244,24 +239,6 @@ export class Batcher<TValue> {
244
239
  this.#execute() // execute immediately
245
240
  }
246
241
 
247
- /**
248
- * Stops the batcher from processing batches
249
- */
250
- stop = (): void => {
251
- this.#setState({ isRunning: false })
252
- this.#clearTimeout()
253
- }
254
-
255
- /**
256
- * Starts the batcher and processes any pending items
257
- */
258
- start = (): void => {
259
- this.#setState({ isRunning: true })
260
- if (this.store.state.items.length > 0) {
261
- this.#execute()
262
- }
263
- }
264
-
265
242
  /**
266
243
  * Returns a copy of all items in the batcher
267
244
  */
package/src/debouncer.ts CHANGED
@@ -213,8 +213,9 @@ export class Debouncer<TFn extends AnyFunction> {
213
213
  if (!this.#getEnabled()) return undefined
214
214
  this.fn(...args) // EXECUTE!
215
215
  this.#setState({
216
- isPending: false,
217
216
  executionCount: this.store.state.executionCount + 1,
217
+ isPending: false,
218
+ lastArgs: undefined,
218
219
  })
219
220
  this.options.onExecute?.(this)
220
221
  }
package/src/queuer.ts CHANGED
@@ -26,14 +26,14 @@ export interface QueuerState<TValue> {
26
26
  * Whether the queuer is active and will process items automatically
27
27
  */
28
28
  isRunning: boolean
29
- /**
30
- * Timestamps when items were added to the queue for expiration tracking
31
- */
32
- itemTimestamps: Array<number>
33
29
  /**
34
30
  * Array of items currently waiting to be processed
35
31
  */
36
32
  items: Array<TValue>
33
+ /**
34
+ * Timestamps when items were added to the queue for expiration tracking
35
+ */
36
+ itemTimestamps: Array<number>
37
37
  /**
38
38
  * Whether the queuer has a pending timeout for processing the next item
39
39
  */
@@ -112,14 +112,14 @@ export interface QueuerOptions<TValue> {
112
112
  * Maximum number of items allowed in the queuer
113
113
  */
114
114
  maxSize?: number
115
- /**
116
- * Callback fired whenever an item expires in the queuer
117
- */
118
- onExpire?: (item: TValue, queuer: Queuer<TValue>) => void
119
115
  /**
120
116
  * Callback fired whenever an item is removed from the queuer
121
117
  */
122
118
  onExecute?: (item: TValue, queuer: Queuer<TValue>) => void
119
+ /**
120
+ * Callback fired whenever an item expires in the queuer
121
+ */
122
+ onExpire?: (item: TValue, queuer: Queuer<TValue>) => void
123
123
  /**
124
124
  * Callback fired whenever an item is added or removed from the queuer
125
125
  */
@@ -472,6 +472,12 @@ export class Queuer<TValue> {
472
472
  return item
473
473
  }
474
474
 
475
+ #getAllItems = (): Array<TValue> => {
476
+ const items = this.peekAllItems()
477
+ this.clear()
478
+ return items
479
+ }
480
+
475
481
  /**
476
482
  * Removes and returns the next item from the queue and processes it using the provided function.
477
483
  *
@@ -509,6 +515,16 @@ export class Queuer<TValue> {
509
515
  this.#tick()
510
516
  }
511
517
 
518
+ /**
519
+ * Processes all items in the queue as a batch using the provided function
520
+ * The queue is cleared after processing
521
+ */
522
+ flushAsBatch = (batchFunction: (items: Array<TValue>) => void): void => {
523
+ const items = this.#getAllItems()
524
+ this.clear()
525
+ batchFunction(items)
526
+ }
527
+
512
528
  /**
513
529
  * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
514
530
  * Internal use only.