@tanstack/pacer 0.6.0 → 0.7.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 (71) hide show
  1. package/dist/cjs/async-debouncer.cjs +10 -6
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +2 -2
  4. package/dist/cjs/async-queuer.cjs +135 -106
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +121 -93
  7. package/dist/cjs/async-rate-limiter.cjs +3 -4
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +1 -2
  10. package/dist/cjs/async-throttler.cjs +14 -4
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +3 -2
  13. package/dist/cjs/batcher.cjs +138 -0
  14. package/dist/cjs/batcher.cjs.map +1 -0
  15. package/dist/cjs/batcher.d.cts +149 -0
  16. package/dist/cjs/debouncer.cjs +3 -4
  17. package/dist/cjs/debouncer.cjs.map +1 -1
  18. package/dist/cjs/debouncer.d.cts +1 -2
  19. package/dist/cjs/index.cjs +3 -0
  20. package/dist/cjs/index.cjs.map +1 -1
  21. package/dist/cjs/index.d.cts +1 -0
  22. package/dist/cjs/queuer.cjs +68 -41
  23. package/dist/cjs/queuer.cjs.map +1 -1
  24. package/dist/cjs/queuer.d.cts +123 -92
  25. package/dist/cjs/rate-limiter.cjs +3 -4
  26. package/dist/cjs/rate-limiter.cjs.map +1 -1
  27. package/dist/cjs/rate-limiter.d.cts +1 -2
  28. package/dist/cjs/throttler.cjs +3 -4
  29. package/dist/cjs/throttler.cjs.map +1 -1
  30. package/dist/cjs/throttler.d.cts +1 -2
  31. package/dist/esm/async-debouncer.d.ts +2 -2
  32. package/dist/esm/async-debouncer.js +10 -6
  33. package/dist/esm/async-debouncer.js.map +1 -1
  34. package/dist/esm/async-queuer.d.ts +121 -93
  35. package/dist/esm/async-queuer.js +135 -106
  36. package/dist/esm/async-queuer.js.map +1 -1
  37. package/dist/esm/async-rate-limiter.d.ts +1 -2
  38. package/dist/esm/async-rate-limiter.js +3 -4
  39. package/dist/esm/async-rate-limiter.js.map +1 -1
  40. package/dist/esm/async-throttler.d.ts +3 -2
  41. package/dist/esm/async-throttler.js +14 -4
  42. package/dist/esm/async-throttler.js.map +1 -1
  43. package/dist/esm/batcher.d.ts +149 -0
  44. package/dist/esm/batcher.js +138 -0
  45. package/dist/esm/batcher.js.map +1 -0
  46. package/dist/esm/debouncer.d.ts +1 -2
  47. package/dist/esm/debouncer.js +3 -4
  48. package/dist/esm/debouncer.js.map +1 -1
  49. package/dist/esm/index.d.ts +1 -0
  50. package/dist/esm/index.js +3 -0
  51. package/dist/esm/index.js.map +1 -1
  52. package/dist/esm/queuer.d.ts +123 -92
  53. package/dist/esm/queuer.js +68 -41
  54. package/dist/esm/queuer.js.map +1 -1
  55. package/dist/esm/rate-limiter.d.ts +1 -2
  56. package/dist/esm/rate-limiter.js +3 -4
  57. package/dist/esm/rate-limiter.js.map +1 -1
  58. package/dist/esm/throttler.d.ts +1 -2
  59. package/dist/esm/throttler.js +3 -4
  60. package/dist/esm/throttler.js.map +1 -1
  61. package/package.json +11 -1
  62. package/src/async-debouncer.ts +12 -6
  63. package/src/async-queuer.ts +216 -193
  64. package/src/async-rate-limiter.ts +3 -4
  65. package/src/async-throttler.ts +18 -4
  66. package/src/batcher.ts +253 -0
  67. package/src/debouncer.ts +3 -4
  68. package/src/index.ts +1 -0
  69. package/src/queuer.ts +142 -98
  70. package/src/rate-limiter.ts +3 -4
  71. package/src/throttler.ts +3 -4
