@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.
Files changed (62) 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 +25 -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 +20 -9
  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 +17 -6
  14. package/dist/cjs/async-throttler.cjs.map +1 -1
  15. package/dist/cjs/async-throttler.d.cts +1 -1
  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/queuer.cjs +16 -4
  20. package/dist/cjs/queuer.cjs.map +1 -1
  21. package/dist/cjs/queuer.d.cts +13 -8
  22. package/dist/cjs/rate-limiter.cjs +53 -12
  23. package/dist/cjs/rate-limiter.cjs.map +1 -1
  24. package/dist/cjs/rate-limiter.d.cts +8 -0
  25. package/dist/cjs/throttler.cjs.map +1 -1
  26. package/dist/cjs/throttler.d.cts +4 -4
  27. package/dist/esm/async-batcher.d.ts +4 -16
  28. package/dist/esm/async-batcher.js +1 -12
  29. package/dist/esm/async-batcher.js.map +1 -1
  30. package/dist/esm/async-debouncer.d.ts +1 -1
  31. package/dist/esm/async-debouncer.js +25 -8
  32. package/dist/esm/async-debouncer.js.map +1 -1
  33. package/dist/esm/async-queuer.d.ts +10 -5
  34. package/dist/esm/async-queuer.js +20 -9
  35. package/dist/esm/async-queuer.js.map +1 -1
  36. package/dist/esm/async-rate-limiter.d.ts +8 -0
  37. package/dist/esm/async-rate-limiter.js +53 -12
  38. package/dist/esm/async-rate-limiter.js.map +1 -1
  39. package/dist/esm/async-throttler.d.ts +1 -1
  40. package/dist/esm/async-throttler.js +17 -6
  41. package/dist/esm/async-throttler.js.map +1 -1
  42. package/dist/esm/batcher.d.ts +4 -16
  43. package/dist/esm/batcher.js +1 -12
  44. package/dist/esm/batcher.js.map +1 -1
  45. package/dist/esm/queuer.d.ts +13 -8
  46. package/dist/esm/queuer.js +16 -4
  47. package/dist/esm/queuer.js.map +1 -1
  48. package/dist/esm/rate-limiter.d.ts +8 -0
  49. package/dist/esm/rate-limiter.js +53 -12
  50. package/dist/esm/rate-limiter.js.map +1 -1
  51. package/dist/esm/throttler.d.ts +4 -4
  52. package/dist/esm/throttler.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/async-batcher.ts +5 -28
  55. package/src/async-debouncer.ts +27 -8
  56. package/src/async-queuer.ts +32 -14
  57. package/src/async-rate-limiter.ts +72 -12
  58. package/src/async-throttler.ts +19 -6
  59. package/src/batcher.ts +5 -28
  60. package/src/queuer.ts +29 -12
  61. package/src/rate-limiter.ts +71 -11
  62. package/src/throttler.ts +4 -4
