@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.
Files changed (91) hide show
  1. package/dist/cjs/async-batcher.cjs +163 -0
  2. package/dist/cjs/async-batcher.cjs.map +1 -0
  3. package/dist/cjs/async-batcher.d.cts +273 -0
  4. package/dist/cjs/async-debouncer.cjs +149 -162
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +76 -57
  7. package/dist/cjs/async-queuer.cjs +282 -343
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +121 -100
  10. package/dist/cjs/async-rate-limiter.cjs +128 -185
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +72 -61
  13. package/dist/cjs/async-throttler.cjs +168 -178
  14. package/dist/cjs/async-throttler.cjs.map +1 -1
  15. package/dist/cjs/async-throttler.d.cts +97 -69
  16. package/dist/cjs/batcher.cjs +110 -119
  17. package/dist/cjs/batcher.cjs.map +1 -1
  18. package/dist/cjs/batcher.d.cts +76 -51
  19. package/dist/cjs/debouncer.cjs +97 -85
  20. package/dist/cjs/debouncer.cjs.map +1 -1
  21. package/dist/cjs/debouncer.d.cts +54 -26
  22. package/dist/cjs/index.cjs +3 -6
  23. package/dist/cjs/index.cjs.map +1 -1
  24. package/dist/cjs/index.d.cts +1 -1
  25. package/dist/cjs/queuer.cjs +247 -294
  26. package/dist/cjs/queuer.cjs.map +1 -1
  27. package/dist/cjs/queuer.d.cts +102 -81
  28. package/dist/cjs/rate-limiter.cjs +97 -130
  29. package/dist/cjs/rate-limiter.cjs.map +1 -1
  30. package/dist/cjs/rate-limiter.d.cts +50 -37
  31. package/dist/cjs/throttler.cjs +107 -123
  32. package/dist/cjs/throttler.cjs.map +1 -1
  33. package/dist/cjs/throttler.d.cts +59 -35
  34. package/dist/cjs/utils.cjs +0 -13
  35. package/dist/cjs/utils.cjs.map +1 -1
  36. package/dist/cjs/utils.d.cts +0 -1
  37. package/dist/esm/async-batcher.d.ts +273 -0
  38. package/dist/esm/async-batcher.js +163 -0
  39. package/dist/esm/async-batcher.js.map +1 -0
  40. package/dist/esm/async-debouncer.d.ts +76 -57
  41. package/dist/esm/async-debouncer.js +149 -162
  42. package/dist/esm/async-debouncer.js.map +1 -1
  43. package/dist/esm/async-queuer.d.ts +121 -100
  44. package/dist/esm/async-queuer.js +282 -343
  45. package/dist/esm/async-queuer.js.map +1 -1
  46. package/dist/esm/async-rate-limiter.d.ts +72 -61
  47. package/dist/esm/async-rate-limiter.js +128 -185
  48. package/dist/esm/async-rate-limiter.js.map +1 -1
  49. package/dist/esm/async-throttler.d.ts +97 -69
  50. package/dist/esm/async-throttler.js +168 -178
  51. package/dist/esm/async-throttler.js.map +1 -1
  52. package/dist/esm/batcher.d.ts +76 -51
  53. package/dist/esm/batcher.js +110 -119
  54. package/dist/esm/batcher.js.map +1 -1
  55. package/dist/esm/debouncer.d.ts +54 -26
  56. package/dist/esm/debouncer.js +97 -85
  57. package/dist/esm/debouncer.js.map +1 -1
  58. package/dist/esm/index.d.ts +1 -1
  59. package/dist/esm/index.js +4 -7
  60. package/dist/esm/queuer.d.ts +102 -81
  61. package/dist/esm/queuer.js +247 -294
  62. package/dist/esm/queuer.js.map +1 -1
  63. package/dist/esm/rate-limiter.d.ts +50 -37
  64. package/dist/esm/rate-limiter.js +97 -130
  65. package/dist/esm/rate-limiter.js.map +1 -1
  66. package/dist/esm/throttler.d.ts +59 -35
  67. package/dist/esm/throttler.js +107 -123
  68. package/dist/esm/throttler.js.map +1 -1
  69. package/dist/esm/utils.d.ts +0 -1
  70. package/dist/esm/utils.js +0 -13
  71. package/dist/esm/utils.js.map +1 -1
  72. package/package.json +14 -11
  73. package/src/async-batcher.ts +475 -0
  74. package/src/async-debouncer.ts +201 -121
  75. package/src/async-queuer.ts +337 -216
  76. package/src/async-rate-limiter.ts +176 -136
  77. package/src/async-throttler.ts +233 -139
  78. package/src/batcher.ts +158 -92
  79. package/src/debouncer.ts +135 -52
  80. package/src/index.ts +1 -1
  81. package/src/queuer.ts +349 -226
  82. package/src/rate-limiter.ts +125 -80
  83. package/src/throttler.ts +152 -78
  84. package/src/utils.ts +0 -15
  85. package/dist/cjs/compare.cjs +0 -72
  86. package/dist/cjs/compare.cjs.map +0 -1
  87. package/dist/cjs/compare.d.cts +0 -12
  88. package/dist/esm/compare.d.ts +0 -12
  89. package/dist/esm/compare.js +0 -72
  90. package/dist/esm/compare.js.map +0 -1
  91. 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: Required<QueuerOptions<any>> = {
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 running)
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
- private _options: Required<QueuerOptions<TValue>>
162
- private _items: Array<TValue> = []
163
- private _itemTimestamps: Array<number> = []
164
- private _executionCount = 0
165
- private _rejectionCount = 0
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._options = { ...defaultOptions, ...initialOptions }
176
- this._running = this._options.started
177
-
178
- for (let i = 0; i < this._options.initialItems.length; i++) {
179
- const item = this._options.initialItems[i]!
180
- const isLast = i === this._options.initialItems.length - 1
181
- this.addItem(item, this._options.addItemsTo, isLast)
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._options = { ...this._options, ...newOptions }
281
+ setOptions = (newOptions: Partial<QueuerOptions<TValue>>): void => {
282
+ this.options = { ...this.options, ...newOptions }
190
283
  }
191
284
 
192
- /**
193
- * Returns the current queuer options, including defaults and any overrides.
194
- */
195
- getOptions(): Required<QueuerOptions<TValue>> {
196
- return this._options
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._options.wait, 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
- private tick() {
211
- if (!this._running) {
212
- this._pendingTick = false
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.checkExpiredItems()
332
+ this.#checkExpiredItems()
218
333
 
219
- while (!this.getIsEmpty()) {
220
- const nextItem = this.execute(this._options.getItemsFrom)
334
+ while (this.store.state.items.length > 0) {
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.getWait()
340
+ const wait = this.#getWait()
227
341
  if (wait > 0) {
228
342
  // Use setTimeout to wait before processing next item
229
- setTimeout(() => this.tick(), wait)
343
+ this.#timeoutId = setTimeout(() => this.#tick(), wait)
230
344
  return
231
345
  }
232
346
 
233
- this.tick()
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._options.onIsRunningChange(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._options.addItemsTo,
346
- runOnUpdate: boolean = true,
347
- ): boolean {
348
- if (this.getIsFull()) {
349
- this._rejectionCount++
350
- this._options.onReject(item, this)
366
+ position: QueuePosition = this.options.addItemsTo ?? 'back',
367
+ runOnItemsChange: boolean = true,
368
+ ): boolean => {
369
+ if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
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
- if (this._options.getPriority !== defaultOptions.getPriority) {
355
- // If custom priority function is provided, insert based on priority
356
- const priority = this._options.getPriority(item)
357
- const insertIndex = this._items.findIndex(
358
- (existing) => this._options.getPriority(existing) < priority,
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
- this._items.push(item)
363
- this._itemTimestamps.push(Date.now())
397
+ items.push(item)
398
+ itemTimestamps.push(Date.now())
364
399
  } else {
365
- this._items.splice(insertIndex, 0, item)
366
- this._itemTimestamps.splice(insertIndex, 0, Date.now())
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
- this._items.unshift(item)
372
- this._itemTimestamps.unshift(Date.now())
405
+ // Default FIFO/LIFO behavior
406
+ items.unshift(item)
407
+ itemTimestamps.unshift(Date.now())
373
408
  } else {
374
- this._items.push(item)
375
- this._itemTimestamps.push(Date.now())
409
+ // LIFO
410
+ items.push(item)
411
+ itemTimestamps.push(Date.now())
376
412
  }
377
413
  }
378
414
 
379
- if (this._running && !this._pendingTick) {
380
- this._pendingTick = true
381
- this.tick()
415
+ this.#setState({
416
+ items,
417
+ itemTimestamps,
418
+ })
419
+
420
+ if (runOnItemsChange) {
421
+ this.options.onItemsChange?.(this)
382
422
  }
383
- if (runOnUpdate) {
384
- this._options.onItemsChange(this)
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._options.getItemsFrom,
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 = this._items.shift()
408
- this._itemTimestamps.shift()
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 = this._items.pop()
411
- this._itemTimestamps.pop()
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._options.onItemsChange(this)
469
+ this.options.onItemsChange?.(this)
416
470
  }
417
471
 
418
472
  return item
@@ -428,95 +482,153 @@ 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._executionCount++
436
- this._options.onExecute(item, this)
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
- * Returns the next item in the queue without removing it.
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
- peekNextItem(
451
- position: QueuePosition = this._options.getItemsFrom,
452
- ): TValue | undefined {
453
- if (position === 'front') {
454
- return this._items[0]
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]
509
+ this.#tick()
457
510
  }
458
511
 
459
512
  /**
460
- * Returns true if the queue is empty (no pending items).
513
+ * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
514
+ * Internal use only.
461
515
  */
462
- getIsEmpty(): boolean {
463
- return this._items.length === 0
464
- }
516
+ #checkExpiredItems = (): void => {
517
+ if (
518
+ (this.options.expirationDuration ?? Infinity) === Infinity &&
519
+ this.options.getIsExpired === defaultOptions.getIsExpired
520
+ ) {
521
+ return
522
+ }
465
523
 
466
- /**
467
- * Returns true if the queue is full (reached maxSize).
468
- */
469
- getIsFull(): boolean {
470
- return this._items.length >= this._options.maxSize
524
+ const now = Date.now()
525
+ const expiredIndices: Array<number> = []
526
+
527
+ // Find indices of expired items
528
+ for (let i = 0; i < this.store.state.items.length; i++) {
529
+ const timestamp = this.store.state.itemTimestamps[i]
530
+ if (timestamp === undefined) continue
531
+
532
+ const item = this.store.state.items[i]
533
+ if (item === undefined) continue
534
+
535
+ const isExpired =
536
+ this.options.getIsExpired !== defaultOptions.getIsExpired
537
+ ? this.options.getIsExpired!(item, timestamp)
538
+ : now - timestamp > (this.options.expirationDuration ?? Infinity)
539
+
540
+ if (isExpired) {
541
+ expiredIndices.push(i)
542
+ }
543
+ }
544
+
545
+ // Remove expired items from back to front to maintain indices
546
+ for (let i = expiredIndices.length - 1; i >= 0; i--) {
547
+ const index = expiredIndices[i]
548
+ if (index === undefined) continue
549
+
550
+ const expiredItem = this.store.state.items[index]
551
+ if (expiredItem === undefined) continue
552
+
553
+ const newItems = [...this.store.state.items]
554
+ const newTimestamps = [...this.store.state.itemTimestamps]
555
+ newItems.splice(index, 1)
556
+ newTimestamps.splice(index, 1)
557
+ this.#setState({
558
+ items: newItems,
559
+ itemTimestamps: newTimestamps,
560
+ expirationCount: this.store.state.expirationCount + 1,
561
+ })
562
+ this.options.onExpire?.(expiredItem, this)
563
+ }
564
+
565
+ if (expiredIndices.length > 0) {
566
+ this.options.onItemsChange?.(this)
567
+ }
471
568
  }
472
569
 
473
570
  /**
474
- * Returns the number of pending items in the queue.
571
+ * Returns the next item in the queue without removing it.
572
+ *
573
+ * Example usage:
574
+ * ```ts
575
+ * queuer.peekNextItem(); // front
576
+ * queuer.peekNextItem('back'); // back
577
+ * ```
475
578
  */
476
- getSize(): number {
477
- return this._items.length
579
+ peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {
580
+ if (position === 'front') {
581
+ return this.store.state.items[0]
582
+ }
583
+ return this.store.state.items[this.store.state.items.length - 1]
478
584
  }
479
585
 
480
586
  /**
481
587
  * Returns a copy of all items in the queue.
482
588
  */
483
- peekAllItems(): Array<TValue> {
484
- return [...this._items]
589
+ peekAllItems = (): Array<TValue> => {
590
+ return [...this.store.state.items]
485
591
  }
486
592
 
487
593
  /**
488
- * Returns the number of items that have been processed and removed from the queue.
594
+ * Starts processing items in the queue. If already isRunning, does nothing.
489
595
  */
490
- getExecutionCount(): number {
491
- return this._executionCount
596
+ start = () => {
597
+ this.#setState({ isRunning: true })
598
+ if (!this.store.state.pendingTick && this.store.state.items.length > 0) {
599
+ this.#tick()
600
+ }
492
601
  }
493
602
 
494
603
  /**
495
- * Returns the number of items that have been rejected from being added to the queue.
604
+ * Stops processing items in the queue. Does not clear the queue.
496
605
  */
497
- getRejectionCount(): number {
498
- return this._rejectionCount
606
+ stop = () => {
607
+ this.#clearTimeout()
608
+ this.#setState({ isRunning: false, pendingTick: false })
499
609
  }
500
610
 
501
- /**
502
- * Returns the number of items that have expired and been removed from the queue.
503
- */
504
- getExpirationCount(): number {
505
- return this._expirationCount
611
+ #clearTimeout = (): void => {
612
+ if (this.#timeoutId) {
613
+ clearTimeout(this.#timeoutId)
614
+ this.#timeoutId = null
615
+ }
506
616
  }
507
617
 
508
618
  /**
509
- * Returns true if the queuer is currently running (processing items).
619
+ * Removes all pending items from the queue. Does not affect items being processed.
510
620
  */
511
- getIsRunning() {
512
- return this._running
621
+ clear = (): void => {
622
+ this.#setState({ items: [], itemTimestamps: [] })
623
+ this.options.onItemsChange?.(this)
513
624
  }
514
625
 
515
626
  /**
516
- * Returns true if the queuer is running but has no items to process.
627
+ * Resets the queuer state to its default values
517
628
  */
518
- getIsIdle() {
519
- return this._running && this.getIsEmpty()
629
+ reset = (): void => {
630
+ this.#setState(getDefaultQueuerState<TValue>())
631
+ this.options.onItemsChange?.(this)
520
632
  }
521
633
  }
522
634
 
@@ -525,9 +637,20 @@ export class Queuer<TValue> {
525
637
  * Items are processed sequentially in FIFO order by default.
526
638
  *
527
639
  * This is a simplified wrapper around the Queuer class that only exposes the
528
- * `addItem` method. The queue is always running and will process items as they are added.
640
+ * `addItem` method. The queue is always isRunning and will process items as they are added.
529
641
  * For more control over queue processing, use the Queuer class directly.
530
642
  *
643
+ * State Management:
644
+ * - Uses TanStack Store for reactive state management
645
+ * - Use `initialState` to provide initial state values when creating the queuer
646
+ * - Use `onExecute` callback to react to item execution and implement custom logic
647
+ * - Use `onItemsChange` callback to react to items being added or removed from the queue
648
+ * - Use `onExpire` callback to react to items expiring and implement custom logic
649
+ * - Use `onReject` callback to react to items being rejected when the queue is full
650
+ * - The state includes execution count, expiration count, rejection count, and isRunning status
651
+ * - State can be accessed via the underlying Queuer instance's `store.state` property
652
+ * - When using framework adapters (React/Solid), state is accessed from the hook's state property
653
+ *
531
654
  * Example usage:
532
655
  * ```ts
533
656
  * // Basic sequential processing
@@ -548,8 +671,8 @@ export class Queuer<TValue> {
548
671
  */
549
672
  export function queue<TValue>(
550
673
  fn: (item: TValue) => void,
551
- options: QueuerOptions<TValue>,
674
+ initialOptions: QueuerOptions<TValue>,
552
675
  ) {
553
- const queuer = new Queuer<TValue>(fn, options)
554
- return queuer.addItem.bind(queuer)
676
+ const queuer = new Queuer<TValue>(fn, initialOptions)
677
+ return queuer.addItem
555
678
  }