@tanstack/pacer 0.7.0 → 0.9.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 (91) hide show
  1. package/dist/cjs/async-batcher.cjs +163 -0
  2. package/dist/cjs/async-batcher.cjs.map +1 -0
  3. package/dist/cjs/async-batcher.d.cts +273 -0
  4. package/dist/cjs/async-debouncer.cjs +149 -162
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +76 -57
  7. package/dist/cjs/async-queuer.cjs +282 -343
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +123 -102
  10. package/dist/cjs/async-rate-limiter.cjs +128 -185
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +72 -61
  13. package/dist/cjs/async-throttler.cjs +168 -178
  14. package/dist/cjs/async-throttler.cjs.map +1 -1
  15. package/dist/cjs/async-throttler.d.cts +97 -69
  16. package/dist/cjs/batcher.cjs +110 -119
  17. package/dist/cjs/batcher.cjs.map +1 -1
  18. package/dist/cjs/batcher.d.cts +76 -51
  19. package/dist/cjs/debouncer.cjs +97 -85
  20. package/dist/cjs/debouncer.cjs.map +1 -1
  21. package/dist/cjs/debouncer.d.cts +54 -26
  22. package/dist/cjs/index.cjs +3 -6
  23. package/dist/cjs/index.cjs.map +1 -1
  24. package/dist/cjs/index.d.cts +1 -1
  25. package/dist/cjs/queuer.cjs +246 -294
  26. package/dist/cjs/queuer.cjs.map +1 -1
  27. package/dist/cjs/queuer.d.cts +105 -84
  28. package/dist/cjs/rate-limiter.cjs +97 -130
  29. package/dist/cjs/rate-limiter.cjs.map +1 -1
  30. package/dist/cjs/rate-limiter.d.cts +50 -37
  31. package/dist/cjs/throttler.cjs +107 -123
  32. package/dist/cjs/throttler.cjs.map +1 -1
  33. package/dist/cjs/throttler.d.cts +59 -35
  34. package/dist/cjs/utils.cjs +0 -13
  35. package/dist/cjs/utils.cjs.map +1 -1
  36. package/dist/cjs/utils.d.cts +0 -1
  37. package/dist/esm/async-batcher.d.ts +273 -0
  38. package/dist/esm/async-batcher.js +163 -0
  39. package/dist/esm/async-batcher.js.map +1 -0
  40. package/dist/esm/async-debouncer.d.ts +76 -57
  41. package/dist/esm/async-debouncer.js +149 -162
  42. package/dist/esm/async-debouncer.js.map +1 -1
  43. package/dist/esm/async-queuer.d.ts +123 -102
  44. package/dist/esm/async-queuer.js +282 -343
  45. package/dist/esm/async-queuer.js.map +1 -1
  46. package/dist/esm/async-rate-limiter.d.ts +72 -61
  47. package/dist/esm/async-rate-limiter.js +128 -185
  48. package/dist/esm/async-rate-limiter.js.map +1 -1
  49. package/dist/esm/async-throttler.d.ts +97 -69
  50. package/dist/esm/async-throttler.js +168 -178
  51. package/dist/esm/async-throttler.js.map +1 -1
  52. package/dist/esm/batcher.d.ts +76 -51
  53. package/dist/esm/batcher.js +110 -119
  54. package/dist/esm/batcher.js.map +1 -1
  55. package/dist/esm/debouncer.d.ts +54 -26
  56. package/dist/esm/debouncer.js +97 -85
  57. package/dist/esm/debouncer.js.map +1 -1
  58. package/dist/esm/index.d.ts +1 -1
  59. package/dist/esm/index.js +4 -7
  60. package/dist/esm/queuer.d.ts +105 -84
  61. package/dist/esm/queuer.js +246 -294
  62. package/dist/esm/queuer.js.map +1 -1
  63. package/dist/esm/rate-limiter.d.ts +50 -37
  64. package/dist/esm/rate-limiter.js +97 -130
  65. package/dist/esm/rate-limiter.js.map +1 -1
  66. package/dist/esm/throttler.d.ts +59 -35
  67. package/dist/esm/throttler.js +107 -123
  68. package/dist/esm/throttler.js.map +1 -1
  69. package/dist/esm/utils.d.ts +0 -1
  70. package/dist/esm/utils.js +0 -13
  71. package/dist/esm/utils.js.map +1 -1
  72. package/package.json +14 -11
  73. package/src/async-batcher.ts +475 -0
  74. package/src/async-debouncer.ts +201 -121
  75. package/src/async-queuer.ts +341 -220
  76. package/src/async-rate-limiter.ts +176 -136
  77. package/src/async-throttler.ts +233 -139
  78. package/src/batcher.ts +159 -93
  79. package/src/debouncer.ts +135 -52
  80. package/src/index.ts +1 -1
  81. package/src/queuer.ts +349 -227
  82. package/src/rate-limiter.ts +125 -80
  83. package/src/throttler.ts +152 -78
  84. package/src/utils.ts +0 -15
  85. package/dist/cjs/compare.cjs +0 -72
  86. package/dist/cjs/compare.cjs.map +0 -1
  87. package/dist/cjs/compare.d.cts +0 -12
  88. package/dist/esm/compare.d.ts +0 -12
  89. package/dist/esm/compare.js +0 -72
  90. package/dist/esm/compare.js.map +0 -1
  91. package/src/compare.ts +0 -105
