@tanstack/pacer 0.8.0 → 0.9.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-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 +246 -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 +246 -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 +348 -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/queuer.ts
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
|
+
import { Store } from '@tanstack/store'
|
|
1
2
|
import { parseFunctionOrValue } from './utils'
|
|
2
3
|
|
|
4
|
+
export interface QueuerState<TValue> {
|
|
5
|
+
/**
|
|
6
|
+
* Number of items that have been processed by the queuer
|
|
7
|
+
*/
|
|
8
|
+
executionCount: number
|
|
9
|
+
/**
|
|
10
|
+
* Number of items that have been removed from the queue due to expiration
|
|
11
|
+
*/
|
|
12
|
+
expirationCount: number
|
|
13
|
+
/**
|
|
14
|
+
* Whether the queuer has no items to process (items array is empty)
|
|
15
|
+
*/
|
|
16
|
+
isEmpty: boolean
|
|
17
|
+
/**
|
|
18
|
+
* Whether the queuer has reached its maximum capacity
|
|
19
|
+
*/
|
|
20
|
+
isFull: boolean
|
|
21
|
+
/**
|
|
22
|
+
* Whether the queuer is not currently processing any items
|
|
23
|
+
*/
|
|
24
|
+
isIdle: boolean
|
|
25
|
+
/**
|
|
26
|
+
* Whether the queuer is active and will process items automatically
|
|
27
|
+
*/
|
|
28
|
+
isRunning: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Timestamps when items were added to the queue for expiration tracking
|
|
31
|
+
*/
|
|
32
|
+
itemTimestamps: Array<number>
|
|
33
|
+
/**
|
|
34
|
+
* Array of items currently waiting to be processed
|
|
35
|
+
*/
|
|
36
|
+
items: Array<TValue>
|
|
37
|
+
/**
|
|
38
|
+
* Whether the queuer has a pending timeout for processing the next item
|
|
39
|
+
*/
|
|
40
|
+
pendingTick: boolean
|
|
41
|
+
/**
|
|
42
|
+
* Number of items that have been rejected from being added to the queue
|
|
43
|
+
*/
|
|
44
|
+
rejectionCount: number
|
|
45
|
+
/**
|
|
46
|
+
* Number of items currently in the queue
|
|
47
|
+
*/
|
|
48
|
+
size: number
|
|
49
|
+
/**
|
|
50
|
+
* Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused
|
|
51
|
+
*/
|
|
52
|
+
status: 'idle' | 'running' | 'stopped'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getDefaultQueuerState<TValue>(): QueuerState<TValue> {
|
|
56
|
+
return {
|
|
57
|
+
executionCount: 0,
|
|
58
|
+
expirationCount: 0,
|
|
59
|
+
isEmpty: true,
|
|
60
|
+
isFull: false,
|
|
61
|
+
isIdle: true,
|
|
62
|
+
isRunning: true,
|
|
63
|
+
itemTimestamps: [],
|
|
64
|
+
items: [],
|
|
65
|
+
pendingTick: false,
|
|
66
|
+
rejectionCount: 0,
|
|
67
|
+
size: 0,
|
|
68
|
+
status: 'idle',
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
3
72
|
/**
|
|
4
73
|
* Options for configuring a Queuer instance.
|
|
5
74
|
*
|
|
@@ -35,6 +104,10 @@ export interface QueuerOptions<TValue> {
|
|
|
35
104
|
* Initial items to populate the queuer with
|
|
36
105
|
*/
|
|
37
106
|
initialItems?: Array<TValue>
|
|
107
|
+
/**
|
|
108
|
+
* Initial state for the queuer
|
|
109
|
+
*/
|
|
110
|
+
initialState?: Partial<QueuerState<TValue>>
|
|
38
111
|
/**
|
|
39
112
|
* Maximum number of items allowed in the queuer
|
|
40
113
|
*/
|
|
@@ -47,10 +120,6 @@ export interface QueuerOptions<TValue> {
|
|
|
47
120
|
* Callback fired whenever an item is removed from the queuer
|
|
48
121
|
*/
|
|
49
122
|
onExecute?: (item: TValue, queuer: Queuer<TValue>) => void
|
|
50
|
-
/**
|
|
51
|
-
* Callback fired whenever the queuer's running state changes
|
|
52
|
-
*/
|
|
53
|
-
onIsRunningChange?: (queuer: Queuer<TValue>) => void
|
|
54
123
|
/**
|
|
55
124
|
* Callback fired whenever an item is added or removed from the queuer
|
|
56
125
|
*/
|
|
@@ -71,7 +140,15 @@ export interface QueuerOptions<TValue> {
|
|
|
71
140
|
wait?: number | ((queuer: Queuer<TValue>) => number)
|
|
72
141
|
}
|
|
73
142
|
|
|
74
|
-
const defaultOptions:
|
|
143
|
+
const defaultOptions: Omit<
|
|
144
|
+
Required<QueuerOptions<any>>,
|
|
145
|
+
| 'initialState'
|
|
146
|
+
| 'onExecute'
|
|
147
|
+
| 'onIsRunningChange'
|
|
148
|
+
| 'onItemsChange'
|
|
149
|
+
| 'onReject'
|
|
150
|
+
| 'onExpire'
|
|
151
|
+
> = {
|
|
75
152
|
addItemsTo: 'back',
|
|
76
153
|
getItemsFrom: 'front',
|
|
77
154
|
getPriority: (item) => item?.priority ?? 0,
|
|
@@ -79,11 +156,6 @@ const defaultOptions: Required<QueuerOptions<any>> = {
|
|
|
79
156
|
expirationDuration: Infinity,
|
|
80
157
|
initialItems: [],
|
|
81
158
|
maxSize: Infinity,
|
|
82
|
-
onExecute: () => {},
|
|
83
|
-
onIsRunningChange: () => {},
|
|
84
|
-
onItemsChange: () => {},
|
|
85
|
-
onReject: () => {},
|
|
86
|
-
onExpire: () => {},
|
|
87
159
|
started: true,
|
|
88
160
|
wait: 0,
|
|
89
161
|
}
|
|
@@ -107,7 +179,7 @@ export type QueuePosition = 'front' | 'back'
|
|
|
107
179
|
* - Callbacks for queue state changes, execution, rejection, and expiration
|
|
108
180
|
*
|
|
109
181
|
* Running behavior:
|
|
110
|
-
* - `start()`: Begins automatically processing items in the queue (defaults to
|
|
182
|
+
* - `start()`: Begins automatically processing items in the queue (defaults to isRunning)
|
|
111
183
|
* - `stop()`: Pauses processing but maintains queue state
|
|
112
184
|
* - `wait`: Configurable delay between processing items
|
|
113
185
|
* - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state
|
|
@@ -136,6 +208,17 @@ export type QueuePosition = 'front' | 'back'
|
|
|
136
208
|
* - `getIsExpired`: Function to override default expiration
|
|
137
209
|
* - `onExpire`: Callback for expired items
|
|
138
210
|
*
|
|
211
|
+
* State Management:
|
|
212
|
+
* - Uses TanStack Store for reactive state management
|
|
213
|
+
* - Use `initialState` to provide initial state values when creating the queuer
|
|
214
|
+
* - Use `onExecute` callback to react to item execution and implement custom logic
|
|
215
|
+
* - Use `onItemsChange` callback to react to items being added or removed from the queue
|
|
216
|
+
* - Use `onExpire` callback to react to items expiring and implement custom logic
|
|
217
|
+
* - Use `onReject` callback to react to items being rejected when the queue is full
|
|
218
|
+
* - The state includes execution count, expiration count, rejection count, and isRunning status
|
|
219
|
+
* - State can be accessed via `queuer.store.state` when using the class directly
|
|
220
|
+
* - When using framework adapters (React/Solid), state is accessed from `queuer.state`
|
|
221
|
+
*
|
|
139
222
|
* Example usage:
|
|
140
223
|
* ```ts
|
|
141
224
|
* // Auto-processing queue with wait time
|
|
@@ -158,174 +241,112 @@ export type QueuePosition = 'front' | 'back'
|
|
|
158
241
|
* ```
|
|
159
242
|
*/
|
|
160
243
|
export class Queuer<TValue> {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
private _expirationCount = 0
|
|
167
|
-
private _onItemsChanges: Array<(item: TValue) => void> = []
|
|
168
|
-
private _running: boolean
|
|
169
|
-
private _pendingTick = false
|
|
244
|
+
readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(
|
|
245
|
+
getDefaultQueuerState<TValue>(),
|
|
246
|
+
)
|
|
247
|
+
options: QueuerOptions<TValue>
|
|
248
|
+
#timeoutId: NodeJS.Timeout | null = null
|
|
170
249
|
|
|
171
250
|
constructor(
|
|
172
251
|
private fn: (item: TValue) => void,
|
|
173
252
|
initialOptions: QueuerOptions<TValue> = {},
|
|
174
253
|
) {
|
|
175
|
-
this.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
254
|
+
this.options = {
|
|
255
|
+
...defaultOptions,
|
|
256
|
+
...initialOptions,
|
|
257
|
+
}
|
|
258
|
+
const isInitiallyRunning =
|
|
259
|
+
this.options.initialState?.isRunning ?? this.options.started ?? true
|
|
260
|
+
this.#setState({
|
|
261
|
+
...this.options.initialState,
|
|
262
|
+
isRunning: isInitiallyRunning,
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
if (this.options.initialState?.items) {
|
|
266
|
+
if (this.store.state.isRunning) {
|
|
267
|
+
this.#tick()
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {
|
|
271
|
+
const item = this.options.initialItems![i]!
|
|
272
|
+
const isLast = i === (this.options.initialItems?.length ?? 0) - 1
|
|
273
|
+
this.addItem(item, this.options.addItemsTo ?? 'back', isLast)
|
|
274
|
+
}
|
|
182
275
|
}
|
|
183
276
|
}
|
|
184
277
|
|
|
185
278
|
/**
|
|
186
279
|
* Updates the queuer options. New options are merged with existing options.
|
|
187
280
|
*/
|
|
188
|
-
setOptions(newOptions: Partial<QueuerOptions<TValue>>): void {
|
|
189
|
-
this.
|
|
281
|
+
setOptions = (newOptions: Partial<QueuerOptions<TValue>>): void => {
|
|
282
|
+
this.options = { ...this.options, ...newOptions }
|
|
190
283
|
}
|
|
191
284
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
285
|
+
#setState = (newState: Partial<QueuerState<TValue>>): void => {
|
|
286
|
+
this.store.setState((state) => {
|
|
287
|
+
const combinedState = {
|
|
288
|
+
...state,
|
|
289
|
+
...newState,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const { items, isRunning } = combinedState
|
|
293
|
+
|
|
294
|
+
const size = items.length
|
|
295
|
+
const isFull = size >= (this.options.maxSize ?? Infinity)
|
|
296
|
+
const isEmpty = size === 0
|
|
297
|
+
const isIdle = isRunning && isEmpty
|
|
298
|
+
|
|
299
|
+
const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
...combinedState,
|
|
303
|
+
isEmpty,
|
|
304
|
+
isFull,
|
|
305
|
+
isIdle,
|
|
306
|
+
size,
|
|
307
|
+
status,
|
|
308
|
+
}
|
|
309
|
+
})
|
|
197
310
|
}
|
|
198
311
|
|
|
199
312
|
/**
|
|
200
313
|
* Returns the current wait time (in milliseconds) between processing items.
|
|
201
314
|
* If a function is provided, it is called with the queuer instance.
|
|
202
315
|
*/
|
|
203
|
-
getWait(): number {
|
|
204
|
-
return parseFunctionOrValue(this.
|
|
316
|
+
#getWait = (): number => {
|
|
317
|
+
return parseFunctionOrValue(this.options.wait ?? 0, this)
|
|
205
318
|
}
|
|
206
319
|
|
|
207
320
|
/**
|
|
208
321
|
* Processes items in the queue up to the wait interval. Internal use only.
|
|
209
322
|
*/
|
|
210
|
-
|
|
211
|
-
if (!this.
|
|
212
|
-
this
|
|
323
|
+
#tick = () => {
|
|
324
|
+
if (!this.store.state.isRunning) {
|
|
325
|
+
this.#setState({ pendingTick: false })
|
|
213
326
|
return
|
|
214
327
|
}
|
|
215
328
|
|
|
329
|
+
this.#setState({ pendingTick: true })
|
|
330
|
+
|
|
216
331
|
// Check for expired items
|
|
217
|
-
this
|
|
332
|
+
this.#checkExpiredItems()
|
|
218
333
|
|
|
219
|
-
while (!this.
|
|
220
|
-
const nextItem = this.execute(this.
|
|
334
|
+
while (!this.store.state.isEmpty) {
|
|
335
|
+
const nextItem = this.execute(this.options.getItemsFrom ?? 'front')
|
|
221
336
|
if (nextItem === undefined) {
|
|
222
337
|
break
|
|
223
338
|
}
|
|
224
|
-
this._onItemsChanges.forEach((cb) => cb(nextItem))
|
|
225
339
|
|
|
226
|
-
const wait = this
|
|
340
|
+
const wait = this.#getWait()
|
|
227
341
|
if (wait > 0) {
|
|
228
342
|
// Use setTimeout to wait before processing next item
|
|
229
|
-
setTimeout(() => this
|
|
343
|
+
this.#timeoutId = setTimeout(() => this.#tick(), wait)
|
|
230
344
|
return
|
|
231
345
|
}
|
|
232
346
|
|
|
233
|
-
this
|
|
234
|
-
}
|
|
235
|
-
this._pendingTick = false
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
|
|
240
|
-
* Internal use only.
|
|
241
|
-
*/
|
|
242
|
-
private checkExpiredItems() {
|
|
243
|
-
if (
|
|
244
|
-
this._options.expirationDuration === Infinity &&
|
|
245
|
-
this._options.getIsExpired === defaultOptions.getIsExpired
|
|
246
|
-
)
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
const now = Date.now()
|
|
250
|
-
const expiredIndices: Array<number> = []
|
|
251
|
-
|
|
252
|
-
// Find indices of expired items
|
|
253
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
254
|
-
const timestamp = this._itemTimestamps[i]
|
|
255
|
-
if (timestamp === undefined) continue
|
|
256
|
-
|
|
257
|
-
const item = this._items[i]
|
|
258
|
-
if (item === undefined) continue
|
|
259
|
-
|
|
260
|
-
const isExpired =
|
|
261
|
-
this._options.getIsExpired !== defaultOptions.getIsExpired
|
|
262
|
-
? this._options.getIsExpired(item, timestamp)
|
|
263
|
-
: now - timestamp > this._options.expirationDuration
|
|
264
|
-
|
|
265
|
-
if (isExpired) {
|
|
266
|
-
expiredIndices.push(i)
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Remove expired items from back to front to maintain indices
|
|
271
|
-
for (let i = expiredIndices.length - 1; i >= 0; i--) {
|
|
272
|
-
const index = expiredIndices[i]
|
|
273
|
-
if (index === undefined) continue
|
|
274
|
-
|
|
275
|
-
const expiredItem = this._items[index]
|
|
276
|
-
if (expiredItem === undefined) continue
|
|
277
|
-
|
|
278
|
-
this._items.splice(index, 1)
|
|
279
|
-
this._itemTimestamps.splice(index, 1)
|
|
280
|
-
this._expirationCount++
|
|
281
|
-
this._options.onExpire(expiredItem, this)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (expiredIndices.length > 0) {
|
|
285
|
-
this._options.onItemsChange(this)
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Stops processing items in the queue. Does not clear the queue.
|
|
291
|
-
*/
|
|
292
|
-
stop() {
|
|
293
|
-
this._running = false
|
|
294
|
-
this._pendingTick = false
|
|
295
|
-
this._options.onIsRunningChange(this)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Starts processing items in the queue. If already running, does nothing.
|
|
300
|
-
*/
|
|
301
|
-
start() {
|
|
302
|
-
this._running = true
|
|
303
|
-
if (!this._pendingTick && !this.getIsEmpty()) {
|
|
304
|
-
this._pendingTick = true
|
|
305
|
-
this.tick()
|
|
347
|
+
this.#tick()
|
|
306
348
|
}
|
|
307
|
-
this
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Removes all pending items from the queue. Does not affect items being processed.
|
|
312
|
-
*/
|
|
313
|
-
clear(): void {
|
|
314
|
-
this._items = []
|
|
315
|
-
this._options.onItemsChange(this)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Resets the queuer to its initial state. Optionally repopulates with initial items.
|
|
320
|
-
* Does not affect callbacks or options.
|
|
321
|
-
*/
|
|
322
|
-
reset(withInitialItems?: boolean): void {
|
|
323
|
-
this.clear()
|
|
324
|
-
this._executionCount = 0
|
|
325
|
-
if (withInitialItems) {
|
|
326
|
-
this._items = [...this._options.initialItems]
|
|
327
|
-
}
|
|
328
|
-
this._running = this._options.started
|
|
349
|
+
this.#setState({ pendingTick: false })
|
|
329
350
|
}
|
|
330
351
|
|
|
331
352
|
/**
|
|
@@ -340,49 +361,71 @@ export class Queuer<TValue> {
|
|
|
340
361
|
* queuer.addItem('task2', 'front');
|
|
341
362
|
* ```
|
|
342
363
|
*/
|
|
343
|
-
addItem(
|
|
364
|
+
addItem = (
|
|
344
365
|
item: TValue,
|
|
345
|
-
position: QueuePosition = this.
|
|
346
|
-
|
|
347
|
-
): boolean {
|
|
348
|
-
if (this.
|
|
349
|
-
this
|
|
350
|
-
|
|
366
|
+
position: QueuePosition = this.options.addItemsTo ?? 'back',
|
|
367
|
+
runOnItemsChange: boolean = true,
|
|
368
|
+
): boolean => {
|
|
369
|
+
if (this.store.state.isFull) {
|
|
370
|
+
this.#setState({
|
|
371
|
+
rejectionCount: this.store.state.rejectionCount + 1,
|
|
372
|
+
})
|
|
373
|
+
this.options.onReject?.(item, this)
|
|
351
374
|
return false
|
|
352
375
|
}
|
|
353
376
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
(
|
|
359
|
-
|
|
377
|
+
// Get priority either from the function or from getPriority option
|
|
378
|
+
const priority =
|
|
379
|
+
this.options.getPriority !== defaultOptions.getPriority
|
|
380
|
+
? this.options.getPriority!(item)
|
|
381
|
+
: (item as any).priority
|
|
382
|
+
|
|
383
|
+
const items = this.store.state.items
|
|
384
|
+
const itemTimestamps = this.store.state.itemTimestamps
|
|
385
|
+
|
|
386
|
+
if (priority !== undefined) {
|
|
387
|
+
// Insert based on priority - higher priority items go to front
|
|
388
|
+
const insertIndex = items.findIndex((existing) => {
|
|
389
|
+
const existingPriority: number =
|
|
390
|
+
this.options.getPriority !== defaultOptions.getPriority
|
|
391
|
+
? this.options.getPriority!(existing)
|
|
392
|
+
: (existing as any).priority
|
|
393
|
+
return existingPriority < priority
|
|
394
|
+
})
|
|
360
395
|
|
|
361
396
|
if (insertIndex === -1) {
|
|
362
|
-
|
|
363
|
-
|
|
397
|
+
items.push(item)
|
|
398
|
+
itemTimestamps.push(Date.now())
|
|
364
399
|
} else {
|
|
365
|
-
|
|
366
|
-
|
|
400
|
+
items.splice(insertIndex, 0, item)
|
|
401
|
+
itemTimestamps.splice(insertIndex, 0, Date.now())
|
|
367
402
|
}
|
|
368
403
|
} else {
|
|
369
|
-
// Default FIFO/LIFO behavior
|
|
370
404
|
if (position === 'front') {
|
|
371
|
-
|
|
372
|
-
|
|
405
|
+
// Default FIFO/LIFO behavior
|
|
406
|
+
items.unshift(item)
|
|
407
|
+
itemTimestamps.unshift(Date.now())
|
|
373
408
|
} else {
|
|
374
|
-
|
|
375
|
-
|
|
409
|
+
// LIFO
|
|
410
|
+
items.push(item)
|
|
411
|
+
itemTimestamps.push(Date.now())
|
|
376
412
|
}
|
|
377
413
|
}
|
|
378
414
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
415
|
+
this.#setState({
|
|
416
|
+
items,
|
|
417
|
+
itemTimestamps,
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
if (runOnItemsChange) {
|
|
421
|
+
this.options.onItemsChange?.(this)
|
|
382
422
|
}
|
|
383
|
-
|
|
384
|
-
|
|
423
|
+
|
|
424
|
+
if (this.store.state.isRunning && !this.store.state.pendingTick) {
|
|
425
|
+
this.#setState({ pendingTick: true })
|
|
426
|
+
this.#tick()
|
|
385
427
|
}
|
|
428
|
+
|
|
386
429
|
return true
|
|
387
430
|
}
|
|
388
431
|
|
|
@@ -398,21 +441,32 @@ export class Queuer<TValue> {
|
|
|
398
441
|
* queuer.getNextItem('back');
|
|
399
442
|
* ```
|
|
400
443
|
*/
|
|
401
|
-
getNextItem(
|
|
402
|
-
position: QueuePosition = this.
|
|
403
|
-
): TValue | undefined {
|
|
444
|
+
getNextItem = (
|
|
445
|
+
position: QueuePosition = this.options.getItemsFrom ?? 'front',
|
|
446
|
+
): TValue | undefined => {
|
|
447
|
+
const { items, itemTimestamps } = this.store.state
|
|
404
448
|
let item: TValue | undefined
|
|
405
449
|
|
|
406
450
|
if (position === 'front') {
|
|
407
|
-
item =
|
|
408
|
-
|
|
451
|
+
item = items[0]
|
|
452
|
+
if (item !== undefined) {
|
|
453
|
+
this.#setState({
|
|
454
|
+
items: items.slice(1),
|
|
455
|
+
itemTimestamps: itemTimestamps.slice(1),
|
|
456
|
+
})
|
|
457
|
+
}
|
|
409
458
|
} else {
|
|
410
|
-
item =
|
|
411
|
-
|
|
459
|
+
item = items[items.length - 1]
|
|
460
|
+
if (item !== undefined) {
|
|
461
|
+
this.#setState({
|
|
462
|
+
items: items.slice(0, -1),
|
|
463
|
+
itemTimestamps: itemTimestamps.slice(0, -1),
|
|
464
|
+
})
|
|
465
|
+
}
|
|
412
466
|
}
|
|
413
467
|
|
|
414
468
|
if (item !== undefined) {
|
|
415
|
-
this.
|
|
469
|
+
this.options.onItemsChange?.(this)
|
|
416
470
|
}
|
|
417
471
|
|
|
418
472
|
return item
|
|
@@ -428,95 +482,152 @@ export class Queuer<TValue> {
|
|
|
428
482
|
* queuer.execute('back');
|
|
429
483
|
* ```
|
|
430
484
|
*/
|
|
431
|
-
execute(position?: QueuePosition): TValue | undefined {
|
|
485
|
+
execute = (position?: QueuePosition): TValue | undefined => {
|
|
432
486
|
const item = this.getNextItem(position)
|
|
433
487
|
if (item !== undefined) {
|
|
434
488
|
this.fn(item)
|
|
435
|
-
this
|
|
436
|
-
|
|
489
|
+
this.#setState({
|
|
490
|
+
executionCount: this.store.state.executionCount + 1,
|
|
491
|
+
})
|
|
492
|
+
this.options.onExecute?.(item, this)
|
|
437
493
|
}
|
|
438
494
|
return item
|
|
439
495
|
}
|
|
440
496
|
|
|
441
497
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* Example usage:
|
|
445
|
-
* ```ts
|
|
446
|
-
* queuer.peekNextItem(); // front
|
|
447
|
-
* queuer.peekNextItem('back'); // back
|
|
448
|
-
* ```
|
|
498
|
+
* Processes a specified number of items to execute immediately with no wait time
|
|
499
|
+
* If no numberOfItems is provided, all items will be processed
|
|
449
500
|
*/
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
501
|
+
flush = (
|
|
502
|
+
numberOfItems: number = this.store.state.items.length,
|
|
503
|
+
position?: QueuePosition,
|
|
504
|
+
): void => {
|
|
505
|
+
this.#clearTimeout() // clear any pending timeout
|
|
506
|
+
for (let i = 0; i < numberOfItems; i++) {
|
|
507
|
+
this.execute(position)
|
|
455
508
|
}
|
|
456
|
-
return this._items[this._items.length - 1]
|
|
457
509
|
}
|
|
458
510
|
|
|
459
511
|
/**
|
|
460
|
-
*
|
|
512
|
+
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
|
|
513
|
+
* Internal use only.
|
|
461
514
|
*/
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
515
|
+
#checkExpiredItems = (): void => {
|
|
516
|
+
if (
|
|
517
|
+
(this.options.expirationDuration ?? Infinity) === Infinity &&
|
|
518
|
+
this.options.getIsExpired === defaultOptions.getIsExpired
|
|
519
|
+
) {
|
|
520
|
+
return
|
|
521
|
+
}
|
|
465
522
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
523
|
+
const now = Date.now()
|
|
524
|
+
const expiredIndices: Array<number> = []
|
|
525
|
+
|
|
526
|
+
// Find indices of expired items
|
|
527
|
+
for (let i = 0; i < this.store.state.items.length; i++) {
|
|
528
|
+
const timestamp = this.store.state.itemTimestamps[i]
|
|
529
|
+
if (timestamp === undefined) continue
|
|
530
|
+
|
|
531
|
+
const item = this.store.state.items[i]
|
|
532
|
+
if (item === undefined) continue
|
|
533
|
+
|
|
534
|
+
const isExpired =
|
|
535
|
+
this.options.getIsExpired !== defaultOptions.getIsExpired
|
|
536
|
+
? this.options.getIsExpired!(item, timestamp)
|
|
537
|
+
: now - timestamp > (this.options.expirationDuration ?? Infinity)
|
|
538
|
+
|
|
539
|
+
if (isExpired) {
|
|
540
|
+
expiredIndices.push(i)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Remove expired items from back to front to maintain indices
|
|
545
|
+
for (let i = expiredIndices.length - 1; i >= 0; i--) {
|
|
546
|
+
const index = expiredIndices[i]
|
|
547
|
+
if (index === undefined) continue
|
|
548
|
+
|
|
549
|
+
const expiredItem = this.store.state.items[index]
|
|
550
|
+
if (expiredItem === undefined) continue
|
|
551
|
+
|
|
552
|
+
const newItems = [...this.store.state.items]
|
|
553
|
+
const newTimestamps = [...this.store.state.itemTimestamps]
|
|
554
|
+
newItems.splice(index, 1)
|
|
555
|
+
newTimestamps.splice(index, 1)
|
|
556
|
+
this.#setState({
|
|
557
|
+
items: newItems,
|
|
558
|
+
itemTimestamps: newTimestamps,
|
|
559
|
+
expirationCount: this.store.state.expirationCount + 1,
|
|
560
|
+
})
|
|
561
|
+
this.options.onExpire?.(expiredItem, this)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (expiredIndices.length > 0) {
|
|
565
|
+
this.options.onItemsChange?.(this)
|
|
566
|
+
}
|
|
471
567
|
}
|
|
472
568
|
|
|
473
569
|
/**
|
|
474
|
-
* Returns the
|
|
570
|
+
* Returns the next item in the queue without removing it.
|
|
571
|
+
*
|
|
572
|
+
* Example usage:
|
|
573
|
+
* ```ts
|
|
574
|
+
* queuer.peekNextItem(); // front
|
|
575
|
+
* queuer.peekNextItem('back'); // back
|
|
576
|
+
* ```
|
|
475
577
|
*/
|
|
476
|
-
|
|
477
|
-
|
|
578
|
+
peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {
|
|
579
|
+
if (position === 'front') {
|
|
580
|
+
return this.store.state.items[0]
|
|
581
|
+
}
|
|
582
|
+
return this.store.state.items[this.store.state.size - 1]
|
|
478
583
|
}
|
|
479
584
|
|
|
480
585
|
/**
|
|
481
586
|
* Returns a copy of all items in the queue.
|
|
482
587
|
*/
|
|
483
|
-
peekAllItems(): Array<TValue> {
|
|
484
|
-
return [...this.
|
|
588
|
+
peekAllItems = (): Array<TValue> => {
|
|
589
|
+
return [...this.store.state.items]
|
|
485
590
|
}
|
|
486
591
|
|
|
487
592
|
/**
|
|
488
|
-
*
|
|
593
|
+
* Starts processing items in the queue. If already isRunning, does nothing.
|
|
489
594
|
*/
|
|
490
|
-
|
|
491
|
-
|
|
595
|
+
start = () => {
|
|
596
|
+
this.#setState({ isRunning: true })
|
|
597
|
+
if (!this.store.state.pendingTick && !this.store.state.isEmpty) {
|
|
598
|
+
this.#tick()
|
|
599
|
+
}
|
|
492
600
|
}
|
|
493
601
|
|
|
494
602
|
/**
|
|
495
|
-
*
|
|
603
|
+
* Stops processing items in the queue. Does not clear the queue.
|
|
496
604
|
*/
|
|
497
|
-
|
|
498
|
-
|
|
605
|
+
stop = () => {
|
|
606
|
+
this.#clearTimeout()
|
|
607
|
+
this.#setState({ isRunning: false, pendingTick: false })
|
|
499
608
|
}
|
|
500
609
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
610
|
+
#clearTimeout = (): void => {
|
|
611
|
+
if (this.#timeoutId) {
|
|
612
|
+
clearTimeout(this.#timeoutId)
|
|
613
|
+
this.#timeoutId = null
|
|
614
|
+
}
|
|
506
615
|
}
|
|
507
616
|
|
|
508
617
|
/**
|
|
509
|
-
*
|
|
618
|
+
* Removes all pending items from the queue. Does not affect items being processed.
|
|
510
619
|
*/
|
|
511
|
-
|
|
512
|
-
|
|
620
|
+
clear = (): void => {
|
|
621
|
+
this.#setState({ items: [], itemTimestamps: [] })
|
|
622
|
+
this.options.onItemsChange?.(this)
|
|
513
623
|
}
|
|
514
624
|
|
|
515
625
|
/**
|
|
516
|
-
*
|
|
626
|
+
* Resets the queuer state to its default values
|
|
517
627
|
*/
|
|
518
|
-
|
|
519
|
-
|
|
628
|
+
reset = (): void => {
|
|
629
|
+
this.#setState(getDefaultQueuerState<TValue>())
|
|
630
|
+
this.options.onItemsChange?.(this)
|
|
520
631
|
}
|
|
521
632
|
}
|
|
522
633
|
|
|
@@ -525,9 +636,20 @@ export class Queuer<TValue> {
|
|
|
525
636
|
* Items are processed sequentially in FIFO order by default.
|
|
526
637
|
*
|
|
527
638
|
* This is a simplified wrapper around the Queuer class that only exposes the
|
|
528
|
-
* `addItem` method. The queue is always
|
|
639
|
+
* `addItem` method. The queue is always isRunning and will process items as they are added.
|
|
529
640
|
* For more control over queue processing, use the Queuer class directly.
|
|
530
641
|
*
|
|
642
|
+
* State Management:
|
|
643
|
+
* - Uses TanStack Store for reactive state management
|
|
644
|
+
* - Use `initialState` to provide initial state values when creating the queuer
|
|
645
|
+
* - Use `onExecute` callback to react to item execution and implement custom logic
|
|
646
|
+
* - Use `onItemsChange` callback to react to items being added or removed from the queue
|
|
647
|
+
* - Use `onExpire` callback to react to items expiring and implement custom logic
|
|
648
|
+
* - Use `onReject` callback to react to items being rejected when the queue is full
|
|
649
|
+
* - The state includes execution count, expiration count, rejection count, and isRunning status
|
|
650
|
+
* - State can be accessed via the underlying Queuer instance's `store.state` property
|
|
651
|
+
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
|
|
652
|
+
*
|
|
531
653
|
* Example usage:
|
|
532
654
|
* ```ts
|
|
533
655
|
* // Basic sequential processing
|
|
@@ -548,8 +670,8 @@ export class Queuer<TValue> {
|
|
|
548
670
|
*/
|
|
549
671
|
export function queue<TValue>(
|
|
550
672
|
fn: (item: TValue) => void,
|
|
551
|
-
|
|
673
|
+
initialOptions: QueuerOptions<TValue>,
|
|
552
674
|
) {
|
|
553
|
-
const queuer = new Queuer<TValue>(fn,
|
|
554
|
-
return queuer.addItem
|
|
675
|
+
const queuer = new Queuer<TValue>(fn, initialOptions)
|
|
676
|
+
return queuer.addItem
|
|
555
677
|
}
|