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