@tanstack/pacer 0.1.0 → 0.2.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 +60 -44
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +37 -24
  4. package/dist/cjs/async-queuer.cjs +149 -125
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +65 -48
  7. package/dist/cjs/async-rate-limiter.cjs +63 -46
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +39 -27
  10. package/dist/cjs/async-throttler.cjs +70 -47
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +43 -25
  13. package/dist/cjs/debouncer.cjs +46 -22
  14. package/dist/cjs/debouncer.cjs.map +1 -1
  15. package/dist/cjs/debouncer.d.cts +25 -11
  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 +114 -104
  20. package/dist/cjs/queuer.cjs.map +1 -1
  21. package/dist/cjs/queuer.d.cts +53 -40
  22. package/dist/cjs/rate-limiter.cjs +54 -42
  23. package/dist/cjs/rate-limiter.cjs.map +1 -1
  24. package/dist/cjs/rate-limiter.d.cts +37 -45
  25. package/dist/cjs/throttler.cjs +61 -41
  26. package/dist/cjs/throttler.cjs.map +1 -1
  27. package/dist/cjs/throttler.d.cts +35 -22
  28. package/dist/cjs/types.d.cts +12 -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 +37 -24
  33. package/dist/esm/async-debouncer.js +60 -44
  34. package/dist/esm/async-debouncer.js.map +1 -1
  35. package/dist/esm/async-queuer.d.ts +65 -48
  36. package/dist/esm/async-queuer.js +149 -125
  37. package/dist/esm/async-queuer.js.map +1 -1
  38. package/dist/esm/async-rate-limiter.d.ts +39 -27
  39. package/dist/esm/async-rate-limiter.js +63 -46
  40. package/dist/esm/async-rate-limiter.js.map +1 -1
  41. package/dist/esm/async-throttler.d.ts +43 -25
  42. package/dist/esm/async-throttler.js +70 -47
  43. package/dist/esm/async-throttler.js.map +1 -1
  44. package/dist/esm/debouncer.d.ts +25 -11
  45. package/dist/esm/debouncer.js +46 -22
  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 +53 -40
  51. package/dist/esm/queuer.js +114 -104
  52. package/dist/esm/queuer.js.map +1 -1
  53. package/dist/esm/rate-limiter.d.ts +37 -45
  54. package/dist/esm/rate-limiter.js +54 -42
  55. package/dist/esm/rate-limiter.js.map +1 -1
  56. package/dist/esm/throttler.d.ts +35 -22
  57. package/dist/esm/throttler.js +61 -41
  58. package/dist/esm/throttler.js.map +1 -1
  59. package/dist/esm/types.d.ts +12 -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 +90 -62
  65. package/src/async-queuer.ts +178 -145
  66. package/src/async-rate-limiter.ts +93 -67
  67. package/src/async-throttler.ts +98 -63
  68. package/src/debouncer.ts +71 -35
  69. package/src/index.ts +2 -0
  70. package/src/queuer.ts +135 -118
  71. package/src/rate-limiter.ts +79 -81
  72. package/src/throttler.ts +87 -61
  73. package/src/types.ts +17 -0
  74. package/src/utils.ts +13 -0
@@ -36,10 +36,18 @@ export interface AsyncQueuerOptions<TValue> {
36
36
  item: () => Promise<TValue>,
37
37
  queuer: AsyncQueuer<TValue>,
38
38
  ) => void
39
+ /**
40
+ * Callback fired whenever the queuer's running state changes
41
+ */
42
+ onIsRunningChange?: (queuer: AsyncQueuer<TValue>) => void
39
43
  /**
40
44
  * Callback fired whenever an item is added or removed from the queuer
41
45
  */
