@tanstack/pacer 0.1.0 → 0.3.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.
Files changed (74) hide show
  1. package/dist/cjs/async-debouncer.cjs +112 -63
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +66 -25
  4. package/dist/cjs/async-queuer.cjs +198 -124
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +91 -49
  7. package/dist/cjs/async-rate-limiter.cjs +83 -55
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +55 -28
  10. package/dist/cjs/async-throttler.cjs +121 -70
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +75 -25
  13. package/dist/cjs/debouncer.cjs +45 -23
  14. package/dist/cjs/debouncer.cjs.map +1 -1
  15. package/dist/cjs/debouncer.d.cts +27 -12
  16. package/dist/cjs/index.cjs +2 -0
  17. package/dist/cjs/index.cjs.map +1 -1
  18. package/dist/cjs/index.d.cts +2 -0
  19. package/dist/cjs/queuer.cjs +161 -101
  20. package/dist/cjs/queuer.cjs.map +1 -1
  21. package/dist/cjs/queuer.d.cts +80 -38
  22. package/dist/cjs/rate-limiter.cjs +52 -44
  23. package/dist/cjs/rate-limiter.cjs.map +1 -1
  24. package/dist/cjs/rate-limiter.d.cts +38 -46
  25. package/dist/cjs/throttler.cjs +57 -44
  26. package/dist/cjs/throttler.cjs.map +1 -1
  27. package/dist/cjs/throttler.d.cts +35 -23
  28. package/dist/cjs/types.d.cts +8 -0
  29. package/dist/cjs/utils.cjs +13 -0
  30. package/dist/cjs/utils.cjs.map +1 -0
  31. package/dist/cjs/utils.d.cts +1 -0
  32. package/dist/esm/async-debouncer.d.ts +66 -25
  33. package/dist/esm/async-debouncer.js +112 -63
  34. package/dist/esm/async-debouncer.js.map +1 -1
  35. package/dist/esm/async-queuer.d.ts +91 -49
  36. package/dist/esm/async-queuer.js +198 -124
  37. package/dist/esm/async-queuer.js.map +1 -1
  38. package/dist/esm/async-rate-limiter.d.ts +55 -28
  39. package/dist/esm/async-rate-limiter.js +83 -55
  40. package/dist/esm/async-rate-limiter.js.map +1 -1
  41. package/dist/esm/async-throttler.d.ts +75 -25
  42. package/dist/esm/async-throttler.js +121 -70
  43. package/dist/esm/async-throttler.js.map +1 -1
  44. package/dist/esm/debouncer.d.ts +27 -12
  45. package/dist/esm/debouncer.js +45 -23
  46. package/dist/esm/debouncer.js.map +1 -1
  47. package/dist/esm/index.d.ts +2 -0
  48. package/dist/esm/index.js +2 -0
  49. package/dist/esm/index.js.map +1 -1
  50. package/dist/esm/queuer.d.ts +80 -38
  51. package/dist/esm/queuer.js +161 -101
  52. package/dist/esm/queuer.js.map +1 -1
  53. package/dist/esm/rate-limiter.d.ts +38 -46
  54. package/dist/esm/rate-limiter.js +52 -44
  55. package/dist/esm/rate-limiter.js.map +1 -1
  56. package/dist/esm/throttler.d.ts +35 -23
  57. package/dist/esm/throttler.js +57 -44
  58. package/dist/esm/throttler.js.map +1 -1
  59. package/dist/esm/types.d.ts +8 -0
  60. package/dist/esm/utils.d.ts +1 -0
  61. package/dist/esm/utils.js +13 -0
  62. package/dist/esm/utils.js.map +1 -0
  63. package/package.json +8 -1
  64. package/src/async-debouncer.ts +157 -88
  65. package/src/async-queuer.ts +266 -148
  66. package/src/async-rate-limiter.ts +123 -83
  67. package/src/async-throttler.ts +173 -89
  68. package/src/debouncer.ts +71 -42
  69. package/src/index.ts +2 -0
  70. package/src/queuer.ts +219 -114
  71. package/src/rate-limiter.ts +74 -88
  72. package/src/throttler.ts +83 -65
  73. package/src/types.ts +9 -0
  74. package/src/utils.ts +13 -0
