@tanstack/pacer 0.8.0 → 0.9.1
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-batcher.cjs +163 -0
- package/dist/cjs/async-batcher.cjs.map +1 -0
- package/dist/cjs/async-batcher.d.cts +273 -0
- package/dist/cjs/async-debouncer.cjs +149 -162
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +76 -57
- package/dist/cjs/async-queuer.cjs +282 -343
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +121 -100
- package/dist/cjs/async-rate-limiter.cjs +128 -185
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +72 -61
- package/dist/cjs/async-throttler.cjs +168 -178
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +97 -69
- package/dist/cjs/batcher.cjs +110 -119
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +76 -51
- package/dist/cjs/debouncer.cjs +97 -85
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +54 -26
- package/dist/cjs/index.cjs +3 -6
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/queuer.cjs +247 -294
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +102 -81
- package/dist/cjs/rate-limiter.cjs +97 -130
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +50 -37
- package/dist/cjs/throttler.cjs +107 -123
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +59 -35
- package/dist/cjs/utils.cjs +0 -13
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +0 -1
- package/dist/esm/async-batcher.d.ts +273 -0
- package/dist/esm/async-batcher.js +163 -0
- package/dist/esm/async-batcher.js.map +1 -0
- package/dist/esm/async-debouncer.d.ts +76 -57
- package/dist/esm/async-debouncer.js +149 -162
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +121 -100
- package/dist/esm/async-queuer.js +282 -343
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +72 -61
- package/dist/esm/async-rate-limiter.js +128 -185
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +97 -69
- package/dist/esm/async-throttler.js +168 -178
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +76 -51
- package/dist/esm/batcher.js +110 -119
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/debouncer.d.ts +54 -26
- package/dist/esm/debouncer.js +97 -85
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +4 -7
- package/dist/esm/queuer.d.ts +102 -81
- package/dist/esm/queuer.js +247 -294
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +50 -37
- package/dist/esm/rate-limiter.js +97 -130
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +59 -35
- package/dist/esm/throttler.js +107 -123
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.d.ts +0 -1
- package/dist/esm/utils.js +0 -13
- package/dist/esm/utils.js.map +1 -1
- package/package.json +14 -11
- package/src/async-batcher.ts +475 -0
- package/src/async-debouncer.ts +201 -121
- package/src/async-queuer.ts +337 -216
- package/src/async-rate-limiter.ts +176 -136
- package/src/async-throttler.ts +233 -139
- package/src/batcher.ts +158 -92
- package/src/debouncer.ts +135 -52
- package/src/index.ts +1 -1
- package/src/queuer.ts +349 -226
- package/src/rate-limiter.ts +125 -80
- package/src/throttler.ts +152 -78
- package/src/utils.ts +0 -15
- package/dist/cjs/compare.cjs +0 -72
- package/dist/cjs/compare.cjs.map +0 -1
- package/dist/cjs/compare.d.cts +0 -12
- package/dist/esm/compare.d.ts +0 -12
- package/dist/esm/compare.js +0 -72
- package/dist/esm/compare.js.map +0 -1
- package/src/compare.ts +0 -105
package/src/async-queuer.ts
CHANGED
|
@@ -1,7 +1,96 @@
|
|
|
1
|
+
import { Store } from '@tanstack/store'
|
|
1
2
|
import { parseFunctionOrValue } from './utils'
|
|
2
3
|
import type { OptionalKeys } from './types'
|
|
3
4
|
import type { QueuePosition } from './queuer'
|
|
4
5
|
|
|
6
|
+
export interface AsyncQueuerState<TValue> {
|
|
7
|
+
/**
|
|
8
|
+
* Items currently being processed by the queuer
|
|
9
|
+
*/
|
|
10
|
+
activeItems: Array<TValue>
|
|
11
|
+
/**
|
|
12
|
+
* Number of task executions that have resulted in errors
|
|
13
|
+
*/
|
|
14
|
+
errorCount: number
|
|
15
|
+
/**
|
|
16
|
+
* Number of items that have been removed from the queue due to expiration
|
|
17
|
+
*/
|
|
18
|
+
expirationCount: number
|
|
19
|
+
/**
|
|
20
|
+
* Whether the queuer has no items to process (items array is empty)
|
|
21
|
+
*/
|
|
22
|
+
isEmpty: boolean
|
|
23
|
+
/**
|
|
24
|
+
* Whether the queuer has reached its maximum capacity
|
|
25
|
+
*/
|
|
26
|
+
isFull: boolean
|
|
27
|
+
/**
|
|
28
|
+
* Whether the queuer is not currently processing any items
|
|
29
|
+
*/
|
|
30
|
+
isIdle: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Whether the queuer is active and will process items automatically
|
|
33
|
+
*/
|
|
34
|
+
isRunning: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Timestamps when items were added to the queue for expiration tracking
|
|
37
|
+
*/
|
|
38
|
+
itemTimestamps: Array<number>
|
|
39
|
+
/**
|
|
40
|
+
* Array of items currently waiting to be processed
|
|
41
|
+
*/
|
|
42
|
+
items: Array<TValue>
|
|
43
|
+
/**
|
|
44
|
+
* The result from the most recent task execution
|
|
45
|
+
*/
|
|
46
|
+
lastResult: any
|
|
47
|
+
/**
|
|
48
|
+
* Whether the queuer has a pending timeout for processing the next item
|
|
49
|
+
*/
|
|
50
|
+
pendingTick: boolean
|
|
51
|
+
/**
|
|
52
|
+
* Number of items that have been rejected from being added to the queue
|
|
53
|
+
*/
|
|
54
|
+
rejectionCount: number
|
|
55
|
+
/**
|
|
56
|
+
* Number of task executions that have completed (either successfully or with errors)
|
|
57
|
+
*/
|
|
58
|
+
settledCount: number
|
|
59
|
+
/**
|
|
60
|
+
* Number of items currently in the queue
|
|
61
|
+
*/
|
|
62
|
+
size: number
|
|
63
|
+
/**
|
|
64
|
+
* Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused
|
|
65
|
+
*/
|
|
66
|
+
status: 'idle' | 'running' | 'stopped'
|
|
67
|
+
/**
|
|
68
|
+
* Number of task executions that have completed successfully
|
|
69
|
+
*/
|
|
70
|
+
successCount: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {
|
|
74
|
+
return structuredClone({
|
|
75
|
+
activeItems: [],
|
|
76
|
+
errorCount: 0,
|
|
77
|
+
expirationCount: 0,
|
|
78
|
+
isEmpty: true,
|
|
79
|
+
isFull: false,
|
|
80
|
+
isIdle: true,
|
|
81
|
+
isRunning: true,
|
|
82
|
+
itemTimestamps: [],
|
|
83
|
+
items: [],
|
|
84
|
+
lastResult: null,
|
|
85
|
+
pendingTick: false,
|
|
86
|
+
rejectionCount: 0,
|
|
87
|
+
settledCount: 0,
|
|
88
|
+
size: 0,
|
|
89
|
+
status: 'idle',
|
|
90
|
+
successCount: 0,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
5
94
|
export interface AsyncQueuerOptions<TValue> {
|
|
6
95
|
/**
|
|
7
96
|
* Default position to add items to the queuer
|
|
@@ -39,6 +128,10 @@ export interface AsyncQueuerOptions<TValue> {
|
|
|
39
128
|
* Initial items to populate the queuer with
|
|
40
129
|
*/
|
|
41
130
|
initialItems?: Array<TValue>
|
|
131
|
+
/**
|
|
132
|
+
* Initial state for the async queuer
|
|
133
|
+
*/
|
|
134
|
+
initialState?: Partial<AsyncQueuerState<TValue>>
|
|
42
135
|
/**
|
|
43
136
|
* Maximum number of items allowed in the queuer
|
|
44
137
|
*/
|
|
@@ -53,10 +146,6 @@ export interface AsyncQueuerOptions<TValue> {
|
|
|
53
146
|
* Callback fired whenever an item expires in the queuer
|
|
54
147
|
*/
|
|
55
148
|
onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void
|
|
56
|
-
/**
|
|
57
|
-
* Callback fired whenever the queuer's running state changes
|
|
58
|
-
*/
|
|
59
|
-
onIsRunningChange?: (queuer: AsyncQueuer<TValue>) => void
|
|
60
149
|
/**
|
|
61
150
|
* Callback fired whenever an item is added or removed from the queuer
|
|
62
151
|
*/
|
|
@@ -93,12 +182,12 @@ export interface AsyncQueuerOptions<TValue> {
|
|
|
93
182
|
|
|
94
183
|
type AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<
|
|
95
184
|
Required<AsyncQueuerOptions<any>>,
|
|
185
|
+
| 'initialState'
|
|
96
186
|
| 'throwOnError'
|
|
97
187
|
| 'onSuccess'
|
|
98
188
|
| 'onSettled'
|
|
99
189
|
| 'onReject'
|
|
100
190
|
| 'onItemsChange'
|
|
101
|
-
| 'onIsRunningChange'
|
|
102
191
|
| 'onExpire'
|
|
103
192
|
| 'onError'
|
|
104
193
|
>
|
|
@@ -138,6 +227,19 @@ const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {
|
|
|
138
227
|
* - Both onError and throwOnError can be used together; the handler will be called before any error is thrown
|
|
139
228
|
* - The error state can be checked using the AsyncQueuer instance
|
|
140
229
|
*
|
|
230
|
+
* State Management:
|
|
231
|
+
* - Uses TanStack Store for reactive state management
|
|
232
|
+
* - Use `initialState` to provide initial state values when creating the async queuer
|
|
233
|
+
* - Use `onSuccess` callback to react to successful task execution and implement custom logic
|
|
234
|
+
* - Use `onError` callback to react to task execution errors and implement custom error handling
|
|
235
|
+
* - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic
|
|
236
|
+
* - Use `onItemsChange` callback to react to items being added or removed from the queue
|
|
237
|
+
* - Use `onExpire` callback to react to items expiring and implement custom logic
|
|
238
|
+
* - Use `onReject` callback to react to items being rejected when the queue is full
|
|
239
|
+
* - The state includes error count, expiration count, rejection count, running status, and success/settle counts
|
|
240
|
+
* - State can be accessed via `asyncQueuer.store.state` when using the class directly
|
|
241
|
+
* - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`
|
|
242
|
+
*
|
|
141
243
|
* Example usage:
|
|
142
244
|
* ```ts
|
|
143
245
|
* const asyncQueuer = new AsyncQueuer<string>(async (item) => {
|
|
@@ -155,148 +257,134 @@ const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {
|
|
|
155
257
|
* ```
|
|
156
258
|
*/
|
|
157
259
|
export class AsyncQueuer<TValue> {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
private _rejectionCount = 0
|
|
164
|
-
private _expirationCount = 0
|
|
165
|
-
private _items: Array<TValue> = []
|
|
166
|
-
private _itemTimestamps: Array<number> = []
|
|
167
|
-
private _pendingTick = false
|
|
168
|
-
private _running: boolean
|
|
169
|
-
private _lastResult: any
|
|
260
|
+
readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<
|
|
261
|
+
AsyncQueuerState<TValue>
|
|
262
|
+
>(getDefaultAsyncQueuerState<TValue>())
|
|
263
|
+
options: AsyncQueuerOptions<TValue>
|
|
264
|
+
#timeoutIds: Set<NodeJS.Timeout> = new Set()
|
|
170
265
|
|
|
171
266
|
constructor(
|
|
172
|
-
private fn: (
|
|
173
|
-
initialOptions: AsyncQueuerOptions<TValue
|
|
267
|
+
private fn: (item: TValue) => Promise<any>,
|
|
268
|
+
initialOptions: AsyncQueuerOptions<TValue> = {},
|
|
174
269
|
) {
|
|
175
|
-
this.
|
|
270
|
+
this.options = {
|
|
176
271
|
...defaultOptions,
|
|
177
272
|
...initialOptions,
|
|
178
273
|
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
|
|
179
274
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
275
|
+
const isInitiallyRunning =
|
|
276
|
+
this.options.initialState?.isRunning ?? this.options.started ?? true
|
|
277
|
+
this.#setState({
|
|
278
|
+
...this.options.initialState,
|
|
279
|
+
isRunning: isInitiallyRunning,
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
if (this.options.initialState?.items) {
|
|
283
|
+
if (this.store.state.isRunning) {
|
|
284
|
+
this.#tick()
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {
|
|
288
|
+
const item = this.options.initialItems![i]!
|
|
289
|
+
const isLast = i === (this.options.initialItems?.length ?? 0) - 1
|
|
290
|
+
this.addItem(item, this.options.addItemsTo ?? 'back', isLast)
|
|
291
|
+
}
|
|
186
292
|
}
|
|
187
293
|
}
|
|
188
294
|
|
|
189
295
|
/**
|
|
190
296
|
* Updates the queuer options. New options are merged with existing options.
|
|
191
297
|
*/
|
|
192
|
-
setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void {
|
|
193
|
-
this.
|
|
298
|
+
setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {
|
|
299
|
+
this.options = { ...this.options, ...newOptions }
|
|
194
300
|
}
|
|
195
301
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
302
|
+
#setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {
|
|
303
|
+
this.store.setState((state) => {
|
|
304
|
+
const combinedState = {
|
|
305
|
+
...state,
|
|
306
|
+
...newState,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const { activeItems, items, isRunning } = combinedState
|
|
310
|
+
|
|
311
|
+
const size = items.length
|
|
312
|
+
const isFull = size >= (this.options.maxSize ?? Infinity)
|
|
313
|
+
const isEmpty = size === 0
|
|
314
|
+
const isIdle = isRunning && isEmpty && activeItems.length === 0
|
|
315
|
+
|
|
316
|
+
const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
...combinedState,
|
|
320
|
+
isEmpty,
|
|
321
|
+
isFull,
|
|
322
|
+
isIdle,
|
|
323
|
+
size,
|
|
324
|
+
status,
|
|
325
|
+
}
|
|
326
|
+
})
|
|
201
327
|
}
|
|
202
328
|
|
|
203
329
|
/**
|
|
204
330
|
* Returns the current wait time (in milliseconds) between processing items.
|
|
205
331
|
* If a function is provided, it is called with the queuer instance.
|
|
206
332
|
*/
|
|
207
|
-
getWait(): number {
|
|
208
|
-
return parseFunctionOrValue(this.
|
|
333
|
+
#getWait = (): number => {
|
|
334
|
+
return parseFunctionOrValue(this.options.wait ?? 0, this)
|
|
209
335
|
}
|
|
210
336
|
|
|
211
337
|
/**
|
|
212
338
|
* Returns the current concurrency limit for processing items.
|
|
213
339
|
* If a function is provided, it is called with the queuer instance.
|
|
214
340
|
*/
|
|
215
|
-
getConcurrency(): number {
|
|
216
|
-
return parseFunctionOrValue(this.
|
|
341
|
+
#getConcurrency = (): number => {
|
|
342
|
+
return parseFunctionOrValue(this.options.concurrency ?? 1, this)
|
|
217
343
|
}
|
|
218
344
|
|
|
219
345
|
/**
|
|
220
346
|
* Processes items in the queue up to the concurrency limit. Internal use only.
|
|
221
347
|
*/
|
|
222
|
-
|
|
223
|
-
if (!this.
|
|
224
|
-
this
|
|
348
|
+
#tick = () => {
|
|
349
|
+
if (!this.store.state.isRunning) {
|
|
350
|
+
this.#setState({ pendingTick: false })
|
|
225
351
|
return
|
|
226
352
|
}
|
|
353
|
+
this.#setState({ pendingTick: true })
|
|
227
354
|
|
|
228
355
|
// Check for expired items
|
|
229
|
-
this
|
|
356
|
+
this.#checkExpiredItems()
|
|
230
357
|
|
|
231
358
|
// Process items concurrently up to the concurrency limit
|
|
359
|
+
const activeItems = this.store.state.activeItems
|
|
232
360
|
while (
|
|
233
|
-
|
|
234
|
-
|
|
361
|
+
activeItems.length < this.#getConcurrency() &&
|
|
362
|
+
this.store.state.items.length > 0
|
|
235
363
|
) {
|
|
236
364
|
const nextItem = this.peekNextItem()
|
|
237
365
|
if (!nextItem) {
|
|
238
366
|
break
|
|
239
367
|
}
|
|
240
|
-
|
|
241
|
-
this
|
|
368
|
+
activeItems.push(nextItem)
|
|
369
|
+
this.#setState({
|
|
370
|
+
activeItems,
|
|
371
|
+
})
|
|
242
372
|
;(async () => {
|
|
243
|
-
|
|
373
|
+
const result = await this.execute()
|
|
374
|
+
this.#setState({ lastResult: result })
|
|
244
375
|
|
|
245
|
-
const wait = this
|
|
376
|
+
const wait = this.#getWait()
|
|
246
377
|
if (wait > 0) {
|
|
247
|
-
setTimeout(() => this
|
|
378
|
+
const timeoutId = setTimeout(() => this.#tick(), wait)
|
|
379
|
+
this.#timeoutIds.add(timeoutId)
|
|
248
380
|
return
|
|
249
381
|
}
|
|
250
382
|
|
|
251
|
-
this
|
|
383
|
+
this.#tick()
|
|
252
384
|
})()
|
|
253
385
|
}
|
|
254
386
|
|
|
255
|
-
this
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Starts processing items in the queue. If already running, does nothing.
|
|
260
|
-
*/
|
|
261
|
-
start(): void {
|
|
262
|
-
this._running = true
|
|
263
|
-
if (!this._pendingTick && !this.getIsEmpty()) {
|
|
264
|
-
this._pendingTick = true
|
|
265
|
-
this.tick()
|
|
266
|
-
}
|
|
267
|
-
this._options.onIsRunningChange?.(this)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Stops processing items in the queue. Does not clear the queue.
|
|
272
|
-
*/
|
|
273
|
-
stop(): void {
|
|
274
|
-
this._running = false
|
|
275
|
-
this._pendingTick = false
|
|
276
|
-
this._options.onIsRunningChange?.(this)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Removes all pending items from the queue. Does not affect active tasks.
|
|
281
|
-
*/
|
|
282
|
-
clear(): void {
|
|
283
|
-
this._items = []
|
|
284
|
-
this._options.onItemsChange?.(this)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Resets the queuer to its initial state. Optionally repopulates with initial items.
|
|
289
|
-
* Does not affect callbacks or options.
|
|
290
|
-
*/
|
|
291
|
-
reset(withInitialItems?: boolean): void {
|
|
292
|
-
this.clear()
|
|
293
|
-
this._successCount = 0
|
|
294
|
-
this._errorCount = 0
|
|
295
|
-
this._settledCount = 0
|
|
296
|
-
if (withInitialItems) {
|
|
297
|
-
this._items = [...this._options.initialItems]
|
|
298
|
-
}
|
|
299
|
-
this._running = this._options.started
|
|
387
|
+
this.#setState({ pendingTick: false })
|
|
300
388
|
}
|
|
301
389
|
|
|
302
390
|
/**
|
|
@@ -309,60 +397,71 @@ export class AsyncQueuer<TValue> {
|
|
|
309
397
|
* queuer.addItem('task2', 'front');
|
|
310
398
|
* ```
|
|
311
399
|
*/
|
|
312
|
-
addItem(
|
|
313
|
-
item: TValue
|
|
314
|
-
position: QueuePosition = this.
|
|
400
|
+
addItem = (
|
|
401
|
+
item: TValue,
|
|
402
|
+
position: QueuePosition = this.options.addItemsTo ?? 'back',
|
|
315
403
|
runOnItemsChange: boolean = true,
|
|
316
|
-
):
|
|
317
|
-
if (this.
|
|
318
|
-
this
|
|
319
|
-
|
|
320
|
-
|
|
404
|
+
): boolean => {
|
|
405
|
+
if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
|
|
406
|
+
this.#setState({
|
|
407
|
+
rejectionCount: this.store.state.rejectionCount + 1,
|
|
408
|
+
})
|
|
409
|
+
this.options.onReject?.(item, this)
|
|
410
|
+
return false
|
|
321
411
|
}
|
|
322
412
|
|
|
323
413
|
// Get priority either from the function or from getPriority option
|
|
324
414
|
const priority =
|
|
325
|
-
this.
|
|
326
|
-
? this.
|
|
327
|
-
: item.priority
|
|
415
|
+
this.options.getPriority !== defaultOptions.getPriority
|
|
416
|
+
? this.options.getPriority!(item)
|
|
417
|
+
: (item as any).priority
|
|
418
|
+
|
|
419
|
+
const items = this.store.state.items
|
|
420
|
+
const itemTimestamps = this.store.state.itemTimestamps
|
|
328
421
|
|
|
329
422
|
if (priority !== undefined) {
|
|
330
423
|
// Insert based on priority - higher priority items go to front
|
|
331
|
-
const insertIndex =
|
|
424
|
+
const insertIndex = items.findIndex((existing) => {
|
|
332
425
|
const existingPriority =
|
|
333
|
-
this.
|
|
334
|
-
? this.
|
|
426
|
+
this.options.getPriority !== defaultOptions.getPriority
|
|
427
|
+
? this.options.getPriority!(existing)
|
|
335
428
|
: (existing as any).priority
|
|
336
429
|
return existingPriority < priority
|
|
337
430
|
})
|
|
338
431
|
|
|
339
432
|
if (insertIndex === -1) {
|
|
340
|
-
|
|
341
|
-
|
|
433
|
+
items.push(item)
|
|
434
|
+
itemTimestamps.push(Date.now())
|
|
342
435
|
} else {
|
|
343
|
-
|
|
344
|
-
|
|
436
|
+
items.splice(insertIndex, 0, item)
|
|
437
|
+
itemTimestamps.splice(insertIndex, 0, Date.now())
|
|
345
438
|
}
|
|
346
439
|
} else {
|
|
347
440
|
if (position === 'front') {
|
|
348
441
|
// Default FIFO/LIFO behavior
|
|
349
|
-
|
|
350
|
-
|
|
442
|
+
items.unshift(item)
|
|
443
|
+
itemTimestamps.unshift(Date.now())
|
|
351
444
|
} else {
|
|
352
445
|
// LIFO
|
|
353
|
-
|
|
354
|
-
|
|
446
|
+
items.push(item)
|
|
447
|
+
itemTimestamps.push(Date.now())
|
|
355
448
|
}
|
|
356
449
|
}
|
|
357
450
|
|
|
451
|
+
this.#setState({
|
|
452
|
+
items,
|
|
453
|
+
itemTimestamps,
|
|
454
|
+
})
|
|
455
|
+
|
|
358
456
|
if (runOnItemsChange) {
|
|
359
|
-
this.
|
|
457
|
+
this.options.onItemsChange?.(this)
|
|
360
458
|
}
|
|
361
459
|
|
|
362
|
-
if (this.
|
|
363
|
-
this
|
|
364
|
-
this.tick()
|
|
460
|
+
if (this.store.state.isRunning && !this.store.state.pendingTick) {
|
|
461
|
+
this.#tick()
|
|
365
462
|
}
|
|
463
|
+
|
|
464
|
+
return true
|
|
366
465
|
}
|
|
367
466
|
|
|
368
467
|
/**
|
|
@@ -377,21 +476,32 @@ export class AsyncQueuer<TValue> {
|
|
|
377
476
|
* queuer.getNextItem('back');
|
|
378
477
|
* ```
|
|
379
478
|
*/
|
|
380
|
-
getNextItem(
|
|
381
|
-
position: QueuePosition = this.
|
|
382
|
-
): TValue | undefined {
|
|
479
|
+
getNextItem = (
|
|
480
|
+
position: QueuePosition = this.options.getItemsFrom ?? 'front',
|
|
481
|
+
): TValue | undefined => {
|
|
482
|
+
const { items, itemTimestamps } = this.store.state
|
|
383
483
|
let item: TValue | undefined
|
|
384
484
|
|
|
385
485
|
if (position === 'front') {
|
|
386
|
-
item =
|
|
387
|
-
|
|
486
|
+
item = items[0]
|
|
487
|
+
if (item !== undefined) {
|
|
488
|
+
this.#setState({
|
|
489
|
+
items: items.slice(1),
|
|
490
|
+
itemTimestamps: itemTimestamps.slice(1),
|
|
491
|
+
})
|
|
492
|
+
}
|
|
388
493
|
} else {
|
|
389
|
-
item =
|
|
390
|
-
|
|
494
|
+
item = items[items.length - 1]
|
|
495
|
+
if (item !== undefined) {
|
|
496
|
+
this.#setState({
|
|
497
|
+
items: items.slice(0, -1),
|
|
498
|
+
itemTimestamps: itemTimestamps.slice(0, -1),
|
|
499
|
+
})
|
|
500
|
+
}
|
|
391
501
|
}
|
|
392
502
|
|
|
393
503
|
if (item !== undefined) {
|
|
394
|
-
this.
|
|
504
|
+
this.options.onItemsChange?.(this)
|
|
395
505
|
}
|
|
396
506
|
|
|
397
507
|
return item
|
|
@@ -407,55 +517,78 @@ export class AsyncQueuer<TValue> {
|
|
|
407
517
|
* queuer.execute('back');
|
|
408
518
|
* ```
|
|
409
519
|
*/
|
|
410
|
-
async
|
|
520
|
+
execute = async (position?: QueuePosition): Promise<any> => {
|
|
411
521
|
const item = this.getNextItem(position)
|
|
412
522
|
if (item !== undefined) {
|
|
413
523
|
try {
|
|
414
|
-
|
|
415
|
-
this
|
|
416
|
-
|
|
524
|
+
const lastResult = await this.fn(item)
|
|
525
|
+
this.#setState({
|
|
526
|
+
successCount: this.store.state.successCount + 1,
|
|
527
|
+
lastResult,
|
|
528
|
+
})
|
|
529
|
+
this.options.onSuccess?.(lastResult, this)
|
|
417
530
|
} catch (error) {
|
|
418
|
-
this
|
|
419
|
-
|
|
420
|
-
|
|
531
|
+
this.#setState({
|
|
532
|
+
errorCount: this.store.state.errorCount + 1,
|
|
533
|
+
})
|
|
534
|
+
this.options.onError?.(error, this)
|
|
535
|
+
if (this.options.throwOnError) {
|
|
421
536
|
throw error
|
|
422
537
|
}
|
|
423
538
|
} finally {
|
|
424
|
-
this
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
539
|
+
this.#setState({
|
|
540
|
+
activeItems: this.store.state.activeItems.filter(
|
|
541
|
+
(activeItem) => activeItem !== item,
|
|
542
|
+
),
|
|
543
|
+
settledCount: this.store.state.settledCount + 1,
|
|
544
|
+
})
|
|
545
|
+
this.options.onSettled?.(this)
|
|
428
546
|
}
|
|
429
547
|
}
|
|
430
548
|
return item
|
|
431
549
|
}
|
|
432
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Processes a specified number of items to execute immediately with no wait time
|
|
553
|
+
* If no numberOfItems is provided, all items will be processed
|
|
554
|
+
*/
|
|
555
|
+
flush = (
|
|
556
|
+
numberOfItems: number = this.store.state.items.length,
|
|
557
|
+
position?: QueuePosition,
|
|
558
|
+
): void => {
|
|
559
|
+
this.#clearTimeouts() // clear any pending timeouts
|
|
560
|
+
for (let i = 0; i < numberOfItems; i++) {
|
|
561
|
+
this.execute(position)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
433
565
|
/**
|
|
434
566
|
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
|
|
435
567
|
* Internal use only.
|
|
436
568
|
*/
|
|
437
|
-
|
|
569
|
+
#checkExpiredItems = (): void => {
|
|
438
570
|
if (
|
|
439
|
-
this.
|
|
440
|
-
this.
|
|
441
|
-
)
|
|
571
|
+
(this.options.expirationDuration ?? Infinity) === Infinity &&
|
|
572
|
+
this.options.getIsExpired === defaultOptions.getIsExpired
|
|
573
|
+
) {
|
|
442
574
|
return
|
|
575
|
+
}
|
|
443
576
|
|
|
444
577
|
const now = Date.now()
|
|
445
578
|
const expiredIndices: Array<number> = []
|
|
446
579
|
|
|
447
580
|
// Find indices of expired items
|
|
448
|
-
for (let i = 0; i < this.
|
|
449
|
-
const timestamp = this.
|
|
581
|
+
for (let i = 0; i < this.store.state.items.length; i++) {
|
|
582
|
+
const timestamp = this.store.state.itemTimestamps[i]
|
|
450
583
|
if (timestamp === undefined) continue
|
|
451
584
|
|
|
452
|
-
const item = this.
|
|
585
|
+
const item = this.store.state.items[i]
|
|
453
586
|
if (item === undefined) continue
|
|
454
587
|
|
|
455
588
|
const isExpired =
|
|
456
|
-
this.
|
|
457
|
-
? this.
|
|
458
|
-
: now - timestamp > this.
|
|
589
|
+
this.options.getIsExpired !== defaultOptions.getIsExpired
|
|
590
|
+
? this.options.getIsExpired!(item, timestamp)
|
|
591
|
+
: now - timestamp > (this.options.expirationDuration ?? Infinity)
|
|
459
592
|
|
|
460
593
|
if (isExpired) {
|
|
461
594
|
expiredIndices.push(i)
|
|
@@ -467,17 +600,23 @@ export class AsyncQueuer<TValue> {
|
|
|
467
600
|
const index = expiredIndices[i]
|
|
468
601
|
if (index === undefined) continue
|
|
469
602
|
|
|
470
|
-
const expiredItem = this.
|
|
603
|
+
const expiredItem = this.store.state.items[index]
|
|
471
604
|
if (expiredItem === undefined) continue
|
|
472
605
|
|
|
473
|
-
this.
|
|
474
|
-
this.
|
|
475
|
-
|
|
476
|
-
|
|
606
|
+
const newItems = [...this.store.state.items]
|
|
607
|
+
const newTimestamps = [...this.store.state.itemTimestamps]
|
|
608
|
+
newItems.splice(index, 1)
|
|
609
|
+
newTimestamps.splice(index, 1)
|
|
610
|
+
this.#setState({
|
|
611
|
+
items: newItems,
|
|
612
|
+
itemTimestamps: newTimestamps,
|
|
613
|
+
expirationCount: this.store.state.expirationCount + 1,
|
|
614
|
+
})
|
|
615
|
+
this.options.onExpire?.(expiredItem, this)
|
|
477
616
|
}
|
|
478
617
|
|
|
479
618
|
if (expiredIndices.length > 0) {
|
|
480
|
-
this.
|
|
619
|
+
this.options.onItemsChange?.(this)
|
|
481
620
|
}
|
|
482
621
|
}
|
|
483
622
|
|
|
@@ -490,102 +629,71 @@ export class AsyncQueuer<TValue> {
|
|
|
490
629
|
* queuer.peekNextItem('back'); // back
|
|
491
630
|
* ```
|
|
492
631
|
*/
|
|
493
|
-
peekNextItem(position: QueuePosition = 'front'): TValue | undefined {
|
|
632
|
+
peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {
|
|
494
633
|
if (position === 'front') {
|
|
495
|
-
return this.
|
|
634
|
+
return this.store.state.items[0]
|
|
496
635
|
}
|
|
497
|
-
return this.
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Returns true if the queue is empty (no pending items).
|
|
502
|
-
*/
|
|
503
|
-
getIsEmpty(): boolean {
|
|
504
|
-
return this._items.length === 0
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Returns true if the queue is full (reached maxSize).
|
|
509
|
-
*/
|
|
510
|
-
getIsFull(): boolean {
|
|
511
|
-
return this._items.length >= this._options.maxSize
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Returns the number of pending items in the queue.
|
|
516
|
-
*/
|
|
517
|
-
getSize(): number {
|
|
518
|
-
return this._items.length
|
|
636
|
+
return this.store.state.items[this.store.state.items.length - 1]
|
|
519
637
|
}
|
|
520
638
|
|
|
521
639
|
/**
|
|
522
640
|
* Returns a copy of all items in the queue, including active and pending items.
|
|
523
641
|
*/
|
|
524
|
-
peekAllItems(): Array<TValue> {
|
|
642
|
+
peekAllItems = (): Array<TValue> => {
|
|
525
643
|
return [...this.peekActiveItems(), ...this.peekPendingItems()]
|
|
526
644
|
}
|
|
527
645
|
|
|
528
646
|
/**
|
|
529
647
|
* Returns the items currently being processed (active tasks).
|
|
530
648
|
*/
|
|
531
|
-
peekActiveItems(): Array<TValue> {
|
|
532
|
-
return
|
|
649
|
+
peekActiveItems = (): Array<TValue> => {
|
|
650
|
+
return [...this.store.state.activeItems]
|
|
533
651
|
}
|
|
534
652
|
|
|
535
653
|
/**
|
|
536
654
|
* Returns the items waiting to be processed (pending tasks).
|
|
537
655
|
*/
|
|
538
|
-
peekPendingItems(): Array<TValue> {
|
|
539
|
-
return [...this.
|
|
656
|
+
peekPendingItems = (): Array<TValue> => {
|
|
657
|
+
return [...this.store.state.items]
|
|
540
658
|
}
|
|
541
659
|
|
|
542
660
|
/**
|
|
543
|
-
*
|
|
544
|
-
*/
|
|
545
|
-
getSuccessCount(): number {
|
|
546
|
-
return this._successCount
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Returns the number of items that have failed processing.
|
|
551
|
-
*/
|
|
552
|
-
getErrorCount(): number {
|
|
553
|
-
return this._errorCount
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Returns the number of items that have completed processing (success or error).
|
|
661
|
+
* Starts processing items in the queue. If already running, does nothing.
|
|
558
662
|
*/
|
|
559
|
-
|
|
560
|
-
|
|
663
|
+
start = (): void => {
|
|
664
|
+
this.#setState({ isRunning: true })
|
|
665
|
+
if (!this.store.state.pendingTick && this.store.state.items.length > 0) {
|
|
666
|
+
this.#tick()
|
|
667
|
+
}
|
|
561
668
|
}
|
|
562
669
|
|
|
563
670
|
/**
|
|
564
|
-
*
|
|
671
|
+
* Stops processing items in the queue. Does not clear the queue.
|
|
565
672
|
*/
|
|
566
|
-
|
|
567
|
-
|
|
673
|
+
stop = (): void => {
|
|
674
|
+
this.#clearTimeouts()
|
|
675
|
+
this.#setState({ isRunning: false, pendingTick: false })
|
|
568
676
|
}
|
|
569
677
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
getIsRunning(): boolean {
|
|
574
|
-
return this._running
|
|
678
|
+
#clearTimeouts = (): void => {
|
|
679
|
+
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))
|
|
680
|
+
this.#timeoutIds.clear()
|
|
575
681
|
}
|
|
576
682
|
|
|
577
683
|
/**
|
|
578
|
-
*
|
|
684
|
+
* Removes all pending items from the queue. Does not affect active tasks.
|
|
579
685
|
*/
|
|
580
|
-
|
|
581
|
-
|
|
686
|
+
clear = (): void => {
|
|
687
|
+
this.#setState({ items: [], itemTimestamps: [] })
|
|
688
|
+
this.options.onItemsChange?.(this)
|
|
582
689
|
}
|
|
583
690
|
|
|
584
691
|
/**
|
|
585
|
-
*
|
|
692
|
+
* Resets the queuer state to its default values
|
|
586
693
|
*/
|
|
587
|
-
|
|
588
|
-
|
|
694
|
+
reset = (): void => {
|
|
695
|
+
this.#setState(getDefaultAsyncQueuerState<TValue>())
|
|
696
|
+
this.options.onItemsChange?.(this)
|
|
589
697
|
}
|
|
590
698
|
}
|
|
591
699
|
|
|
@@ -600,6 +708,19 @@ export class AsyncQueuer<TValue> {
|
|
|
600
708
|
* - Both onError and throwOnError can be used together; the handler will be called before any error is thrown
|
|
601
709
|
* - The error state can be checked using the underlying AsyncQueuer instance
|
|
602
710
|
*
|
|
711
|
+
* State Management:
|
|
712
|
+
* - Uses TanStack Store for reactive state management
|
|
713
|
+
* - Use `initialState` to provide initial state values when creating the async queuer
|
|
714
|
+
* - Use `onSuccess` callback to react to successful task execution and implement custom logic
|
|
715
|
+
* - Use `onError` callback to react to task execution errors and implement custom error handling
|
|
716
|
+
* - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic
|
|
717
|
+
* - Use `onItemsChange` callback to react to items being added or removed from the queue
|
|
718
|
+
* - Use `onExpire` callback to react to items expiring and implement custom logic
|
|
719
|
+
* - Use `onReject` callback to react to items being rejected when the queue is full
|
|
720
|
+
* - The state includes error count, expiration count, rejection count, running status, and success/settle counts
|
|
721
|
+
* - State can be accessed via the underlying AsyncQueuer instance's `store.state` property
|
|
722
|
+
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
|
|
723
|
+
*
|
|
603
724
|
* Example usage:
|
|
604
725
|
* ```ts
|
|
605
726
|
* const enqueue = asyncQueue<string>(async (item) => {
|
|
@@ -614,5 +735,5 @@ export function asyncQueue<TValue>(
|
|
|
614
735
|
initialOptions: AsyncQueuerOptions<TValue>,
|
|
615
736
|
) {
|
|
616
737
|
const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)
|
|
617
|
-
return asyncQueuer.addItem
|
|
738
|
+
return asyncQueuer.addItem
|
|
618
739
|
}
|