@@ -1,10 +1,8 @@
1
1
  import { parseFunctionOrValue } from './utils'
2
- import type { AnyAsyncFunction, OptionalKeys } from './types'
2
+ import type { OptionalKeys } from './types'
3
3
  import type { QueuePosition } from './queuer'
4
4
 
5
- export type AsyncQueuerFn = AnyAsyncFunction & { priority?: number }
6
-
7
- export interface AsyncQueuerOptions<TFn extends AsyncQueuerFn> {
5
+ export interface AsyncQueuerOptions<TValue> {
8
6
  /**
9
7
  * Default position to add items to the queuer
10
8
  * @default 'back'
@@ -15,7 +13,7 @@ export interface AsyncQueuerOptions<TFn extends AsyncQueuerFn> {
15
13
  * Can be a number or a function that returns a number.
16
14
  * @default 1
17
15
  */
18
- concurrency?: number | ((queuer: AsyncQueuer<TFn>) => number)
16
+ concurrency?: number | ((queuer: AsyncQueuer<TValue>) => number)
19
17
  /**
20
18
  * Maximum time in milliseconds that an item can stay in the queue
21
19
  * If not provided, items will never expire
@@ -25,7 +23,7 @@ export interface AsyncQueuerOptions<TFn extends AsyncQueuerFn> {
25
23
  * Function to determine if an item has expired
26
24
  * If provided, this overrides the expirationDuration behavior
27
25
  */
28
- getIsExpired?: (item: TFn, addedAt: number) => boolean
26
+ getIsExpired?: (item: TValue, addedAt: number) => boolean
29
27
  /**
30
28
  * Default position to get items from during processing
31
29
  * @default 'front'
@@ -36,11 +34,11 @@ export interface AsyncQueuerOptions<TFn extends AsyncQueuerFn> {
36
34
  * Higher priority items will be processed first
37
35
  * If not provided, will use static priority values attached to tasks
38
36
  */
39
- getPriority?: (item: TFn) => number
37
+ getPriority?: (item: TValue) => number
40
38
  /**
41
39
  * Initial items to populate the queuer with
42
40
  */
43
- initialItems?: Array<TFn & { priority?: number }>
41
+ initialItems?: Array<TValue>
44
42
  /**
45
43
  * Maximum number of items allowed in the queuer
46
44
  */
@@ -50,35 +48,31 @@ export interface AsyncQueuerOptions<TFn extends AsyncQueuerFn> {
50
48
  * If provided, the handler will be called with the error and queuer instance.
51
49
  * This can be used alongside throwOnError - the handler will be called before any error is thrown.
52
50
  */
53
- onError?: (error: unknown, queuer: AsyncQueuer<TFn>) => void
51
+ onError?: (error: unknown, queuer: AsyncQueuer<TValue>) => void
54
52
  /**
55
53
  * Callback fired whenever an item expires in the queuer
56
54
  */
57
- onExpire?: (item: TFn, queuer: AsyncQueuer<TFn>) => void
58
- /**
59
- * Callback fired whenever an item is removed from the queuer
60
- */
61
- onGetNextItem?: (item: TFn, queuer: AsyncQueuer<TFn>) => void
55
+ onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void
62
56
  /**
63
57
  * Callback fired whenever the queuer's running state changes
64
58
  */
65
- onIsRunningChange?: (queuer: AsyncQueuer<TFn>) => void
59
+ onIsRunningChange?: (queuer: AsyncQueuer<TValue>) => void
66
60
  /**
67
61
  * Callback fired whenever an item is added or removed from the queuer
68
62
  */
69
- onItemsChange?: (queuer: AsyncQueuer<TFn>) => void
63
+ onItemsChange?: (queuer: AsyncQueuer<TValue>) => void
70
64
  /**
71
65
  * Callback fired whenever an item is rejected from being added to the queuer
72
66
  */
73
- onReject?: (item: TFn, queuer: AsyncQueuer<TFn>) => void
67
+ onReject?: (item: TValue, queuer: AsyncQueuer<TValue>) => void
74
68
  /**
75
69
  * Optional callback to call when a task is settled
76
70
  */
77
- onSettled?: (queuer: AsyncQueuer<TFn>) => void
71
+ onSettled?: (queuer: AsyncQueuer<TValue>) => void
78
72
  /**
79
73
  * Optional callback to call when a task succeeds
80
74
  */
81
- onSuccess?: (result: TFn, queuer: AsyncQueuer<TFn>) => void
75
+ onSuccess?: (result: TValue, queuer: AsyncQueuer<TValue>) => void
82
76
  /**
83
77
  * Whether the queuer should start processing tasks immediately or not.
84
78
  */
@@ -94,20 +88,19 @@ export interface AsyncQueuerOptions<TFn extends AsyncQueuerFn> {
94
88
  * Can be a number or a function that returns a number.
95
89
  * @default 0
96
90
  */
97
- wait?: number | ((queuer: AsyncQueuer<TFn>) => number)
91
+ wait?: number | ((queuer: AsyncQueuer<TValue>) => number)
98
92
  }
99
93
 
100
94
  type AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<
101
95
  Required<AsyncQueuerOptions<any>>,
102
- | 'onError'
103
- | 'onExpire'
104
- | 'onGetNextItem'
105
- | 'onIsRunningChange'
106
- | 'onItemsChange'
107
- | 'onReject'
108
- | 'onSettled'
109
- | 'onSuccess'
110
96
  | 'throwOnError'
97
+ | 'onSuccess'
98
+ | 'onSettled'
99
+ | 'onReject'
100
+ | 'onItemsChange'
101
+ | 'onIsRunningChange'
102
+ | 'onExpire'
103
+ | 'onError'
111
104
  >
112
105
 
113
106
  const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {
@@ -124,57 +117,61 @@ const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {
124
117
  }
125
118
 
126
119
  /**
127
- * A flexible asynchronous queue that processes tasks with configurable concurrency control.
120
+ * A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration.
128
121
  *
129
122
  * Features:
130
- * - Priority queue support via getPriority option
123
+ * - Priority queue support via the getPriority option
131
124
  * - Configurable concurrency limit
132
- * - Task success/error/completion callbacks
125
+ * - Callbacks for task success, error, completion, and queue state changes
133
126
  * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior
134
- * - Pause/resume task processing
127
+ * - Pause and resume processing
135
128
  * - Task cancellation
136
- * - Item expiration to clear stale items from the queue
129
+ * - Item expiration to remove stale items from the queue
137
130
  *
138
131
  * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,
139
- * the next pending task is processed if below the concurrency limit.
132
+ * the next pending task is processed if the concurrency limit allows.
140
133
  *
141
134
  * Error Handling:
142
135
  * - If an `onError` handler is provided, it will be called with the error and queuer instance
143
136
  * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
144
137
  * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
145
- * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
146
- * - The error state can be checked using the underlying AsyncQueuer instance
138
+ * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown
139
+ * - The error state can be checked using the AsyncQueuer instance
147
140
  *
148
- * @example
141
+ * Example usage:
149
142
  * ```ts
150
- * const asyncQueuer = new AsyncQueuer<string>({
143
+ * const asyncQueuer = new AsyncQueuer<string>(async (item) => {
144
+ * // process item
145
+ * return item.toUpperCase();
146
+ * }, {
151
147
  * concurrency: 2,
152
148
  * onSuccess: (result) => {
153
- * console.log(result); // 'Hello'
149
+ * console.log(result);
154
150
  * }
155
151
  * });
156
152
  *
157
- * asyncQueuer.addItem(async () => {
158
- * return 'Hello';
159
- * });
160
- *
153
+ * asyncQueuer.addItem('hello');
161
154
  * asyncQueuer.start();
162
155
  * ```
163
156
  */
164
- export class AsyncQueuer<TFn extends AsyncQueuerFn> {
157
+ export class AsyncQueuer<TValue> {
165
158
  private _options: AsyncQueuerOptionsWithOptionalCallbacks
166
- private _activeItems: Set<TFn> = new Set()
159
+ private _activeItems: Set<TValue> = new Set()
167
160
  private _successCount = 0
168
161
  private _errorCount = 0
169
162
  private _settledCount = 0
170
163
  private _rejectionCount = 0
171
164
  private _expirationCount = 0
172
- private _items: Array<TFn> = []
165
+ private _items: Array<TValue> = []
173
166
  private _itemTimestamps: Array<number> = []
174
167
  private _pendingTick = false
175
168
  private _running: boolean
169
+ private _lastResult: any
176
170
 
177
- constructor(initialOptions: AsyncQueuerOptions<TFn> = defaultOptions) {
171
+ constructor(
172
+ private fn: (value: TValue) => Promise<any>,
173
+ initialOptions: AsyncQueuerOptions<TValue>,
174
+ ) {
178
175
  this._options = {
179
176
  ...defaultOptions,
180
177
  ...initialOptions,
@@ -190,36 +187,37 @@ export class AsyncQueuer<TFn extends AsyncQueuerFn> {
190
187
  }
191
188
 
192
189
  /**
193
- * Updates the queuer options
194
- * Returns the new options state
190
+ * Updates the queuer options. New options are merged with existing options.
195
191
  */
196
- setOptions(newOptions: Partial<AsyncQueuerOptions<TFn>>): void {
192
+ setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void {
197
193
  this._options = { ...this._options, ...newOptions }
198
194
  }
199
195
 
200
196
  /**
201
- * Returns the current queuer options
197
+ * Returns the current queuer options, including defaults and any overrides.
202
198
  */
203
- getOptions(): AsyncQueuerOptions<TFn> {
199
+ getOptions(): AsyncQueuerOptions<TValue> {
204
200
  return this._options
205
201
  }
206
202
 
207
203
  /**
208
- * Returns the current wait time between processing items
204
+ * Returns the current wait time (in milliseconds) between processing items.
205
+ * If a function is provided, it is called with the queuer instance.
209
206
  */
210
207
  getWait(): number {
211
208
  return parseFunctionOrValue(this._options.wait, this)
212
209
  }
213
210
 
214
211
  /**
215
- * Returns the current concurrency limit
212
+ * Returns the current concurrency limit for processing items.
213
+ * If a function is provided, it is called with the queuer instance.
216
214
  */
217
215
  getConcurrency(): number {
218
216
  return parseFunctionOrValue(this._options.concurrency, this)
219
217
  }
220
218
 
221
219
  /**
222
- * Processes items in the queuer
220
+ * Processes items in the queue up to the concurrency limit. Internal use only.
223
221
  */
224
222
  private tick() {
225
223
  if (!this._running) {
@@ -230,37 +228,19 @@ export class AsyncQueuer<TFn extends AsyncQueuerFn> {
230
228
  // Check for expired items
231
229
  this.checkExpiredItems()
232
230
 
231
+ // Process items concurrently up to the concurrency limit
233
232
  while (
234
233
  this._activeItems.size < this.getConcurrency() &&
235
234
  !this.getIsEmpty()
236
235
  ) {
237
- const nextFn = this.getNextItem()
238
- if (!nextFn) {
236
+ const nextItem = this.getPeek()
237
+ if (!nextItem) {
239
238
  break
240
239
  }
241
- this._activeItems.add(nextFn)
240
+ this._activeItems.add(nextItem)
242
241
  this._options.onItemsChange?.(this)
243
242
  ;(async () => {
244
- let res!: TFn
245
-
246
- try {
247
- res = await nextFn()
248
- this._successCount++
249
- this._options.onSuccess?.(res, this)
250
- } catch (error) {
251
- this._errorCount++
252
- this._options.onError?.(error, this)
253
- if (this._options.throwOnError) {
254
- throw error
255
- } else {
256
- console.error(error)
257
- }
258
- } finally {
259
- this._settledCount++
260
- this._activeItems.delete(nextFn)
261
- this._options.onItemsChange?.(this)
262
- this._options.onSettled?.(this)
263
- }
243
+ this._lastResult = await this.execute()
264
244
 
265
245
  const wait = this.getWait()
266
246
  if (wait > 0) {
@@ -276,80 +256,19 @@ export class AsyncQueuer<TFn extends AsyncQueuerFn> {
276
256
  }
277
257
 
278
258
  /**
279
- * Checks for and removes expired items from the queuer
280
- */
281
- private checkExpiredItems(): void {
282
- if (
283
- this._options.expirationDuration === Infinity &&
284
- this._options.getIsExpired === defaultOptions.getIsExpired
285
- )
286
- return
287
-
288
- const now = Date.now()
289
- const expiredIndices: Array<number> = []
290
-
291
- // Find indices of expired items
292
- for (let i = 0; i < this._items.length; i++) {
293
- const timestamp = this._itemTimestamps[i]
294
- if (timestamp === undefined) continue
295
-
296
- const item = this._items[i]
297
- if (item === undefined) continue
298
-
299
- const isExpired =
300
- this._options.getIsExpired !== defaultOptions.getIsExpired
301
- ? this._options.getIsExpired(item, timestamp)
302
- : now - timestamp > this._options.expirationDuration
303
-
304
- if (isExpired) {
305
- expiredIndices.push(i)
306
- }
307
- }
308
-
309
- // Remove expired items from back to front to maintain indices
310
- for (let i = expiredIndices.length - 1; i >= 0; i--) {
311
- const index = expiredIndices[i]
312
- if (index === undefined) continue
313
-
314
- const expiredItem = this._items[index]
315
- if (expiredItem === undefined) continue
316
-
317
- this._items.splice(index, 1)
318
- this._itemTimestamps.splice(index, 1)
319
- this._expirationCount++
320
- this._options.onExpire?.(expiredItem, this)
321
- }
322
-
323
- if (expiredIndices.length > 0) {
324
- this._options.onItemsChange?.(this)
325
- }
326
- }
327
-
328
- /**
329
- * Starts the queuer and processes items
259
+ * Starts processing items in the queue. If already running, does nothing.
330
260
  */
331
- start(): Promise<void> {
261
+ start(): void {
332
262
  this._running = true
333
263
  if (!this._pendingTick && !this.getIsEmpty()) {
334
264
  this._pendingTick = true
335
265
  this.tick()
336
266
  }
337
267
  this._options.onIsRunningChange?.(this)
338
-
339
- return new Promise<void>((resolve) => {
340
- const checkIdle = () => {
341
- if (this.getIsIdle()) {
342
- resolve()
343
- } else {
344
- setTimeout(checkIdle, 100)
345
- }
346
- }
347
- checkIdle()
348
- })
349
268
  }
350
269
 
351
270
  /**
352
- * Stops the queuer from processing items
271
+ * Stops processing items in the queue. Does not clear the queue.
353
272
  */
354
273
  stop(): void {
355
274
  this._running = false
@@ -358,7 +277,7 @@ export class AsyncQueuer<TFn extends AsyncQueuerFn> {
358
277
  }
359
278
 
360
279
  /**
361
- * Removes all items from the queuer
280
+ * Removes all pending items from the queue. Does not affect active tasks.
362
281
  */
363
282
  clear(): void {
364
283
  this._items = []
@@ -366,7 +285,8 @@ export class AsyncQueuer<TFn extends AsyncQueuerFn> {
366
285
  }
367
286
 
368
287
  /**
369
- * Resets the queuer to its initial state
288
+ * Resets the queuer to its initial state. Optionally repopulates with initial items.
289
+ * Does not affect callbacks or options.
370
290
  */
371
291
  reset(withInitialItems?: boolean): void {
372
292
  this.clear()
@@ -380,50 +300,57 @@ export class AsyncQueuer<TFn extends AsyncQueuerFn> {
380
300
  }
381
301
 
382
302
  /**
383
- * Adds a task to the queuer
303
+ * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
304
+ * Items can be inserted based on priority or at the front/back depending on configuration.
305
+ *
306
+ * @example
307
+ * ```ts
308
+ * queuer.addItem({ value: 'task', priority: 10 });
309
+ * queuer.addItem('task2', 'front');
310
+ * ```
384
311
  */
385
312
  addItem(
386
- fn: TFn,
313
+ item: TValue & { priority?: number },
387
314
  position: QueuePosition = this._options.addItemsTo,
388
315
  runOnItemsChange: boolean = true,
389
316
  ): void {
390
317
  if (this.getIsFull()) {
391
318
  this._rejectionCount++
392
- this._options.onReject?.(fn, this)
319
+ this._options.onReject?.(item, this)
393
320
  return
394
321
  }
395
322
 
396
323
  // Get priority either from the function or from getPriority option
397
324
  const priority =
398
325
  this._options.getPriority !== defaultOptions.getPriority
399
- ? this._options.getPriority(fn)
400
- : fn.priority
326
+ ? this._options.getPriority(item)
327
+ : item.priority
401
328
 
402
329
  if (priority !== undefined) {
403
- // Insert based on priority
330
+ // Insert based on priority - higher priority items go to front
404
331
  const insertIndex = this._items.findIndex((existing) => {
405
332
  const existingPriority =
406
333
  this._options.getPriority !== defaultOptions.getPriority
407
334
  ? this._options.getPriority(existing)
408
335
  : (existing as any).priority
409
- return existingPriority > priority
336
+ return existingPriority < priority
410
337
  })
411
338
 
412
339
  if (insertIndex === -1) {
413
- this._items.push(fn)
340
+ this._items.push(item)
414
341
  this._itemTimestamps.push(Date.now())
415
342
  } else {
416
- this._items.splice(insertIndex, 0, fn)
343
+ this._items.splice(insertIndex, 0, item)
417
344
  this._itemTimestamps.splice(insertIndex, 0, Date.now())
418
345
  }
419
346
  } else {
420
347
  if (position === 'front') {
421
348
  // Default FIFO/LIFO behavior
422
- this._items.unshift(fn)
349
+ this._items.unshift(item)
423
350
  this._itemTimestamps.unshift(Date.now())
424
351
  } else {
425
352
  // LIFO
426
- this._items.push(fn)
353
+ this._items.push(item)
427
354
  this._itemTimestamps.push(Date.now())
428
355
  }
429
356
  }
@@ -439,12 +366,21 @@ export class AsyncQueuer<TFn extends AsyncQueuerFn> {
439
366
  }
440
367
 
441
368
  /**
442
- * Removes and returns an item from the queuer
369
+ * Removes and returns the next item from the queue without executing the task function.
370
+ * Use for manual queue management. Normally, use execute() to process items.
371
+ *
372
+ * @example
373
+ * ```ts
374
+ * // FIFO
375
+ * queuer.getNextItem();
376
+ * // LIFO
377
+ * queuer.getNextItem('back');
378
+ * ```
443
379
  */
444
380
  getNextItem(
445
381
  position: QueuePosition = this._options.getItemsFrom,
446
- ): TFn | undefined {
447
- let item: TFn | undefined
382
+ ): TValue | undefined {
383
+ let item: TValue | undefined
448
384
 
449
385
  if (position === 'front') {
450
386
  item = this._items.shift()
@@ -456,15 +392,105 @@ export class AsyncQueuer<TFn extends AsyncQueuerFn> {
456
392
 
457
393
  if (item !== undefined) {
458
394
  this._options.onItemsChange?.(this)
459
- this._options.onGetNextItem?.(item, this)
460
395
  }
396
+
461
397
  return item
462
398
  }
463
399
 
464
400
  /**
465
- * Returns an item without removing it
401
+ * Removes and returns the next item from the queue and executes the task function with it.
402
+ *
403
+ * @example
404
+ * ```ts
405
+ * queuer.execute();
406
+ * // LIFO
407
+ * queuer.execute('back');
408
+ * ```
466
409
  */
467
- getPeek(position: QueuePosition = 'front'): TFn | undefined {
410
+ async execute(position?: QueuePosition): Promise<any> {
411
+ const item = this.getNextItem(position)
412
+ if (item !== undefined) {
413
+ try {
414
+ this._lastResult = await this.fn(item)
415
+ this._successCount++
416
+ this._options.onSuccess?.(this._lastResult, this)
417
+ } catch (error) {
418
+ this._errorCount++
419
+ this._options.onError?.(error, this)
420
+ if (this._options.throwOnError) {
421
+ throw error
422
+ }
423
+ } finally {
424
+ this._settledCount++
425
+ this._activeItems.delete(item)
426
+ this._options.onItemsChange?.(this)
427
+ this._options.onSettled?.(this)
428
+ }
429
+ }
430
+ return item
431
+ }
432
+
433
+ /**
434
+ * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
435
+ * Internal use only.
436
+ */
437
+ private checkExpiredItems(): void {
438
+ if (
439
+ this._options.expirationDuration === Infinity &&
440
+ this._options.getIsExpired === defaultOptions.getIsExpired
441
+ )
442
+ return
443
+
444
+ const now = Date.now()
445
+ const expiredIndices: Array<number> = []
446
+
447
+ // Find indices of expired items
448
+ for (let i = 0; i < this._items.length; i++) {
449
+ const timestamp = this._itemTimestamps[i]
450
+ if (timestamp === undefined) continue
451
+
452
+ const item = this._items[i]
453
+ if (item === undefined) continue
454
+
455
+ const isExpired =
456
+ this._options.getIsExpired !== defaultOptions.getIsExpired
457
+ ? this._options.getIsExpired(item, timestamp)
458
+ : now - timestamp > this._options.expirationDuration
459
+
460
+ if (isExpired) {
461
+ expiredIndices.push(i)
462
+ }
463
+ }
464
+
465
+ // Remove expired items from back to front to maintain indices
466
+ for (let i = expiredIndices.length - 1; i >= 0; i--) {
467
+ const index = expiredIndices[i]
468
+ if (index === undefined) continue
469
+
470
+ const expiredItem = this._items[index]
471
+ if (expiredItem === undefined) continue
472
+
473
+ this._items.splice(index, 1)
474
+ this._itemTimestamps.splice(index, 1)
475
+ this._expirationCount++
476
+ this._options.onExpire?.(expiredItem, this)
477
+ }
478
+
479
+ if (expiredIndices.length > 0) {
480
+ this._options.onItemsChange?.(this)
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Returns the next item in the queue without removing it.
486
+ *
487
+ * @example
488
+ * ```ts
489
+ * queuer.getPeek(); // front
490
+ * queuer.getPeek('back'); // back
491
+ * ```
492
+ */
493
+ getPeek(position: QueuePosition = 'front'): TValue | undefined {
468
494
  if (position === 'front') {
469
495
  return this._items[0]
470
496
  }
@@ -472,91 +498,91 @@ export class AsyncQueuer<TFn extends AsyncQueuerFn> {
472
498
  }
473
499
 
474
500
  /**
475
- * Returns true if the queuer is empty
501
+ * Returns true if the queue is empty (no pending items).
476
502
  */
477
503
  getIsEmpty(): boolean {
478
504
  return this._items.length === 0
479
505
  }
480
506
 
481
507
  /**
482
- * Returns true if the queuer is full
508
+ * Returns true if the queue is full (reached maxSize).
483
509
  */
484
510
  getIsFull(): boolean {
485
511
  return this._items.length >= this._options.maxSize
486
512
  }
487
513
 
488
514
  /**
489
- * Returns the current size of the queuer
515
+ * Returns the number of pending items in the queue.
490
516
  */
491
517
  getSize(): number {
492
518
  return this._items.length
493
519
  }
494
520
 
495
521
  /**
496
- * Returns a copy of all items in the queuer
522
+ * Returns a copy of all items in the queue, including active and pending items.
497
523
  */
498
- getAllItems(): Array<TFn> {
524
+ getAllItems(): Array<TValue> {
499
525
  return [...this.getActiveItems(), ...this.getPendingItems()]
500
526
  }
501
527
 
502
528
  /**
503
- * Returns the active items
529
+ * Returns the items currently being processed (active tasks).
504
530
  */
505
- getActiveItems(): Array<TFn> {
531
+ getActiveItems(): Array<TValue> {
506
532
  return Array.from(this._activeItems)
507
533
  }
508
534
 
509
535
  /**
510
- * Returns the pending items
536
+ * Returns the items waiting to be processed (pending tasks).
511
537
  */
512
- getPendingItems(): Array<TFn> {
538
+ getPendingItems(): Array<TValue> {
513
539
  return [...this._items]
514
540
  }
515
541
 
516
542
  /**
517
- * Returns the number of items that have been successfully processed
543
+ * Returns the number of items that have been successfully processed.
518
544
  */
519
545
  getSuccessCount(): number {
520
546
  return this._successCount
521
547
  }
522
548
 
523
549
  /**
524
- * Returns the number of items that have failed processing
550
+ * Returns the number of items that have failed processing.
525
551
  */
526
552
  getErrorCount(): number {
527
553
  return this._errorCount
528
554
  }
529
555
 
530
556
  /**
531
- * Returns the number of items that have completed processing (success or error)
557
+ * Returns the number of items that have completed processing (success or error).
532
558
  */
533
559
  getSettledCount(): number {
534
560
  return this._settledCount
535
561
  }
536
562
 
537
563
  /**
538
- * Returns the number of items that have been rejected from the queuer
564
+ * Returns the number of items that have been rejected from being added to the queue.
539
565
  */
540
566
  getRejectionCount(): number {
541
567
  return this._rejectionCount
542
568
  }
543
569
 
544
570
  /**
545
- * Returns true if the queuer is running
571
+ * Returns true if the queuer is currently running (processing items).
546
572
  */
547
573
  getIsRunning(): boolean {
548
574
  return this._running
549
575
  }
550
576
 
551
577
  /**
552
- * Returns true if the queuer is running but has no items to process
578
+ * Returns true if the queuer is running but has no items to process and no active tasks.
553
579
  */
554
580
  getIsIdle(): boolean {
555
581
  return this._running && this.getIsEmpty() && this._activeItems.size === 0
556
582
  }
557
583
 
558
584
  /**
559
- * Returns the number of items that have expired from the queuer
585
+ * Returns the number of items that have expired and been removed from the queue.
560
586
  */
561
587
  getExpirationCount(): number {
562
588
  return this._expirationCount
@@ -564,32 +590,29 @@ export class AsyncQueuer<TFn extends AsyncQueuerFn> {
564
590
  }
565
591
 
566
592
  /**
567
- * Creates a new AsyncQueuer instance with the given options and returns a bound addItem function.
568
- * The queuer is automatically started and ready to process items.
593
+ * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.
594
+ * The queuer is started automatically and ready to process items.
569
595
  *
570
596
  * Error Handling:
571
597
  * - If an `onError` handler is provided, it will be called with the error and queuer instance
572
598
  * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
573
599
  * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
574
- * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
600
+ * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown
575
601
  * - The error state can be checked using the underlying AsyncQueuer instance
576
602
  *
577
- * @example
603
+ * Example usage:
578
604
  * ```ts
579
- * const enqueue = asyncQueue<string>();
605
+ * const enqueue = asyncQueue<string>(async (item) => {
606
+ * return item.toUpperCase();
607
+ * }, {...options});
580
608
  *
581
- * // Add items to be processed
582
- * enqueue(async () => {
583
- * return 'Hello';
584
- * });
609
+ * enqueue('hello');
585
610
  * ```
586
- *
587
- * @param options - Configuration options for the AsyncQueuer
588
- * @returns A bound addItem function that can be used to add tasks to the queuer
589
611
  */
590
- export function asyncQueue<TFn extends AsyncQueuerFn>(
591
- options: AsyncQueuerOptions<TFn>,
612
+ export function asyncQueue<TValue>(
613
+ fn: (value: TValue) => Promise<any>,
614
+ initialOptions: AsyncQueuerOptions<TValue>,
592
615
  ) {
593
- const queuer = new AsyncQueuer<TFn>(options)
594
- return queuer.addItem.bind(queuer)
616
+ const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)
617
+ return asyncQueuer.addItem.bind(asyncQueuer)
595
618
  }