42
- onUpdate?: (queuer: AsyncQueuer<TValue>) => void
46
+ onItemsChange?: (queuer: AsyncQueuer<TValue>) => void
47
+ /**
48
+ * Callback fired whenever an item is rejected from being added to the queuer
49
+ */
50
+ onReject?: (item: () => Promise<TValue>, queuer: AsyncQueuer<TValue>) => void
43
51
  /**
44
52
  * Whether the queuer should start processing tasks immediately
45
53
  */
@@ -54,23 +62,25 @@ const defaultOptions: Required<AsyncQueuerOptions<any>> = {
54
62
  addItemsTo: 'back',
55
63
  concurrency: 1,
56
64
  getItemsFrom: 'front',
57
- getPriority: (item) => (item as any).priority ?? 0,
65
+ getPriority: (item) => (item as any)?.priority ?? 0,
58
66
  initialItems: [],
59
67
  maxSize: Infinity,
60
68
  onGetNextItem: () => {},
61
- onUpdate: () => {},
69
+ onIsRunningChange: () => {},
70
+ onItemsChange: () => {},
71
+ onReject: () => {},
62
72
  started: false,
63
73
  wait: 0,
64
74
  }
65
75
 
66
76
  /**
67
- * A flexible asynchronous queuer that processes tasks with configurable concurrency control.
77
+ * A flexible asynchronous queue that processes tasks with configurable concurrency control.
68
78
  *
69
79
  * Features:
70
- * - Priority queuer support via getPriority option
80
+ * - Priority queue support via getPriority option
71
81
  * - Configurable concurrency limit
72
82
  * - Task success/error/completion callbacks
73
- * - FIFO (First In First Out) or LIFO (Last In First Out) queuer behavior
83
+ * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior
74
84
  * - Pause/resume task processing
75
85
  * - Task cancellation
76
86
  *
@@ -79,38 +89,39 @@ const defaultOptions: Required<AsyncQueuerOptions<any>> = {
79
89
  *
80
90
  * @example
81
91
  * ```ts
82
- * const queuer = new AsyncQueuer<string>({ concurrency: 2 });
92
+ * const asyncQueuer = new AsyncQueuer<string>({ concurrency: 2 });
83
93
  *
84
- * queuer.addItem(async () => {
94
+ * asyncQueuer.addItem(async () => {
85
95
  * return 'Hello';
86
96
  * });
87
97
  *
88
- * queuer.start();
98
+ * asyncQueuer.start();
89
99
  *
90
- * queuer.onSuccess((result) => {
100
+ * asyncQueuer.onSuccess((result) => {
91
101
  * console.log(result); // 'Hello'
92
102
  * });
93
103
  * ```
94
104
  */
