@tanstack/pacer 0.6.0 → 0.8.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.
- package/dist/cjs/async-debouncer.cjs +10 -6
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +2 -2
- package/dist/cjs/async-queuer.cjs +140 -111
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +121 -93
- package/dist/cjs/async-rate-limiter.cjs +3 -4
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +1 -2
- package/dist/cjs/async-throttler.cjs +14 -4
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +3 -2
- package/dist/cjs/batcher.cjs +138 -0
- package/dist/cjs/batcher.cjs.map +1 -0
- package/dist/cjs/batcher.d.cts +149 -0
- package/dist/cjs/debouncer.cjs +3 -4
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +1 -2
- package/dist/cjs/index.cjs +3 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/queuer.cjs +70 -43
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +126 -95
- package/dist/cjs/rate-limiter.cjs +3 -4
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +1 -2
- package/dist/cjs/throttler.cjs +3 -4
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +1 -2
- package/dist/esm/async-debouncer.d.ts +2 -2
- package/dist/esm/async-debouncer.js +10 -6
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +121 -93
- package/dist/esm/async-queuer.js +140 -111
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +1 -2
- package/dist/esm/async-rate-limiter.js +3 -4
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +3 -2
- package/dist/esm/async-throttler.js +14 -4
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +149 -0
- package/dist/esm/batcher.js +138 -0
- package/dist/esm/batcher.js.map +1 -0
- package/dist/esm/debouncer.d.ts +1 -2
- package/dist/esm/debouncer.js +3 -4
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +126 -95
- package/dist/esm/queuer.js +70 -43
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +1 -2
- package/dist/esm/rate-limiter.js +3 -4
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +1 -2
- package/dist/esm/throttler.js +3 -4
- package/dist/esm/throttler.js.map +1 -1
- package/package.json +11 -1
- package/src/async-debouncer.ts +12 -6
- package/src/async-queuer.ts +217 -194
- package/src/async-rate-limiter.ts +3 -4
- package/src/async-throttler.ts +18 -4
- package/src/batcher.ts +253 -0
- package/src/debouncer.ts +3 -4
- package/src/index.ts +1 -0
- package/src/queuer.ts +145 -101
- package/src/rate-limiter.ts +3 -4
- package/src/throttler.ts +3 -4
package/src/async-queuer.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { parseFunctionOrValue } from './utils'
|
|
2
|
-
import type {
|
|
2
|
+
import type { OptionalKeys } from './types'
|
|
3
3
|
import type { QueuePosition } from './queuer'
|
|
4
4
|
|
|
5
|
-
export
|
|
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<
|
|
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:
|
|
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:
|
|
37
|
+
getPriority?: (item: TValue) => number
|
|
40
38
|
/**
|
|
41
39
|
* Initial items to populate the queuer with
|
|
42
40
|
*/
|
|
43
|
-
initialItems?: Array<
|
|
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<
|
|
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:
|
|
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<
|
|
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<
|
|
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:
|
|
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<
|
|
71
|
+
onSettled?: (queuer: AsyncQueuer<TValue>) => void
|
|
78
72
|
/**
|
|
79
73
|
* Optional callback to call when a task succeeds
|
|
80
74
|
*/
|
|
81
|
-
onSuccess?: (result:
|
|
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<
|
|
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
|
|
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
|
-
* -
|
|
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
|
|
127
|
+
* - Pause and resume processing
|
|
135
128
|
* - Task cancellation
|
|
136
|
-
* - Item expiration to
|
|
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
|
|
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
|
|
146
|
-
* - The error state can be checked using the
|
|
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
|
-
*
|
|
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);
|
|
149
|
+
* console.log(result);
|
|
154
150
|
* }
|
|
155
151
|
* });
|
|
156
152
|
*
|
|
157
|
-
* asyncQueuer.addItem(
|
|
158
|
-
* return 'Hello';
|
|
159
|
-
* });
|
|
160
|
-
*
|
|
153
|
+
* asyncQueuer.addItem('hello');
|
|
161
154
|
* asyncQueuer.start();
|
|
162
155
|
* ```
|
|
163
156
|
*/
|
|
164
|
-
export class AsyncQueuer<
|
|
157
|
+
export class AsyncQueuer<TValue> {
|
|
165
158
|
private _options: AsyncQueuerOptionsWithOptionalCallbacks
|
|
166
|
-
private _activeItems: 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<
|
|
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(
|
|
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<
|
|
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<
|
|
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
|
|
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
|
|
238
|
-
if (!
|
|
236
|
+
const nextItem = this.peekNextItem()
|
|
237
|
+
if (!nextItem) {
|
|
239
238
|
break
|
|
240
239
|
}
|
|
241
|
-
this._activeItems.add(
|
|
240
|
+
this._activeItems.add(nextItem)
|
|
242
241
|
this._options.onItemsChange?.(this)
|
|
243
242
|
;(async () => {
|
|
244
|
-
|
|
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
|
-
*
|
|
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():
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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?.(
|
|
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(
|
|
400
|
-
:
|
|
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
|
|
336
|
+
return existingPriority < priority
|
|
410
337
|
})
|
|
411
338
|
|
|
412
339
|
if (insertIndex === -1) {
|
|
413
|
-
this._items.push(
|
|
340
|
+
this._items.push(item)
|
|
414
341
|
this._itemTimestamps.push(Date.now())
|
|
415
342
|
} else {
|
|
416
|
-
this._items.splice(insertIndex, 0,
|
|
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(
|
|
349
|
+
this._items.unshift(item)
|
|
423
350
|
this._itemTimestamps.unshift(Date.now())
|
|
424
351
|
} else {
|
|
425
352
|
// LIFO
|
|
426
|
-
this._items.push(
|
|
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
|
|
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
|
-
):
|
|
447
|
-
let item:
|
|
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
|
-
*
|
|
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
|
-
|
|
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.peekNextItem(); // front
|
|
490
|
+
* queuer.peekNextItem('back'); // back
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
peekNextItem(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
|
|
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
|
|
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
|
|
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
|
|
522
|
+
* Returns a copy of all items in the queue, including active and pending items.
|
|
497
523
|
*/
|
|
498
|
-
|
|
499
|
-
return [...this.
|
|
524
|
+
peekAllItems(): Array<TValue> {
|
|
525
|
+
return [...this.peekActiveItems(), ...this.peekPendingItems()]
|
|
500
526
|
}
|
|
501
527
|
|
|
502
528
|
/**
|
|
503
|
-
* Returns the active
|
|
529
|
+
* Returns the items currently being processed (active tasks).
|
|
504
530
|
*/
|
|
505
|
-
|
|
531
|
+
peekActiveItems(): Array<TValue> {
|
|
506
532
|
return Array.from(this._activeItems)
|
|
507
533
|
}
|
|
508
534
|
|
|
509
535
|
/**
|
|
510
|
-
* Returns the pending
|
|
536
|
+
* Returns the items waiting to be processed (pending tasks).
|
|
511
537
|
*/
|
|
512
|
-
|
|
538
|
+
peekPendingItems(): 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
|
|
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
|
|
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
|
|
568
|
-
* The queuer is automatically
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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<
|
|
591
|
-
|
|
612
|
+
export function asyncQueue<TValue>(
|
|
613
|
+
fn: (value: TValue) => Promise<any>,
|
|
614
|
+
initialOptions: AsyncQueuerOptions<TValue>,
|
|
592
615
|
) {
|
|
593
|
-
const
|
|
594
|
-
return
|
|
616
|
+
const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)
|
|
617
|
+
return asyncQueuer.addItem.bind(asyncQueuer)
|
|
595
618
|
}
|