@@ -1,7 +1,96 @@
1
+ import { Store } from '@tanstack/store'
1
2
  import { parseFunctionOrValue } from './utils'
2
3
  import type { OptionalKeys } from './types'
3
4
  import type { QueuePosition } from './queuer'
4
5
 
6
+ export interface AsyncQueuerState<TValue> {
7
+ /**
8
+ * Items currently being processed by the queuer
9
+ */
10
+ activeItems: Array<TValue>
11
+ /**
12
+ * Number of task executions that have resulted in errors
13
+ */
14
+ errorCount: number
15
+ /**
16
+ * Number of items that have been removed from the queue due to expiration
17
+ */
18
+ expirationCount: number
19
+ /**
20
+ * Whether the queuer has no items to process (items array is empty)
21
+ */
22
+ isEmpty: boolean
23
+ /**
24
+ * Whether the queuer has reached its maximum capacity
25
+ */
26
+ isFull: boolean
27
+ /**
28
+ * Whether the queuer is not currently processing any items
29
+ */
30
+ isIdle: boolean
31
+ /**
32
+ * Whether the queuer is active and will process items automatically
33
+ */
34
+ isRunning: boolean
35
+ /**
36
+ * Timestamps when items were added to the queue for expiration tracking
37
+ */
38
+ itemTimestamps: Array<number>
39
+ /**
40
+ * Array of items currently waiting to be processed
41
+ */
42
+ items: Array<TValue>
43
+ /**
44
+ * The result from the most recent task execution
45
+ */
46
+ lastResult: any
47
+ /**
48
+ * Whether the queuer has a pending timeout for processing the next item
49
+ */
50
+ pendingTick: boolean
51
+ /**
52
+ * Number of items that have been rejected from being added to the queue
53
+ */
54
+ rejectionCount: number
55
+ /**
56
+ * Number of task executions that have completed (either successfully or with errors)
57
+ */
58
+ settledCount: number
59
+ /**
60
+ * Number of items currently in the queue
61
+ */
62
+ size: number
63
+ /**
64
+ * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused
65
+ */
66
+ status: 'idle' | 'running' | 'stopped'
67
+ /**
68
+ * Number of task executions that have completed successfully
69
+ */
70
+ successCount: number
71
+ }
72
+
73
+ function getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {
74
+ return structuredClone({
75
+ activeItems: [],
76
+ errorCount: 0,
77
+ expirationCount: 0,
78
+ isEmpty: true,
79
+ isFull: false,
80
+ isIdle: true,
81
+ isRunning: true,
82
+ itemTimestamps: [],
83
+ items: [],
84
+ lastResult: null,
85
+ pendingTick: false,
86
+ rejectionCount: 0,
87
+ settledCount: 0,
88
+ size: 0,
89
+ status: 'idle',
90
+ successCount: 0,
91
+ })
92
+ }
93
+
5
94
  export interface AsyncQueuerOptions<TValue> {
6
95
  /**
7
96
  * Default position to add items to the queuer
@@ -39,6 +128,10 @@ export interface AsyncQueuerOptions<TValue> {
39
128
  * Initial items to populate the queuer with
40
129
  */
41
130
  initialItems?: Array<TValue>
131
+ /**
132
+ * Initial state for the async queuer
133
+ */
134
+ initialState?: Partial<AsyncQueuerState<TValue>>
42
135
  /**
43
136
  * Maximum number of items allowed in the queuer
44
137
  */
@@ -53,10 +146,6 @@ export interface AsyncQueuerOptions<TValue> {
53
146
  * Callback fired whenever an item expires in the queuer
54
147
  */
55
148
  onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void
56
- /**
57
- * Callback fired whenever the queuer's running state changes
58
- */
59
- onIsRunningChange?: (queuer: AsyncQueuer<TValue>) => void
60
149
  /**
61
150
  * Callback fired whenever an item is added or removed from the queuer
62
151
  */
@@ -93,12 +182,12 @@ export interface AsyncQueuerOptions<TValue> {
93
182
 
94
183
  type AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<
95
184
  Required<AsyncQueuerOptions<any>>,
185
+ | 'initialState'
96
186
  | 'throwOnError'
97
187
  | 'onSuccess'
98
188
  | 'onSettled'
99
189
  | 'onReject'
100
190
  | 'onItemsChange'
101
- | 'onIsRunningChange'
102
191
  | 'onExpire'
103
192
  | 'onError'
104
193
  >
@@ -138,6 +227,19 @@ const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {
138
227
  * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown
139
228
  * - The error state can be checked using the AsyncQueuer instance
140
229
  *
230
+ * State Management:
231
+ * - Uses TanStack Store for reactive state management
232
+ * - Use `initialState` to provide initial state values when creating the async queuer
233
+ * - Use `onSuccess` callback to react to successful task execution and implement custom logic
234
+ * - Use `onError` callback to react to task execution errors and implement custom error handling
235
+ * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic
236
+ * - Use `onItemsChange` callback to react to items being added or removed from the queue
237
+ * - Use `onExpire` callback to react to items expiring and implement custom logic
238
+ * - Use `onReject` callback to react to items being rejected when the queue is full
239
+ * - The state includes error count, expiration count, rejection count, running status, and success/settle counts
240
+ * - State can be accessed via `asyncQueuer.store.state` when using the class directly
241
+ * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`
242
+ *
141
243
  * Example usage:
142
244
  * ```ts
143
245
  * const asyncQueuer = new AsyncQueuer<string>(async (item) => {
@@ -155,148 +257,134 @@ const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {
155
257
  * ```
156
258
  */
157
259
  export class AsyncQueuer<TValue> {
158
- private _options: AsyncQueuerOptionsWithOptionalCallbacks
159
- private _activeItems: Set<TValue> = new Set()
160
- private _successCount = 0
161
- private _errorCount = 0
162
- private _settledCount = 0
163
- private _rejectionCount = 0
164
- private _expirationCount = 0
165
- private _items: Array<TValue> = []
166
- private _itemTimestamps: Array<number> = []
167
- private _pendingTick = false
168
- private _running: boolean
169
- private _lastResult: any
260
+ readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<
261
+ AsyncQueuerState<TValue>
262
+ >(getDefaultAsyncQueuerState<TValue>())
263
+ options: AsyncQueuerOptions<TValue>
264
+ #timeoutIds: Set<NodeJS.Timeout> = new Set()
170
265
 
171
266
  constructor(
172
- private fn: (value: TValue) => Promise<any>,
173
- initialOptions: AsyncQueuerOptions<TValue>,
267
+ private fn: (item: TValue) => Promise<any>,
268
+ initialOptions: AsyncQueuerOptions<TValue> = {},
174
269
  ) {
175
- this._options = {
270
+ this.options = {
176
271
  ...defaultOptions,
177
272
  ...initialOptions,
178
273
  throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
179
274
  }
180
- this._running = this._options.started
181
-
182
- for (let i = 0; i < this._options.initialItems.length; i++) {
183
- const item = this._options.initialItems[i]!
184
- const isLast = i === this._options.initialItems.length - 1
185
- this.addItem(item, this._options.addItemsTo, isLast)
275
+ const isInitiallyRunning =
276
+ this.options.initialState?.isRunning ?? this.options.started ?? true
277
+ this.#setState({
278
+ ...this.options.initialState,
279
+ isRunning: isInitiallyRunning,
280
+ })
281
+
282
+ if (this.options.initialState?.items) {
283
+ if (this.store.state.isRunning) {
284
+ this.#tick()
285
+ }
286
+ } else {
287
+ for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {
288
+ const item = this.options.initialItems![i]!
289
+ const isLast = i === (this.options.initialItems?.length ?? 0) - 1
290
+ this.addItem(item, this.options.addItemsTo ?? 'back', isLast)
291
+ }
186
292
  }
187
293
  }
188
294
 
189
295
  /**
190
296
  * Updates the queuer options. New options are merged with existing options.
191
297
  */
192
- setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void {
193
- this._options = { ...this._options, ...newOptions }
298
+ setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {
299
+ this.options = { ...this.options, ...newOptions }
194
300
  }
195
301
 
196
- /**
197
- * Returns the current queuer options, including defaults and any overrides.
198
- */
199
- getOptions(): AsyncQueuerOptions<TValue> {
200
- return this._options
302
+ #setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {
303
+ this.store.setState((state) => {
304
+ const combinedState = {
305
+ ...state,
306
+ ...newState,
307
+ }
308
+
309
+ const { activeItems, items, isRunning } = combinedState
310
+
311
+ const size = items.length
312
+ const isFull = size >= (this.options.maxSize ?? Infinity)
313
+ const isEmpty = size === 0
314
+ const isIdle = isRunning && isEmpty && activeItems.length === 0
315
+
316
+ const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'
317
+
318
+ return {
319
+ ...combinedState,
320
+ isEmpty,
321
+ isFull,
322
+ isIdle,
323
+ size,
324
+ status,
325
+ }
326
+ })
201
327
  }
202
328
 
203
329
  /**
204
330
  * Returns the current wait time (in milliseconds) between processing items.
205
331
  * If a function is provided, it is called with the queuer instance.
206
332
  */
207
- getWait(): number {
208
- return parseFunctionOrValue(this._options.wait, this)
333
+ #getWait = (): number => {
334
+ return parseFunctionOrValue(this.options.wait ?? 0, this)
209
335
  }
210
336
 
211
337
  /**
212
338
  * Returns the current concurrency limit for processing items.
213
339
  * If a function is provided, it is called with the queuer instance.
214
340
  */
215
- getConcurrency(): number {
216
- return parseFunctionOrValue(this._options.concurrency, this)
341
+ #getConcurrency = (): number => {
342
+ return parseFunctionOrValue(this.options.concurrency ?? 1, this)
217
343
  }
218
344
 
219
345
  /**
220
346
  * Processes items in the queue up to the concurrency limit. Internal use only.
221
347
  */
222
- private tick() {
223
- if (!this._running) {
224
- this._pendingTick = false
348
+ #tick = () => {
349
+ if (!this.store.state.isRunning) {
350
+ this.#setState({ pendingTick: false })
225
351
  return
226
352
  }
353
+ this.#setState({ pendingTick: true })
227
354
 
228
355
  // Check for expired items
229
- this.checkExpiredItems()
356
+ this.#checkExpiredItems()
230
357
 
231
358
  // Process items concurrently up to the concurrency limit
359
+ const activeItems = this.store.state.activeItems
232
360
  while (
233
- this._activeItems.size < this.getConcurrency() &&
234
- !this.getIsEmpty()
361
+ activeItems.length < this.#getConcurrency() &&
362
+ !this.store.state.isEmpty
235
363
  ) {
236
- const nextItem = this.getPeek()
364
+ const nextItem = this.peekNextItem()
237
365
  if (!nextItem) {
238
366
  break
239
367
  }
240
- this._activeItems.add(nextItem)
241
- this._options.onItemsChange?.(this)
368
+ activeItems.push(nextItem)
369
+ this.#setState({
370
+ activeItems,
371
+ })
242
372
  ;(async () => {
243
- this._lastResult = await this.execute()
373
+ const result = await this.execute()
374
+ this.#setState({ lastResult: result })
244
375
 
245
- const wait = this.getWait()
376
+ const wait = this.#getWait()
246
377
  if (wait > 0) {
247
- setTimeout(() => this.tick(), wait)
378
+ const timeoutId = setTimeout(() => this.#tick(), wait)
379
+ this.#timeoutIds.add(timeoutId)
248
380
  return
249
381
  }
250
382
 
251
- this.tick()
383
+ this.#tick()
252
384
  })()
253
385
  }
254
386
 
255
- this._pendingTick = false
256
- }
257
-
258
- /**
259
- * Starts processing items in the queue. If already running, does nothing.
260
- */
261
- start(): void {
262
- this._running = true
263
- if (!this._pendingTick && !this.getIsEmpty()) {
264
- this._pendingTick = true
265
- this.tick()
266
- }
267
- this._options.onIsRunningChange?.(this)
268
- }
269
-
270
- /**
271
- * Stops processing items in the queue. Does not clear the queue.
272
- */
273
- stop(): void {
274
- this._running = false
275
- this._pendingTick = false
276
- this._options.onIsRunningChange?.(this)
277
- }
278
-
279
- /**
280
- * Removes all pending items from the queue. Does not affect active tasks.
281
- */
282
- clear(): void {
283
- this._items = []
284
- this._options.onItemsChange?.(this)
285
- }
286
-
287
- /**
288
- * Resets the queuer to its initial state. Optionally repopulates with initial items.
289
- * Does not affect callbacks or options.
290
- */
291
- reset(withInitialItems?: boolean): void {
292
- this.clear()
293
- this._successCount = 0
294
- this._errorCount = 0
295
- this._settledCount = 0
296
- if (withInitialItems) {
297
- this._items = [...this._options.initialItems]
298
- }
299
- this._running = this._options.started
387
+ this.#setState({ pendingTick: false })
300
388
  }
301
389
 
302
390
  /**
@@ -309,60 +397,71 @@ export class AsyncQueuer<TValue> {
309
397
  * queuer.addItem('task2', 'front');
310
398
  * ```
311
399
  */
312
- addItem(
313
- item: TValue & { priority?: number },
314
- position: QueuePosition = this._options.addItemsTo,
400
+ addItem = (
401
+ item: TValue,
402
+ position: QueuePosition = this.options.addItemsTo ?? 'back',
315
403
  runOnItemsChange: boolean = true,
316
- ): void {
317
- if (this.getIsFull()) {
318
- this._rejectionCount++
319
- this._options.onReject?.(item, this)
320
- return
404
+ ): boolean => {
405
+ if (this.store.state.isFull) {
406
+ this.#setState({
407
+ rejectionCount: this.store.state.rejectionCount + 1,
408
+ })
409
+ this.options.onReject?.(item, this)
410
+ return false
321
411
  }
322
412
 
323
413
  // Get priority either from the function or from getPriority option
324
414
  const priority =
325
- this._options.getPriority !== defaultOptions.getPriority
326
- ? this._options.getPriority(item)
327
- : item.priority
415
+ this.options.getPriority !== defaultOptions.getPriority
416
+ ? this.options.getPriority!(item)
417
+ : (item as any).priority
418
+
419
+ const items = this.store.state.items
420
+ const itemTimestamps = this.store.state.itemTimestamps
328
421
 
329
422
  if (priority !== undefined) {
330
423
  // Insert based on priority - higher priority items go to front
331
- const insertIndex = this._items.findIndex((existing) => {
424
+ const insertIndex = items.findIndex((existing) => {
332
425
  const existingPriority =
333
- this._options.getPriority !== defaultOptions.getPriority
334
- ? this._options.getPriority(existing)
426
+ this.options.getPriority !== defaultOptions.getPriority
427
+ ? this.options.getPriority!(existing)
335
428
  : (existing as any).priority
336
429
  return existingPriority < priority
337
430
  })
338
431
 
339
432
  if (insertIndex === -1) {
340
- this._items.push(item)
341
- this._itemTimestamps.push(Date.now())
433
+ items.push(item)
434
+ itemTimestamps.push(Date.now())
342
435
  } else {
343
- this._items.splice(insertIndex, 0, item)
344
- this._itemTimestamps.splice(insertIndex, 0, Date.now())
436
+ items.splice(insertIndex, 0, item)
437
+ itemTimestamps.splice(insertIndex, 0, Date.now())
345
438
  }
346
439
  } else {
347
440
  if (position === 'front') {
348
441
  // Default FIFO/LIFO behavior
349
- this._items.unshift(item)
350
- this._itemTimestamps.unshift(Date.now())
442
+ items.unshift(item)
443
+ itemTimestamps.unshift(Date.now())
351
444
  } else {
352
445
  // LIFO
353
- this._items.push(item)
354
- this._itemTimestamps.push(Date.now())
446
+ items.push(item)
447
+ itemTimestamps.push(Date.now())
355
448
  }
356
449
  }
357
450
 
451
+ this.#setState({
452
+ items,
453
+ itemTimestamps,
454
+ })
455
+
358
456
  if (runOnItemsChange) {
359
- this._options.onItemsChange?.(this)
457
+ this.options.onItemsChange?.(this)
360
458
  }
361
459
 
362
- if (this._running && !this._pendingTick) {
363
- this._pendingTick = true
364
- this.tick()
460
+ if (this.store.state.isRunning && !this.store.state.pendingTick) {
461
+ this.#tick()
365
462
  }
463
+
464
+ return true
366
465
  }
367
466
 
368
467
  /**
@@ -377,21 +476,32 @@ export class AsyncQueuer<TValue> {
377
476
  * queuer.getNextItem('back');
378
477
  * ```
379
478
  */
380
- getNextItem(
381
- position: QueuePosition = this._options.getItemsFrom,
382
- ): TValue | undefined {
479
+ getNextItem = (
480
+ position: QueuePosition = this.options.getItemsFrom ?? 'front',
481
+ ): TValue | undefined => {
482
+ const { items, itemTimestamps } = this.store.state
383
483
  let item: TValue | undefined
384
484
 
385
485
  if (position === 'front') {
386
- item = this._items.shift()
387
- this._itemTimestamps.shift()
486
+ item = items[0]
487
+ if (item !== undefined) {
488
+ this.#setState({
489
+ items: items.slice(1),
490
+ itemTimestamps: itemTimestamps.slice(1),
491
+ })
492
+ }
388
493
  } else {
389
- item = this._items.pop()
390
- this._itemTimestamps.pop()
494
+ item = items[items.length - 1]
495
+ if (item !== undefined) {
496
+ this.#setState({
497
+ items: items.slice(0, -1),
498
+ itemTimestamps: itemTimestamps.slice(0, -1),
499
+ })
500
+ }
391
501
  }
392
502
 
393
503
  if (item !== undefined) {
394
- this._options.onItemsChange?.(this)
504
+ this.options.onItemsChange?.(this)
395
505
  }
396
506
 
397
507
  return item
@@ -407,55 +517,78 @@ export class AsyncQueuer<TValue> {
407
517
  * queuer.execute('back');
408
518
  * ```
409
519
  */
410
- async execute(position?: QueuePosition): Promise<any> {
520
+ execute = async (position?: QueuePosition): Promise<any> => {
411
521
  const item = this.getNextItem(position)
412
522
  if (item !== undefined) {
413
523
  try {
414
- this._lastResult = await this.fn(item)
415
- this._successCount++
416
- this._options.onSuccess?.(this._lastResult, this)
524
+ const lastResult = await this.fn(item)
525
+ this.#setState({
526
+ successCount: this.store.state.successCount + 1,
527
+ lastResult,
528
+ })
529
+ this.options.onSuccess?.(lastResult, this)
417
530
  } catch (error) {
418
- this._errorCount++
419
- this._options.onError?.(error, this)
420
- if (this._options.throwOnError) {
531
+ this.#setState({
532
+ errorCount: this.store.state.errorCount + 1,
533
+ })
534
+ this.options.onError?.(error, this)
535
+ if (this.options.throwOnError) {
421
536
  throw error
422
537
  }
423
538
  } finally {
424
- this._settledCount++
425
- this._activeItems.delete(item)
426
- this._options.onItemsChange?.(this)
427
- this._options.onSettled?.(this)
539
+ this.#setState({
540
+ activeItems: this.store.state.activeItems.filter(
541
+ (activeItem) => activeItem !== item,
542
+ ),
543
+ settledCount: this.store.state.settledCount + 1,
544
+ })
545
+ this.options.onSettled?.(this)
428
546
  }
429
547
  }
430
548
  return item
431
549
  }
432
550
 
551
+ /**
552
+ * Processes a specified number of items to execute immediately with no wait time
553
+ * If no numberOfItems is provided, all items will be processed
554
+ */
555
+ flush = (
556
+ numberOfItems: number = this.store.state.items.length,
557
+ position?: QueuePosition,
558
+ ): void => {
559
+ this.#clearTimeouts() // clear any pending timeouts
560
+ for (let i = 0; i < numberOfItems; i++) {
561
+ this.execute(position)
562
+ }
563
+ }
564
+
433
565
  /**
434
566
  * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
435
567
  * Internal use only.
436
568
  */
437
- private checkExpiredItems(): void {
569
+ #checkExpiredItems = (): void => {
438
570
  if (
439
- this._options.expirationDuration === Infinity &&
440
- this._options.getIsExpired === defaultOptions.getIsExpired
441
- )
571
+ (this.options.expirationDuration ?? Infinity) === Infinity &&
572
+ this.options.getIsExpired === defaultOptions.getIsExpired
573
+ ) {
442
574
  return
575
+ }
443
576
 
444
577
  const now = Date.now()
445
578
  const expiredIndices: Array<number> = []
446
579
 
447
580
  // Find indices of expired items
448
- for (let i = 0; i < this._items.length; i++) {
449
- const timestamp = this._itemTimestamps[i]
581
+ for (let i = 0; i < this.store.state.size; i++) {
582
+ const timestamp = this.store.state.itemTimestamps[i]
450
583
  if (timestamp === undefined) continue
451
584
 
452
- const item = this._items[i]
585
+ const item = this.store.state.items[i]
453
586
  if (item === undefined) continue
454
587
 
455
588
  const isExpired =
456
- this._options.getIsExpired !== defaultOptions.getIsExpired
457
- ? this._options.getIsExpired(item, timestamp)
458
- : now - timestamp > this._options.expirationDuration
589
+ this.options.getIsExpired !== defaultOptions.getIsExpired
590
+ ? this.options.getIsExpired!(item, timestamp)
591
+ : now - timestamp > (this.options.expirationDuration ?? Infinity)
459
592
 
460
593
  if (isExpired) {
461
594
  expiredIndices.push(i)
@@ -467,17 +600,23 @@ export class AsyncQueuer<TValue> {
467
600
  const index = expiredIndices[i]
468
601
  if (index === undefined) continue
469
602
 
470
- const expiredItem = this._items[index]
603
+ const expiredItem = this.store.state.items[index]
471
604
  if (expiredItem === undefined) continue
472
605
 
473
- this._items.splice(index, 1)
474
- this._itemTimestamps.splice(index, 1)
475
- this._expirationCount++
476
- this._options.onExpire?.(expiredItem, this)
606
+ const newItems = [...this.store.state.items]
607
+ const newTimestamps = [...this.store.state.itemTimestamps]
608
+ newItems.splice(index, 1)
609
+ newTimestamps.splice(index, 1)
610
+ this.#setState({
611
+ items: newItems,
612
+ itemTimestamps: newTimestamps,
613
+ expirationCount: this.store.state.expirationCount + 1,
614
+ })
615
+ this.options.onExpire?.(expiredItem, this)
477
616
  }
478
617
 
479
618
  if (expiredIndices.length > 0) {
480
- this._options.onItemsChange?.(this)
619
+ this.options.onItemsChange?.(this)
481
620
  }
482
621
  }
483
622
 
@@ -486,106 +625,75 @@ export class AsyncQueuer<TValue> {
486
625
  *
487
626
  * @example
488
627
  * ```ts
489
- * queuer.getPeek(); // front
490
- * queuer.getPeek('back'); // back
628
+ * queuer.peekNextItem(); // front
629
+ * queuer.peekNextItem('back'); // back
491
630
  * ```
492
631
  */
493
- getPeek(position: QueuePosition = 'front'): TValue | undefined {
632
+ peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {
494
633
  if (position === 'front') {
495
- return this._items[0]
634
+ return this.store.state.items[0]
496
635
  }
497
- return this._items[this._items.length - 1]
498
- }
499
-
500
- /**
501
- * Returns true if the queue is empty (no pending items).
502
- */
503
- getIsEmpty(): boolean {
504
- return this._items.length === 0
505
- }
506
-
507
- /**
508
- * Returns true if the queue is full (reached maxSize).
509
- */
510
- getIsFull(): boolean {
511
- return this._items.length >= this._options.maxSize
512
- }
513
-
514
- /**
515
- * Returns the number of pending items in the queue.
516
- */
517
- getSize(): number {
518
- return this._items.length
636
+ return this.store.state.items[this.store.state.size - 1]
519
637
  }
520
638
 
521
639
  /**
522
640
  * Returns a copy of all items in the queue, including active and pending items.
523
641
  */
524
- getAllItems(): Array<TValue> {
525
- return [...this.getActiveItems(), ...this.getPendingItems()]
642
+ peekAllItems = (): Array<TValue> => {
643
+ return [...this.peekActiveItems(), ...this.peekPendingItems()]
526
644
  }
527
645
 
528
646
  /**
529
647
  * Returns the items currently being processed (active tasks).
530
648
  */
531
- getActiveItems(): Array<TValue> {
532
- return Array.from(this._activeItems)
649
+ peekActiveItems = (): Array<TValue> => {
650
+ return [...this.store.state.activeItems]
533
651
  }
534
652
 
535
653
  /**
536
654
  * Returns the items waiting to be processed (pending tasks).
537
655
  */
538
- getPendingItems(): Array<TValue> {
539
- return [...this._items]
656
+ peekPendingItems = (): Array<TValue> => {
657
+ return [...this.store.state.items]
540
658
  }
541
659
 
542
660
  /**
543
- * Returns the number of items that have been successfully processed.
544
- */
545
- getSuccessCount(): number {
546
- return this._successCount
547
- }
548
-
549
- /**
550
- * Returns the number of items that have failed processing.
551
- */
552
- getErrorCount(): number {
553
- return this._errorCount
554
- }
555
-
556
- /**
557
- * Returns the number of items that have completed processing (success or error).
661
+ * Starts processing items in the queue. If already running, does nothing.
558
662
  */
559
- getSettledCount(): number {
560
- return this._settledCount
663
+ start = (): void => {
664
+ this.#setState({ isRunning: true })
665
+ if (!this.store.state.pendingTick && !this.store.state.isEmpty) {
666
+ this.#tick()
667
+ }
561
668
  }
562
669
 
563
670
  /**
564
- * Returns the number of items that have been rejected from being added to the queue.
671
+ * Stops processing items in the queue. Does not clear the queue.
565
672
  */
566
- getRejectionCount(): number {
567
- return this._rejectionCount
673
+ stop = (): void => {
674
+ this.#clearTimeouts()
675
+ this.#setState({ isRunning: false, pendingTick: false })
568
676
  }
569
677
 
570
- /**
571
- * Returns true if the queuer is currently running (processing items).
572
- */
573
- getIsRunning(): boolean {
574
- return this._running
678
+ #clearTimeouts = (): void => {
679
+ this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))
680
+ this.#timeoutIds.clear()
575
681
  }
576
682
 
577
683
  /**
578
- * Returns true if the queuer is running but has no items to process and no active tasks.
684
+ * Removes all pending items from the queue. Does not affect active tasks.
579
685
  */
580
- getIsIdle(): boolean {
581
- return this._running && this.getIsEmpty() && this._activeItems.size === 0
686
+ clear = (): void => {
687
+ this.#setState({ items: [], itemTimestamps: [] })
688
+ this.options.onItemsChange?.(this)
582
689
  }
583
690
 
584
691
  /**
585
- * Returns the number of items that have expired and been removed from the queue.
692
+ * Resets the queuer state to its default values
586
693
  */
587
- getExpirationCount(): number {
588
- return this._expirationCount
694
+ reset = (): void => {
695
+ this.#setState(getDefaultAsyncQueuerState<TValue>())
696
+ this.options.onItemsChange?.(this)
589
697
  }
590
698
  }
591
699
 
@@ -600,6 +708,19 @@ export class AsyncQueuer<TValue> {
600
708
  * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown
601
709
  * - The error state can be checked using the underlying AsyncQueuer instance
602
710
  *
711
+ * State Management:
712
+ * - Uses TanStack Store for reactive state management
713
+ * - Use `initialState` to provide initial state values when creating the async queuer
714
+ * - Use `onSuccess` callback to react to successful task execution and implement custom logic
715
+ * - Use `onError` callback to react to task execution errors and implement custom error handling
716
+ * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic
717
+ * - Use `onItemsChange` callback to react to items being added or removed from the queue
718
+ * - Use `onExpire` callback to react to items expiring and implement custom logic
719
+ * - Use `onReject` callback to react to items being rejected when the queue is full
720
+ * - The state includes error count, expiration count, rejection count, running status, and success/settle counts
721
+ * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property
722
+ * - When using framework adapters (React/Solid), state is accessed from the hook's state property
723
+ *
603
724
  * Example usage:
604
725
  * ```ts
605
726
  * const enqueue = asyncQueue<string>(async (item) => {
@@ -614,5 +735,5 @@ export function asyncQueue<TValue>(
614
735
  initialOptions: AsyncQueuerOptions<TValue>,
615
736
  ) {
616
737
  const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)
617
- return asyncQueuer.addItem.bind(asyncQueuer)
738
+ return asyncQueuer.addItem
618
739
  }