95
105
  export class AsyncQueuer<TValue> {
96
- protected options: Required<AsyncQueuerOptions<TValue>>
97
- private items: Array<() => Promise<TValue>> = []
98
- private activeItems: Set<() => Promise<TValue>> = new Set()
99
- private onSuccessCallbacks: Array<(result: TValue) => void> = []
100
- private onErrorCallbacks: Array<(error: Error) => void> = []
101
- private onSettledCallbacks: Array<(result: TValue | Error) => void> = []
102
- private running: boolean
103
- private pendingTick = false
104
- private executionCount = 0
106
+ private _options: Required<AsyncQueuerOptions<TValue>>
107
+ private _activeItems: Set<() => Promise<TValue>> = new Set()
108
+ private _executionCount = 0
109
+ private _rejectionCount = 0
110
+ private _items: Array<() => Promise<TValue>> = []
111
+ private _onErrorCallbacks: Array<(error: Error) => void> = []
112
+ private _onSettledCallbacks: Array<(result: TValue | Error) => void> = []
113
+ private _onSuccessCallbacks: Array<(result: TValue) => void> = []
114
+ private _pendingTick = false
115
+ private _running: boolean
105
116
 
106
117
  constructor(initialOptions: AsyncQueuerOptions<TValue> = defaultOptions) {
107
- this.options = { ...defaultOptions, ...initialOptions }
108
- this.running = this.options.started
118
+ this._options = { ...defaultOptions, ...initialOptions }
119
+ this._running = this._options.started
109
120
 
110
- for (let i = 0; i < this.options.initialItems.length; i++) {
111
- const item = this.options.initialItems[i]!
112
- const isLast = i === this.options.initialItems.length - 1
113
- this.addItem(item, this.options.addItemsTo, isLast)
121
+ for (let i = 0; i < this._options.initialItems.length; i++) {
122
+ const item = this._options.initialItems[i]!
123
+ const isLast = i === this._options.initialItems.length - 1
124
+ this.addItem(item, this._options.addItemsTo, isLast)
114
125
  }
115
126
  }
116
127
 
@@ -121,29 +132,36 @@ export class AsyncQueuer<TValue> {
121
132
  setOptions(
122
133
  newOptions: Partial<AsyncQueuerOptions<TValue>>,
123
134
  ): AsyncQueuerOptions<TValue> {
124
- this.options = { ...this.options, ...newOptions }
125
- return this.options
135
+ this._options = { ...this._options, ...newOptions }
136
+ return this._options
137
+ }
138
+
139
+ /**
140
+ * Returns the current queuer options
141
+ */
142
+ getOptions(): Required<AsyncQueuerOptions<TValue>> {
143
+ return this._options
126
144
  }
127
145
 
128
146
  /**
129
147
  * Processes items in the queuer
130
148
  */
131
- protected tick() {
132
- if (!this.running) {
133
- this.pendingTick = false
149
+ private tick() {
150
+ if (!this._running) {
151
+ this._pendingTick = false
134
152
  return
135
153
  }
136
154
 
137
155
  while (
138
- this.activeItems.size < this.options.concurrency &&
139
- !this.isEmpty()
156
+ this._activeItems.size < this._options.concurrency &&
157
+ !this.getIsEmpty()
140
158
  ) {
141
159
  const nextFn = this.getNextItem()
142
160
  if (!nextFn) {
143
161
  break
144
162
  }
145
-
146
- this.activeItems.add(nextFn)
163
+ this._activeItems.add(nextFn)
164
+ this._options.onItemsChange(this)
147
165
  ;(async () => {
148
166
  let success = false
149
167
  let res!: TValue
@@ -155,19 +173,19 @@ export class AsyncQueuer<TValue> {
155
173
  } catch (e) {
156
174
  error = e as Error
157
175
  } finally {
158
- this.options.onUpdate(this)
176
+ this._activeItems.delete(nextFn)
177
+ this._options.onItemsChange(this)
159
178
  }
160
179
 
161
- this.activeItems.delete(nextFn)
162
180
  if (success) {
163
- this.onSuccessCallbacks.forEach((cb) => cb(res))
181
+ this._onSuccessCallbacks.forEach((cb) => cb(res))
164
182
  } else {
165
- this.onErrorCallbacks.forEach((cb) => cb(error!))
183
+ this._onErrorCallbacks.forEach((cb) => cb(error!))
166
184
  }
167
- this.onSettledCallbacks.forEach((cb) => cb(success ? res : error!))
185
+ this._onSettledCallbacks.forEach((cb) => cb(success ? res : error!))
168
186
 
169
- if (this.options.wait > 0) {
170
- setTimeout(() => this.tick(), this.options.wait)
187
+ if (this._options.wait > 0) {
188
+ setTimeout(() => this.tick(), this._options.wait)
171
189
  return
172
190
  }
173
191
 
@@ -175,7 +193,59 @@ export class AsyncQueuer<TValue> {
175
193
  })()
176
194
  }
177
195
 
178
- this.pendingTick = false
196
+ this._pendingTick = false
197
+ }
198
+
199
+ /**
200
+ * Starts the queuer and processes items
201
+ */
202
+ start(): Promise<void> {
203
+ this._running = true
204
+ if (!this._pendingTick && !this.getIsEmpty()) {
205
+ this._pendingTick = true
206
+ this.tick()
207
+ }
208
+ this._options.onIsRunningChange(this)
209
+
210
+ return new Promise<void>((resolve) => {
211
+ const checkIdle = () => {
212
+ if (this.getIsIdle()) {
213
+ resolve()
214
+ } else {
215
+ setTimeout(checkIdle, 100)
216
+ }
217
+ }
218
+ checkIdle()
219
+ })
220
+ }
221
+
222
+ /**
223
+ * Stops the queuer from processing items
224
+ */
225
+ stop(): void {
226
+ this._running = false
227
+ this._pendingTick = false
228
+ this._options.onIsRunningChange(this)
229
+ }
230
+
231
+ /**
232
+ * Removes all items from the queuer
233
+ */
234
+ clear(): void {
235
+ this._items = []
236
+ this._options.onItemsChange(this)
237
+ }
238
+
239
+ /**
240
+ * Resets the queuer to its initial state
241
+ */
242
+ reset(withInitialItems?: boolean): void {
243
+ this.clear()
244
+ this._executionCount = 0
245
+ if (withInitialItems) {
246
+ this._items = [...this._options.initialItems]
247
+ }
248
+ this._running = this._options.started
179
249
  }
180
250
 
181
251
  /**
@@ -183,10 +253,12 @@ export class AsyncQueuer<TValue> {
183
253
  */
184
254
  addItem(
185
255
  fn: (() => Promise<TValue>) & { priority?: number },
186
- position: QueuePosition = this.options.addItemsTo,
256
+ position: QueuePosition = this._options.addItemsTo,
187
257
  runOnUpdate: boolean = true,
188
258
  ): Promise<TValue> {
189
- if (this.isFull()) {
259
+ if (this.getIsFull()) {
260
+ this._rejectionCount++
261
+ this._options.onReject(fn, this)
190
262
  return Promise.reject(new Error('Queuer is full'))
191
263
  }
192
264
 
@@ -207,40 +279,40 @@ export class AsyncQueuer<TValue> {
207
279
 
208
280
  // Get priority either from the function or from getPriority option
209
281
  const priority =
210
- this.options.getPriority !== defaultOptions.getPriority
211
- ? this.options.getPriority(task)
282
+ this._options.getPriority !== defaultOptions.getPriority
283
+ ? this._options.getPriority(task)
212
284
  : task.priority
213
285
 
214
286
  if (priority !== undefined) {
215
287
  // Insert based on priority
216
- const insertIndex = this.items.findIndex((existing) => {
288
+ const insertIndex = this._items.findIndex((existing) => {
217
289
  const existingPriority =
218
- this.options.getPriority !== defaultOptions.getPriority
219
- ? this.options.getPriority(existing)
290
+ this._options.getPriority !== defaultOptions.getPriority
291
+ ? this._options.getPriority(existing)
220
292
  : (existing as any).priority
221
293
  return existingPriority > priority
222
294
  })
223
295
 
224
296
  if (insertIndex === -1) {
225
- this.items.push(task)
297
+ this._items.push(task)
226
298
  } else {
227
- this.items.splice(insertIndex, 0, task)
299
+ this._items.splice(insertIndex, 0, task)
228
300
  }
229
301
  } else {
230
302
  // Default FIFO/LIFO behavior
231
303
  if (position === 'front') {
232
- this.items.unshift(task)
304
+ this._items.unshift(task)
233
305
  } else {
234
- this.items.push(task)
306
+ this._items.push(task)
235
307
  }
236
308
  }
237
309
 
238
310
  if (runOnUpdate) {
239
- this.options.onUpdate(this)
311
+ this._options.onItemsChange(this)
240
312
  }
241
313
 
242
- if (this.running && !this.pendingTick) {
243
- this.pendingTick = true
314
+ if (this._running && !this._pendingTick) {
315
+ this._pendingTick = true
244
316
  this.tick()
245
317
  }
246
318
  })
@@ -250,20 +322,20 @@ export class AsyncQueuer<TValue> {
250
322
  * Removes and returns an item from the queuer
251
323
  */
252
324
  getNextItem(
253
- position: QueuePosition = this.options.getItemsFrom,
325
+ position: QueuePosition = this._options.getItemsFrom,
254
326
  ): (() => Promise<TValue>) | undefined {
255
327
  let item: (() => Promise<TValue>) | undefined
256
328
 
257
329
  if (position === 'front') {
258
- item = this.items.shift()
330
+ item = this._items.shift()
259
331
  } else {
260
- item = this.items.pop()
332
+ item = this._items.pop()
261
333
  }
262
334
 
263
335
  if (item !== undefined) {
264
- this.executionCount++
265
- this.options.onUpdate(this)
266
- this.options.onGetNextItem(item, this)
336
+ this._executionCount++
337
+ this._options.onItemsChange(this)
338
+ this._options.onGetNextItem(item, this)
267
339
  }
268
340
  return item
269
341
  }
@@ -271,89 +343,94 @@ export class AsyncQueuer<TValue> {
271
343
  /**
272
344
  * Returns an item without removing it
273
345
  */
274
- peek(position: QueuePosition = 'front'): (() => Promise<TValue>) | undefined {
346
+ getPeek(
347
+ position: QueuePosition = 'front',
348
+ ): (() => Promise<TValue>) | undefined {
275
349
  if (position === 'front') {
276
- return this.items[0]
350
+ return this._items[0]
277
351
  }
278
- return this.items[this.items.length - 1]
352
+ return this._items[this._items.length - 1]
279
353
  }
280
354
 
281
355
  /**
282
356
  * Returns true if the queuer is empty
283
357
  */
284
- isEmpty(): boolean {
285
- return this.items.length === 0
358
+ getIsEmpty(): boolean {
359
+ return this._items.length === 0
286
360
  }
287
361
 
288
362
  /**
289
363
  * Returns true if the queuer is full
290
364
  */
291
- isFull(): boolean {
292
- return this.items.length >= this.options.maxSize
365
+ getIsFull(): boolean {
366
+ return this._items.length >= this._options.maxSize
293
367
  }
294
368
 
295
369
  /**
296
370
  * Returns the current size of the queuer
297
371
  */
298
- size(): number {
299
- return this.items.length
372
+ getSize(): number {
373
+ return this._items.length
300
374
  }
301
375
 
302
376
  /**
303
- * Removes all items from the queuer
377
+ * Returns a copy of all items in the queuer
304
378
  */
305
- clear(): void {
306
- this.items = []
307
- this.options.onUpdate(this)
379
+ getAllItems(): Array<() => Promise<TValue>> {
380
+ return [...this.getActiveItems(), ...this.getPendingItems()]
308
381
  }
309
382
 
310
383
  /**
311
- * Resets the queuer to its initial state
384
+ * Returns the active items
312
385
  */
313
- reset(withInitialItems?: boolean): void {
314
- this.clear()
315
- this.executionCount = 0
316
- if (withInitialItems) {
317
- this.items = [...this.options.initialItems]
318
- }
319
- this.running = this.options.started
386
+ getActiveItems(): Array<() => Promise<TValue>> {
387
+ return Array.from(this._activeItems)
320
388
  }
321
389
 
322
390
  /**
323
- * Returns a copy of all items in the queuer
391
+ * Returns the pending items
324
392
  */
325
- getAllItems(): Array<() => Promise<TValue>> {
326
- return [...this.items]
393
+ getPendingItems(): Array<() => Promise<TValue>> {
394
+ return [...this._items]
327
395
  }
328
396
 
329
397
  /**
330
398
  * Returns the number of items that have been removed from the queuer
331
399
  */
332
400
  getExecutionCount(): number {
333
- return this.executionCount
401
+ return this._executionCount
334
402
  }
335
403
 
336
404
  /**
337
- * Returns the active items
405
+ * Returns the number of items that have been rejected from the queuer
338
406
  */
339
- getActiveItems(): Array<() => Promise<TValue>> {
340
- return Array.from(this.activeItems)
407
+ getRejectionCount(): number {
408
+ return this._rejectionCount
341
409
  }
342
410
 
343
411
  /**
344
- * Returns the pending items
412
+ * Returns true if the queuer is running
345
413
  */
346
- getPendingItems(): Array<() => Promise<TValue>> {
347
- return this.getAllItems()
414
+ getIsRunning(): boolean {
415
+ return this._running
416
+ }
417
+
418
+ /**
419
+ * Returns true if the queuer is running but has no items to process
420
+ */
421
+ getIsIdle(): boolean {
422
+ return this._running && this.getIsEmpty() && this._activeItems.size === 0
348
423
  }
349
424
 
350
425
  /**
351
426
  * Adds a callback to be called when a task succeeds
352
427
  */
353
428
  onSuccess(cb: (result: TValue) => void) {
354
- this.onSuccessCallbacks.push(cb)
429
+ this._onSuccessCallbacks.push(cb)
355
430
  return () => {
356
- this.onSuccessCallbacks = this.onSuccessCallbacks.filter((d) => d !== cb)
431
+ this._onSuccessCallbacks = this._onSuccessCallbacks.filter(
432
+ (d) => d !== cb,
433
+ )
357
434
  }
358
435
  }
359
436
 
@@ -361,9 +438,9 @@ export class AsyncQueuer<TValue> {
361
438
  * Adds a callback to be called when a task errors
362
439
  */
363
440
  onError(cb: (error: Error) => void) {
364
- this.onErrorCallbacks.push(cb)
441
+ this._onErrorCallbacks.push(cb)
365
442
  return () => {
366
- this.onErrorCallbacks = this.onErrorCallbacks.filter((d) => d !== cb)
443
+ this._onErrorCallbacks = this._onErrorCallbacks.filter((d) => d !== cb)
367
444
  }
368
445
  }
369
446
 
@@ -371,56 +448,12 @@ export class AsyncQueuer<TValue> {
371
448
  * Adds a callback to be called when a task is settled
372
449
  */
373
450
  onSettled(cb: (result: TValue | Error) => void) {
374
- this.onSettledCallbacks.push(cb)
451
+ this._onSettledCallbacks.push(cb)
375
452
  return () => {
376
- this.onSettledCallbacks = this.onSettledCallbacks.filter((d) => d !== cb)
377
- }
378
- }
379
-
380
- /**
381
- * Starts the queuer and processes items
382
- */
383
- start(): Promise<void> {
384
- this.running = true
385
- if (!this.pendingTick && !this.isEmpty()) {
386
- this.pendingTick = true
387
- this.tick()
453
+ this._onSettledCallbacks = this._onSettledCallbacks.filter(
454
+ (d) => d !== cb,
455
+ )
388
456
  }
389
- this.options.onUpdate(this)
390
-
391
- return new Promise<void>((resolve) => {
392
- const checkIdle = () => {
393
- if (this.isIdle()) {
394
- resolve()
395
- } else {
396
- setTimeout(checkIdle, 100)
397
- }
398
- }
399
- checkIdle()
400
- })
401
- }
402
-
403
- /**
404
- * Stops the queuer from processing items
405
- */
406
- stop(): void {
407
- this.running = false
408
- this.pendingTick = false
409
- this.options.onUpdate(this)
410
- }
411
-
412
- /**
413
- * Returns true if the queuer is running
414
- */
415
- isRunning(): boolean {
416
- return this.running
417
- }
418
-
419
- /**
420
- * Returns true if the queuer is running but has no items to process
421
- */
422
- isIdle(): boolean {
423
- return this.running && this.isEmpty() && this.activeItems.size === 0
424
457
  }
425
458
  }
426
459