@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.
- package/dist/cjs/async-debouncer.cjs +32 -17
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +51 -9
- package/dist/cjs/async-queuer.cjs +189 -191
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +151 -94
- package/dist/cjs/async-rate-limiter.cjs +23 -13
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +54 -5
- package/dist/cjs/async-throttler.cjs +38 -17
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +51 -8
- 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 +68 -41
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +123 -92
- 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/cjs/types.d.cts +1 -0
- package/dist/esm/async-debouncer.d.ts +51 -9
- package/dist/esm/async-debouncer.js +32 -17
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +151 -94
- package/dist/esm/async-queuer.js +189 -191
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +54 -5
- package/dist/esm/async-rate-limiter.js +23 -13
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +51 -8
- package/dist/esm/async-throttler.js +38 -17
- 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 +123 -92
- package/dist/esm/queuer.js +68 -41
- 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/dist/esm/types.d.ts +1 -0
- package/package.json +11 -1
- package/src/async-debouncer.ts +76 -20
- package/src/async-queuer.ts +309 -275
- package/src/async-rate-limiter.ts +73 -16
- package/src/async-throttler.ts +84 -20
- package/src/batcher.ts +253 -0
- package/src/debouncer.ts +3 -4
- package/src/index.ts +1 -0
- package/src/queuer.ts +142 -98
- package/src/rate-limiter.ts +3 -4
- package/src/throttler.ts +3 -4
- package/src/types.ts +3 -0
package/src/async-queuer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
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:
|
|
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:
|
|
37
|
+
getPriority?: (item: TValue) => number
|
|
38
38
|
/**
|
|
39
39
|
* Initial items to populate the queuer with
|
|
40
40
|
*/
|
|
41
|
-
initialItems?: Array<
|
|
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
|
-
*
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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:
|
|
67
|
+
onReject?: (item: TValue, queuer: AsyncQueuer<TValue>) => void
|
|
65
68
|
/**
|
|
66
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
* -
|
|
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
|
|
127
|
+
* - Pause and resume processing
|
|
108
128
|
* - Task cancellation
|
|
109
|
-
* - Item expiration to
|
|
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
|
|
132
|
+
* the next pending task is processed if the concurrency limit allows.
|
|
113
133
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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
|
-
*
|
|
119
|
-
*
|
|
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:
|
|
131
|
-
private _activeItems: Set<
|
|
132
|
-
private
|
|
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<
|
|
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
|
-
|
|
144
|
-
|
|
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():
|
|
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
|
|
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
|
|
200
|
-
if (!
|
|
236
|
+
const nextItem = this.getPeek()
|
|
237
|
+
if (!nextItem) {
|
|
201
238
|
break
|
|
202
239
|
}
|
|
203
|
-
this._activeItems.add(
|
|
204
|
-
this._options.onItemsChange(this)
|
|
240
|
+
this._activeItems.add(nextItem)
|
|
241
|
+
this._options.onItemsChange?.(this)
|
|
205
242
|
;(async () => {
|
|
206
|
-
|
|
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
|
-
*
|
|
259
|
+
* Starts processing items in the queue. If already running, does nothing.
|
|
242
260
|
*/
|
|
243
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
313
|
+
item: TValue & { priority?: number },
|
|
347
314
|
position: QueuePosition = this._options.addItemsTo,
|
|
348
315
|
runOnItemsChange: boolean = true,
|
|
349
|
-
):
|
|
316
|
+
): void {
|
|
350
317
|
if (this.getIsFull()) {
|
|
351
318
|
this._rejectionCount++
|
|
352
|
-
this._options.onReject(
|
|
353
|
-
return
|
|
319
|
+
this._options.onReject?.(item, this)
|
|
320
|
+
return
|
|
354
321
|
}
|
|
355
322
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
this.
|
|
374
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
358
|
+
if (runOnItemsChange) {
|
|
359
|
+
this._options.onItemsChange?.(this)
|
|
360
|
+
}
|
|
408
361
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
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
|
-
):
|
|
422
|
-
let item:
|
|
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.
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
522
|
+
* Returns a copy of all items in the queue, including active and pending items.
|
|
475
523
|
*/
|
|
476
|
-
getAllItems(): Array<
|
|
524
|
+
getAllItems(): Array<TValue> {
|
|
477
525
|
return [...this.getActiveItems(), ...this.getPendingItems()]
|
|
478
526
|
}
|
|
479
527
|
|
|
480
528
|
/**
|
|
481
|
-
* Returns the active
|
|
529
|
+
* Returns the items currently being processed (active tasks).
|
|
482
530
|
*/
|
|
483
|
-
getActiveItems(): Array<
|
|
531
|
+
getActiveItems(): Array<TValue> {
|
|
484
532
|
return Array.from(this._activeItems)
|
|
485
533
|
}
|
|
486
534
|
|
|
487
535
|
/**
|
|
488
|
-
* Returns the pending
|
|
536
|
+
* Returns the items waiting to be processed (pending tasks).
|
|
489
537
|
*/
|
|
490
|
-
getPendingItems(): Array<
|
|
538
|
+
getPendingItems(): Array<TValue> {
|
|
491
539
|
return [...this._items]
|
|
492
540
|
}
|
|
493
541
|
|
|
494
542
|
/**
|
|
495
|
-
* Returns the number of items that have been
|
|
543
|
+
* Returns the number of items that have been successfully processed.
|
|
496
544
|
*/
|
|
497
|
-
|
|
498
|
-
return this.
|
|
545
|
+
getSuccessCount(): number {
|
|
546
|
+
return this._successCount
|
|
499
547
|
}
|
|
500
548
|
|
|
501
549
|
/**
|
|
502
|
-
* Returns the number of items that have
|
|
550
|
+
* Returns the number of items that have failed processing.
|
|
503
551
|
*/
|
|
504
|
-
|
|
505
|
-
return this.
|
|
552
|
+
getErrorCount(): number {
|
|
553
|
+
return this._errorCount
|
|
506
554
|
}
|
|
507
555
|
|
|
508
556
|
/**
|
|
509
|
-
* Returns
|
|
557
|
+
* Returns the number of items that have completed processing (success or error).
|
|
510
558
|
*/
|
|
511
|
-
|
|
512
|
-
return this.
|
|
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
|
-
*
|
|
564
|
+
* Returns the number of items that have been rejected from being added to the queue.
|
|
524
565
|
*/
|
|
525
|
-
|
|
526
|
-
this.
|
|
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
|
-
*
|
|
571
|
+
* Returns true if the queuer is currently running (processing items).
|
|
536
572
|
*/
|
|
537
|
-
|
|
538
|
-
this.
|
|
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
|
-
*
|
|
578
|
+
* Returns true if the queuer is running but has no items to process and no active tasks.
|
|
546
579
|
*/
|
|
547
|
-
|
|
548
|
-
this.
|
|
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
|
|
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
|
|
566
|
-
* 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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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>(
|
|
582
|
-
|
|
583
|
-
|
|
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
|
}
|