package/src/queuer.ts CHANGED
@@ -7,6 +7,16 @@ export interface QueuerOptions<TValue> {
7
7
  * @default 'back'
8
8
  */
9
9
  addItemsTo?: QueuePosition
10
+ /**
11
+ * Maximum time in milliseconds that an item can stay in the queue
12
+ * If not provided, items will never expire
13
+ */
14
+ expirationDuration?: number
15
+ /**
16
+ * Function to determine if an item has expired
17
+ * If provided, this overrides the expirationDuration behavior
18
+ */
19
+ getIsExpired?: (item: TValue, addedAt: number) => boolean
10
20
  /**
11
21
  * Default position to get items from during processing
12
22
  * @default 'front'
@@ -25,14 +35,26 @@ export interface QueuerOptions<TValue> {
25
35
  * Maximum number of items allowed in the queuer
26
36
  */
27
37
  maxSize?: number
38
+ /**
39
+ * Callback fired whenever an item expires in the queuer
40
+ */
41
+ onExpire?: (item: TValue, queuer: Queuer<TValue>) => void
28
42
  /**
29
43
  * Callback fired whenever an item is removed from the queuer
30
44
  */
31
45
  onGetNextItem?: (item: TValue, queuer: Queuer<TValue>) => void
46
+ /**
47
+ * Callback fired whenever the queuer's running state changes
48
+ */
49
+ onIsRunningChange?: (queuer: Queuer<TValue>) => void
32
50
  /**
33
51
  * Callback fired whenever an item is added or removed from the queuer
34
52
  */
35
- onUpdate?: (queuer: Queuer<TValue>) => void
53
+ onItemsChange?: (queuer: Queuer<TValue>) => void
54
+ /**
55
+ * Callback fired whenever an item is rejected from being added to the queuer
56
+ */
57
+ onReject?: (item: TValue, queuer: Queuer<TValue>) => void
36
58
  /**
37
59
  * Whether the queuer should start processing tasks immediately
38
60
  */
@@ -46,11 +68,16 @@ export interface QueuerOptions<TValue> {
46
68
  const defaultOptions: Required<QueuerOptions<any>> = {
47
69
  addItemsTo: 'back',
48
70
  getItemsFrom: 'front',
49
- getPriority: () => 0,
71
+ getPriority: (item) => item?.priority ?? 0,
72
+ getIsExpired: () => false,
73
+ expirationDuration: Infinity,
50
74
  initialItems: [],
51
75
  maxSize: Infinity,
52
76
  onGetNextItem: () => {},
53
- onUpdate: () => {},
77
+ onIsRunningChange: () => {},
78
+ onItemsChange: () => {},
79
+ onReject: () => {},
80
+ onExpire: () => {},
54
81
  started: false,
55
82
  wait: 0,
56
83
  }
@@ -87,7 +114,12 @@ export type QueuePosition = 'front' | 'back'
87
114
  * - start(): begins processing items in the queuer
88
115
  * - stop(): pauses processing
89
116
  * - wait: configurable delay between processing items
90
- * - onUpdate/onGetNextItem: callbacks for monitoring queuer state
117
+ * - onItemsChange/onGetNextItem: callbacks for monitoring queuer state
118
+ *
119
+ * Supports item expiration to clear stale items from the queuer
120
+ * - expirationDuration: maximum time in milliseconds that an item can stay in the queue
121
+ * - getIsExpired: function to override default expiration behavior
122
+ * - onExpire: callback for when an item expires
91
123
  *
92
124
  * @example
93
125
  * ```ts
@@ -102,7 +134,7 @@ export type QueuePosition = 'front' | 'back'
102
134
  * getPriority: (n) => n, // Higher numbers have priority
103
135
  * started: true, // Begin processing immediately
104
136
  * wait: 1000, // Wait 1s between items
105
- * onGetNextItem: (item) => console.log(item)
137
+ * onGetNextItem: (item, queuer) => console.log(item)
106
138
  * });
107
139
  * priorityQueue.addItem(1); // [1]
108
140
  * priorityQueue.addItem(3); // [3, 1] - 3 processed first
@@ -110,59 +142,161 @@ export type QueuePosition = 'front' | 'back'
110
142
  * ```
111
143
  */
112
144
  export class Queuer<TValue> {
113
- protected options: Required<QueuerOptions<TValue>>
114
- private items: Array<TValue> = []
115
- private executionCount = 0
116
- private onUpdates: Array<(item: TValue) => void> = []
117
- private running: boolean
118
- private pendingTick = false
145
+ private _options: Required<QueuerOptions<TValue>>
146
+ private _items: Array<TValue> = []
147
+ private _itemTimestamps: Array<number> = []
148
+ private _executionCount = 0
149
+ private _rejectionCount = 0
150
+ private _expirationCount = 0
151
+ private _onItemsChanges: Array<(item: TValue) => void> = []
152
+ private _running: boolean
153
+ private _pendingTick = false
119
154
 
120
155
  constructor(initialOptions: QueuerOptions<TValue> = defaultOptions) {
121
- this.options = { ...defaultOptions, ...initialOptions }
122
- this.running = this.options.started
156
+ this._options = { ...defaultOptions, ...initialOptions }
157
+ this._running = this._options.started
123
158
 
124
- for (let i = 0; i < this.options.initialItems.length; i++) {
125
- const item = this.options.initialItems[i]!
126
- const isLast = i === this.options.initialItems.length - 1
127
- this.addItem(item, this.options.addItemsTo, isLast)
159
+ for (let i = 0; i < this._options.initialItems.length; i++) {
160
+ const item = this._options.initialItems[i]!
161
+ const isLast = i === this._options.initialItems.length - 1
162
+ this.addItem(item, this._options.addItemsTo, isLast)
128
163
  }
129
164
  }
130
165
 
166
+ /**
167
+ * Updates the queuer options
168
+ * Returns the new options state
169
+ */
170
+ setOptions(newOptions: Partial<QueuerOptions<TValue>>): void {
171
+ this._options = { ...this._options, ...newOptions }
172
+ }
173
+
174
+ /**
175
+ * Returns the current queuer options
176
+ */
177
+ getOptions(): Required<QueuerOptions<TValue>> {
178
+ return this._options
179
+ }
180
+
131
181
  /**
132
182
  * Processes items in the queuer
133
183
  */
134
- protected tick() {
135
- if (!this.running) {
136
- this.pendingTick = false
184
+ private tick() {
185
+ if (!this._running) {
186
+ this._pendingTick = false
137
187
  return
138
188
  }
139
- while (!this.isEmpty()) {
140
- const nextItem = this.getNextItem(this.options.getItemsFrom)
189
+
190
+ // Check for expired items
191
+ this.checkExpiredItems()
192
+
193
+ while (!this.getIsEmpty()) {
194
+ const nextItem = this.getNextItem(this._options.getItemsFrom)
141
195
  if (nextItem === undefined) {
142
196
  break
143
197
  }
144
- this.onUpdates.forEach((cb) => cb(nextItem))
198
+ this._onItemsChanges.forEach((cb) => cb(nextItem))
145
199
 
146
- if (this.options.wait > 0) {
200
+ if (this._options.wait > 0) {
147
201
  // Use setTimeout to wait before processing next item
148
- setTimeout(() => this.tick(), this.options.wait)
202
+ setTimeout(() => this.tick(), this._options.wait)
149
203
  return
150
204
  }
151
205
 
152
206
  this.tick()
153
207
  }
154
- this.pendingTick = false
208
+ this._pendingTick = false
155
209
  }
156
210
 
157
211
  /**
158
- * Updates the queuer options
159
- * Returns the new options state
212
+ * Checks for and removes expired items from the queuer
213
+ */
214
+ private checkExpiredItems() {
215
+ if (
216
+ this._options.expirationDuration === Infinity &&
217
+ this._options.getIsExpired === defaultOptions.getIsExpired
218
+ )
219
+ return
220
+
221
+ const now = Date.now()
222
+ const expiredIndices: Array<number> = []
223
+
224
+ // Find indices of expired items
225
+ for (let i = 0; i < this._items.length; i++) {
226
+ const timestamp = this._itemTimestamps[i]
227
+ if (timestamp === undefined) continue
228
+
229
+ const item = this._items[i]
230
+ if (item === undefined) continue
231
+
232
+ const isExpired =
233
+ this._options.getIsExpired !== defaultOptions.getIsExpired
234
+ ? this._options.getIsExpired(item, timestamp)
235
+ : now - timestamp > this._options.expirationDuration
236
+
237
+ if (isExpired) {
238
+ expiredIndices.push(i)
239
+ }
240
+ }
241
+
242
+ // Remove expired items from back to front to maintain indices
243
+ for (let i = expiredIndices.length - 1; i >= 0; i--) {
244
+ const index = expiredIndices[i]
245
+ if (index === undefined) continue
246
+
247
+ const expiredItem = this._items[index]
248
+ if (expiredItem === undefined) continue
249
+
250
+ this._items.splice(index, 1)
251
+ this._itemTimestamps.splice(index, 1)
252
+ this._expirationCount++
253
+ this._options.onExpire(expiredItem, this)
254
+ }
255
+
256
+ if (expiredIndices.length > 0) {
257
+ this._options.onItemsChange(this)
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Stops the queuer from processing items
263
+ */
264
+ stop() {
265
+ this._running = false
266
+ this._pendingTick = false
267
+ this._options.onIsRunningChange(this)
268
+ }
269
+
270
+ /**
271
+ * Starts the queuer and processes items
272
+ */
273
+ start() {
274
+ this._running = true
275
+ if (!this._pendingTick && !this.getIsEmpty()) {
276
+ this._pendingTick = true
277
+ this.tick()
278
+ }
279
+ this._options.onIsRunningChange(this)
280
+ }
281
+
282
+ /**
283
+ * Removes all items from the queuer
284
+ */
285
+ clear(): void {
286
+ this._items = []
287
+ this._options.onItemsChange(this)
288
+ }
289
+
290
+ /**
291
+ * Resets the queuer to its initial state
160
292
  */
161
- setOptions(
162
- newOptions: Partial<QueuerOptions<TValue>>,
163
- ): QueuerOptions<TValue> {
164
- this.options = { ...this.options, ...newOptions }
165
- return this.options
293
+ reset(withInitialItems?: boolean): void {
294
+ this.clear()
295
+ this._executionCount = 0
296
+ if (withInitialItems) {
297
+ this._items = [...this._options.initialItems]
298
+ }
299
+ this._running = this._options.started
166
300
  }
167
301
 
168
302
  /**
@@ -171,40 +305,46 @@ export class Queuer<TValue> {
171
305
  */
172
306
  addItem(
173
307
  item: TValue,
174
- position: QueuePosition = this.options.addItemsTo,
308
+ position: QueuePosition = this._options.addItemsTo,
175
309
  runOnUpdate: boolean = true,
176
310
  ): boolean {
177
- if (this.isFull()) {
311
+ if (this.getIsFull()) {
312
+ this._rejectionCount++
313
+ this._options.onReject(item, this)
178
314
  return false
179
315
  }
180
316
 
181
- if (this.options.getPriority !== defaultOptions.getPriority) {
317
+ if (this._options.getPriority !== defaultOptions.getPriority) {
182
318
  // If custom priority function is provided, insert based on priority
183
- const priority = this.options.getPriority(item)
184
- const insertIndex = this.items.findIndex(
185
- (existing) => this.options.getPriority(existing) > priority,
319
+ const priority = this._options.getPriority(item)
320
+ const insertIndex = this._items.findIndex(
321
+ (existing) => this._options.getPriority(existing) > priority,
186
322
  )
187
323
 
188
324
  if (insertIndex === -1) {
189
- this.items.push(item)
325
+ this._items.push(item)
326
+ this._itemTimestamps.push(Date.now())
190
327
  } else {
191
- this.items.splice(insertIndex, 0, item)
328
+ this._items.splice(insertIndex, 0, item)
329
+ this._itemTimestamps.splice(insertIndex, 0, Date.now())
192
330
  }
193
331
  } else {
194
332
  // Default FIFO/LIFO behavior
195
333
  if (position === 'front') {
196
- this.items.unshift(item)
334
+ this._items.unshift(item)
335
+ this._itemTimestamps.unshift(Date.now())
197
336
  } else {
198
- this.items.push(item)
337
+ this._items.push(item)
338
+ this._itemTimestamps.push(Date.now())
199
339
  }
200
340
  }
201
341
 
202
- if (this.running && !this.pendingTick) {
203
- this.pendingTick = true
342
+ if (this._running && !this._pendingTick) {
343
+ this._pendingTick = true
204
344
  this.tick()
205
345
  }
206
346
  if (runOnUpdate) {
207
- this.options.onUpdate(this)
347
+ this._options.onItemsChange(this)
208
348
  }
209
349
  return true
210
350
  }
@@ -221,20 +361,22 @@ export class Queuer<TValue> {
221
361
  * ```
222
362
  */
223
363
  getNextItem(
224
- position: QueuePosition = this.options.getItemsFrom,
364
+ position: QueuePosition = this._options.getItemsFrom,
225
365
  ): TValue | undefined {
226
366
  let item: TValue | undefined
227
367
 
228
368
  if (position === 'front') {
229
- item = this.items.shift()
369
+ item = this._items.shift()
370
+ this._itemTimestamps.shift()
230
371
  } else {
231
- item = this.items.pop()
372
+ item = this._items.pop()
373
+ this._itemTimestamps.pop()
232
374
  }
233
375
 
234
376
  if (item !== undefined) {
235
- this.executionCount++
236
- this.options.onUpdate(this)
237
- this.options.onGetNextItem(item, this)
377
+ this._executionCount++
378
+ this._options.onItemsChange(this)
379
+ this._options.onGetNextItem(item, this)
238
380
  }
239
381
  return item
240
382
  }
@@ -245,118 +387,81 @@ export class Queuer<TValue> {
245
387
  * @example
246
388
  * ```ts
247
389
  * // Look at next item to getNextItem
248
- * queuer.peek()
390
+ * queuer.getPeek()
249
391
  * // Look at last item (like stack top)
250
- * queuer.peek('back')
392
+ * queuer.getPeek('back')
251
393
  * ```
252
394
  */
253
- peek(
254
- position: QueuePosition = this.options.getItemsFrom,
395
+ getPeek(
396
+ position: QueuePosition = this._options.getItemsFrom,
255
397
  ): TValue | undefined {
256
398
  if (position === 'front') {
257
- return this.items[0]
399
+ return this._items[0]
258
400
  }
259
- return this.items[this.items.length - 1]
401
+ return this._items[this._items.length - 1]
260
402
  }
261
403
 
262
404
  /**
263
405
  * Returns true if the queuer is empty
264
406
  */
265
- isEmpty(): boolean {
266
- return this.items.length === 0
407
+ getIsEmpty(): boolean {
408
+ return this._items.length === 0
267
409
  }
268
410
 
269
411
  /**
270
412
  * Returns true if the queuer is full
271
413
  */
272
- isFull(): boolean {
273
- return this.items.length >= this.options.maxSize
414
+ getIsFull(): boolean {
415
+ return this._items.length >= this._options.maxSize
274
416
  }
275
417
 
276
418
  /**
277
419
  * Returns the current size of the queuer
278
420
  */
279
- size(): number {
280
- return this.items.length
281
- }
282
-
283
- /**
284
- * Removes all items from the queuer
285
- */
286
- clear(): void {
287
- this.items = []
288
- this.options.onUpdate(this)
289
- }
290
-
291
- /**
292
- * Resets the queuer to its initial state
293
- */
294
- reset(withInitialItems?: boolean): void {
295
- this.clear()
296
- this.executionCount = 0
297
- if (withInitialItems) {
298
- this.items = [...this.options.initialItems]
299
- }
300
- this.running = this.options.started
421
+ getSize(): number {
422
+ return this._items.length
301
423
  }
302
424
 
303
425
  /**
304
426
  * Returns a copy of all items in the queuer
305
427
  */
306
428
  getAllItems(): Array<TValue> {
307
- return [...this.items]
429
+ return [...this._items]
308
430
  }
309
431
 
310
432
  /**
311
433
  * Returns the number of items that have been removed from the queuer
312
434
  */
313
435
  getExecutionCount(): number {
314
- return this.executionCount
436
+ return this._executionCount
315
437
  }
316
438
 
317
439
  /**
318
- * Adds a callback to be called when an item is processed
440
+ * Returns the number of items that have been rejected from the queuer
319
441
  */
320
- onUpdate(cb: (item: TValue) => void) {
321
- this.onUpdates.push(cb)
322
- return () => {
323
- this.onUpdates = this.onUpdates.filter((d) => d !== cb)
324
- }
442
+ getRejectionCount(): number {
443
+ return this._rejectionCount
325
444
  }
326
445
 
327
446
  /**
328
- * Stops the queuer from processing items
447
+ * Returns the number of items that have expired from the queuer
329
448
  */
330
- stop() {
331
- this.running = false
332
- this.pendingTick = false
333
- this.options.onUpdate(this)
334
- }
335
-
336
- /**
337
- * Starts the queuer and processes items
338
- */
339
- start() {
340
- this.running = true
341
- if (!this.pendingTick && !this.isEmpty()) {
342
- this.pendingTick = true
343
- this.tick()
344
- }
345
- this.options.onUpdate(this)
449
+ getExpirationCount(): number {
450
+ return this._expirationCount
346
451
  }
347
452
 
348
453
  /**
349
454
  * Returns true if the queuer is running
350
455
  */
351
- isRunning() {
352
- return this.running
456
+ getIsRunning() {
457
+ return this._running
353
458
  }
354
459
 
355
460
  /**
356
461
  * Returns true if the queuer is running but has no items to process
357
462
  */
358
- isIdle() {
359
- return this.running && this.isEmpty()
463
+ getIsIdle() {
464
+ return this._running && this.getIsEmpty()
360
465
  }
361
466
  }
362
467
 
@@ -374,7 +479,7 @@ export class Queuer<TValue> {
374
479
  * // Basic sequential processing
375
480
  * const processItems = queuer<number>({
376
481
  * wait: 1000,
377
- * onUpdate: (queuer) => console.log(queuer.getAllItems())
482
+ * onItemsChange: (queuer) => console.log(queuer.getAllItems())
378
483
  * })
379
484
  * processItems(1) // Logs: 1
380
485
  * processItems(2) // Logs: 2 after 1 completes