@@ -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,7 +307,7 @@ 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({
@@ -322,11 +324,31 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
322
324
  /**
323
325
  * Processes the current pending execution immediately
324
326
  */
325
- flush = (): void => {
327
+ flush = async (): Promise<ReturnType<TFn> | undefined> => {
326
328
  if (this.store.state.isPending && this.store.state.lastArgs) {
327
329
  this.#abortExecution() // abort any current execution
328
330
  this.#clearTimeout() // clear any existing timeout
329
- this.#execute(...this.store.state.lastArgs)
331
+ const result = await this.#execute(...this.store.state.lastArgs)
332
+
333
+ // Resolve any pending promise from maybeExecute
334
+ this.#resolvePreviousPromiseInternal()
335
+
336
+ return result
337
+ }
338
+ return undefined
339
+ }
340
+
341
+ #resolvePreviousPromiseInternal = (): void => {
342
+ if (this.#resolvePreviousPromise) {
343
+ this.#resolvePreviousPromise(this.store.state.lastResult)
344
+ this.#resolvePreviousPromise = null
345
+ }
346
+ }
347
+
348
+ #rejectPreviousPromiseInternal = (error: unknown): void => {
349
+ if (this.#rejectPreviousPromise) {
350
+ this.#rejectPreviousPromise(error)
351
+ this.#rejectPreviousPromise = null
330
352
  }
331
353
  }
332
354
 
@@ -339,10 +361,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
339
361
 
340
362
  #cancelPendingExecution = (): void => {
341
363
  this.#clearTimeout()
342
- if (this.#resolvePreviousPromise) {
343
- this.#resolvePreviousPromise(this.store.state.lastResult)
344
- this.#resolvePreviousPromise = null
345
- }
364
+ this.#resolvePreviousPromiseInternal()
346
365
  this.#setState({
347
366
  isPending: false,
348
367
  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
  */
@@ -359,7 +359,7 @@ export class AsyncQueuer<TValue> {
359
359
  const activeItems = this.store.state.activeItems
360
360
  while (
361
361
  activeItems.length < this.#getConcurrency() &&
362
- !this.store.state.isEmpty
362
+ this.store.state.items.length > 0
363
363
  ) {
364
364
  const nextItem = this.peekNextItem()
365
365
  if (!nextItem) {
@@ -402,7 +402,7 @@ export class AsyncQueuer<TValue> {
402
402
  position: QueuePosition = this.options.addItemsTo ?? 'back',
403
403
  runOnItemsChange: boolean = true,
404
404
  ): boolean => {
405
- if (this.store.state.isFull) {
405
+ if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
406
406
  this.#setState({
407
407
  rejectionCount: this.store.state.rejectionCount + 1,
408
408
  })
@@ -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
  /**
@@ -578,7 +596,7 @@ export class AsyncQueuer<TValue> {
578
596
  const expiredIndices: Array<number> = []
579
597
 
580
598
  // Find indices of expired items
581
- for (let i = 0; i < this.store.state.size; i++) {
599
+ for (let i = 0; i < this.store.state.items.length; i++) {
582
600
  const timestamp = this.store.state.itemTimestamps[i]
583
601
  if (timestamp === undefined) continue
584
602
 
@@ -633,7 +651,7 @@ export class AsyncQueuer<TValue> {
633
651
  if (position === 'front') {
634
652
  return this.store.state.items[0]
635
653
  }
636
- return this.store.state.items[this.store.state.size - 1]
654
+ return this.store.state.items[this.store.state.items.length - 1]
637
655
  }
638
656
 
639
657
  /**
@@ -662,7 +680,7 @@ export class AsyncQueuer<TValue> {
662
680
  */
663
681
  start = (): void => {
664
682
  this.#setState({ isRunning: true })
665
- if (!this.store.state.pendingTick && !this.store.state.isEmpty) {
683
+ if (!this.store.state.pendingTick && this.store.state.items.length > 0) {
666
684
  this.#tick()
667
685
  }
668
686
  }
@@ -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
 
@@ -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()
@@ -353,12 +353,18 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
353
353
  /**
354
354
  * Processes the current pending execution immediately
355
355
  */
356
- flush = (): void => {
356
+ flush = async (): Promise<ReturnType<TFn> | undefined> => {
357
357
  if (this.store.state.isPending && this.store.state.lastArgs) {
358
358
  this.#abortExecution() // abort any current execution
359
359
  this.#clearTimeout() // clear any existing timeout
360
- this.#execute(...this.store.state.lastArgs)
360
+ const result = await this.#execute(...this.store.state.lastArgs)
361
+
362
+ // Resolve any pending promise from maybeExecute
363
+ this.#resolvePreviousPromiseInternal()
364
+
365
+ return result
361
366
  }
367
+ return undefined
362
368
  }
363
369
 
364
370
  #resolvePreviousPromiseInternal = (): void => {
@@ -368,6 +374,13 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
368
374
  }
369
375
  }
370
376
 
377
+ #rejectPreviousPromiseInternal = (error: unknown): void => {
378
+ if (this.#rejectPreviousPromise) {
379
+ this.#rejectPreviousPromise(error)
380
+ this.#rejectPreviousPromise = null
381
+ }
382
+ }
383
+
371
384
  #clearTimeout = (): void => {
372
385
  if (this.#timeoutId) {
373
386
  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/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
  */
@@ -331,7 +331,7 @@ export class Queuer<TValue> {
331
331
  // Check for expired items
332
332
  this.#checkExpiredItems()
333
333
 
334
- while (!this.store.state.isEmpty) {
334
+ while (this.store.state.items.length > 0) {
335
335
  const nextItem = this.execute(this.options.getItemsFrom ?? 'front')
336
336
  if (nextItem === undefined) {
337
337
  break
@@ -366,7 +366,7 @@ export class Queuer<TValue> {
366
366
  position: QueuePosition = this.options.addItemsTo ?? 'back',
367
367
  runOnItemsChange: boolean = true,
368
368
  ): boolean => {
369
- if (this.store.state.isFull) {
369
+ if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
370
370
  this.#setState({
371
371
  rejectionCount: this.store.state.rejectionCount + 1,
372
372
  })
@@ -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
  *
@@ -506,6 +512,17 @@ export class Queuer<TValue> {
506
512
  for (let i = 0; i < numberOfItems; i++) {
507
513
  this.execute(position)
508
514
  }
515
+ this.#tick()
516
+ }
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)
509
526
  }
510
527
 
511
528
  /**
@@ -579,7 +596,7 @@ export class Queuer<TValue> {
579
596
  if (position === 'front') {
580
597
  return this.store.state.items[0]
581
598
  }
582
- return this.store.state.items[this.store.state.size - 1]
599
+ return this.store.state.items[this.store.state.items.length - 1]
583
600
  }
584
601
 
585
602
  /**
@@ -594,7 +611,7 @@ export class Queuer<TValue> {
594
611
  */
595
612
  start = () => {
596
613
  this.#setState({ isRunning: true })
597
- if (!this.store.state.pendingTick && !this.store.state.isEmpty) {
614
+ if (!this.store.state.pendingTick && this.store.state.items.length > 0) {
598
615
  this.#tick()
599
616
  }
600
617
  }