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