@tanstack/pacer 0.6.0 → 0.7.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 (71) hide show
  1. package/dist/cjs/async-debouncer.cjs +10 -6
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +2 -2
  4. package/dist/cjs/async-queuer.cjs +135 -106
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +121 -93
  7. package/dist/cjs/async-rate-limiter.cjs +3 -4
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +1 -2
  10. package/dist/cjs/async-throttler.cjs +14 -4
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +3 -2
  13. package/dist/cjs/batcher.cjs +138 -0
  14. package/dist/cjs/batcher.cjs.map +1 -0
  15. package/dist/cjs/batcher.d.cts +149 -0
  16. package/dist/cjs/debouncer.cjs +3 -4
  17. package/dist/cjs/debouncer.cjs.map +1 -1
  18. package/dist/cjs/debouncer.d.cts +1 -2
  19. package/dist/cjs/index.cjs +3 -0
  20. package/dist/cjs/index.cjs.map +1 -1
  21. package/dist/cjs/index.d.cts +1 -0
  22. package/dist/cjs/queuer.cjs +68 -41
  23. package/dist/cjs/queuer.cjs.map +1 -1
  24. package/dist/cjs/queuer.d.cts +123 -92
  25. package/dist/cjs/rate-limiter.cjs +3 -4
  26. package/dist/cjs/rate-limiter.cjs.map +1 -1
  27. package/dist/cjs/rate-limiter.d.cts +1 -2
  28. package/dist/cjs/throttler.cjs +3 -4
  29. package/dist/cjs/throttler.cjs.map +1 -1
  30. package/dist/cjs/throttler.d.cts +1 -2
  31. package/dist/esm/async-debouncer.d.ts +2 -2
  32. package/dist/esm/async-debouncer.js +10 -6
  33. package/dist/esm/async-debouncer.js.map +1 -1
  34. package/dist/esm/async-queuer.d.ts +121 -93
  35. package/dist/esm/async-queuer.js +135 -106
  36. package/dist/esm/async-queuer.js.map +1 -1
  37. package/dist/esm/async-rate-limiter.d.ts +1 -2
  38. package/dist/esm/async-rate-limiter.js +3 -4
  39. package/dist/esm/async-rate-limiter.js.map +1 -1
  40. package/dist/esm/async-throttler.d.ts +3 -2
  41. package/dist/esm/async-throttler.js +14 -4
  42. package/dist/esm/async-throttler.js.map +1 -1
  43. package/dist/esm/batcher.d.ts +149 -0
  44. package/dist/esm/batcher.js +138 -0
  45. package/dist/esm/batcher.js.map +1 -0
  46. package/dist/esm/debouncer.d.ts +1 -2
  47. package/dist/esm/debouncer.js +3 -4
  48. package/dist/esm/debouncer.js.map +1 -1
  49. package/dist/esm/index.d.ts +1 -0
  50. package/dist/esm/index.js +3 -0
  51. package/dist/esm/index.js.map +1 -1
  52. package/dist/esm/queuer.d.ts +123 -92
  53. package/dist/esm/queuer.js +68 -41
  54. package/dist/esm/queuer.js.map +1 -1
  55. package/dist/esm/rate-limiter.d.ts +1 -2
  56. package/dist/esm/rate-limiter.js +3 -4
  57. package/dist/esm/rate-limiter.js.map +1 -1
  58. package/dist/esm/throttler.d.ts +1 -2
  59. package/dist/esm/throttler.js +3 -4
  60. package/dist/esm/throttler.js.map +1 -1
  61. package/package.json +11 -1
  62. package/src/async-debouncer.ts +12 -6
  63. package/src/async-queuer.ts +216 -193
  64. package/src/async-rate-limiter.ts +3 -4
  65. package/src/async-throttler.ts +18 -4
  66. package/src/batcher.ts +253 -0
  67. package/src/debouncer.ts +3 -4
  68. package/src/index.ts +1 -0
  69. package/src/queuer.ts +142 -98
  70. package/src/rate-limiter.ts +3 -4
  71. package/src/throttler.ts +3 -4
@@ -147,7 +147,6 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
147
147
 
148
148
  /**
149
149
  * Updates the rate limiter options
150
- * Returns the new options state
151
150
  */
152
151
  setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void {
153
152
  this._options = { ...this._options, ...newOptions }
@@ -221,7 +220,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
221
220
  if (this._options.windowType === 'sliding') {
222
221
  // For sliding window, we can execute if we have capacity in the current window
223
222
  if (this._executionTimes.length < limit) {
224
- await this.executeFunction(...args)
223
+ await this.execute(...args)
225
224
  return this._lastResult
226
225
  }
227
226
  } else {
@@ -231,7 +230,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
231
230
  const isNewWindow = oldestExecution + window <= now
232
231
 
233
232
  if (isNewWindow || this._executionTimes.length < limit) {
234
- await this.executeFunction(...args)
233
+ await this.execute(...args)
235
234
  return this._lastResult
236
235
  }
237
236
  }
@@ -240,7 +239,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
240
239
  return undefined
241
240
  }
242
241
 
243
- private async executeFunction(
242
+ private async execute(
244
243
  ...args: Parameters<TFn>
245
244
  ): Promise<ReturnType<TFn> | undefined> {
246
245
  if (!this.getEnabled()) return
@@ -114,6 +114,9 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
114
114
  private _settleCount = 0
115
115
  private _successCount = 0
116
116
  private _timeoutId: NodeJS.Timeout | null = null
117
+ private _resolvePreviousPromise:
118
+ | ((value?: ReturnType<TFn> | undefined) => void)
119
+ | null = null
117
120
 
118
121
  constructor(
119
122
  private fn: TFn,
@@ -128,7 +131,6 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
128
131
 
129
132
  /**
130
133
  * Updates the throttler options
131
- * Returns the new options state
132
134
  */
133
135
  setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {
134
136
  this._options = { ...this._options, ...newOptions }
@@ -181,15 +183,18 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
181
183
  const timeSinceLastExecution = now - this._lastExecutionTime
182
184
  const wait = this.getWait()
183
185
 
186
+ this.resolvePreviousPromise()
187
+
184
188
  // Handle leading execution
185
189
  if (this._options.leading && timeSinceLastExecution >= wait) {
186
- await this.executeFunction(...args)
190
+ await this.execute(...args)
187
191
  return this._lastResult
188
192
  } else {
189
193
  // Store the most recent arguments for potential trailing execution
190
194
  this._lastArgs = args
191
195
 
192
196
  return new Promise((resolve) => {
197
+ this._resolvePreviousPromise = resolve
193
198
  // Clear any existing timeout to ensure we use the latest arguments
194
199
  if (this._timeoutId) {
195
200
  clearTimeout(this._timeoutId)
@@ -203,8 +208,9 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
203
208
  const timeoutDuration = wait - _timeSinceLastExecution
204
209
  this._timeoutId = setTimeout(async () => {
205
210
  if (this._lastArgs !== undefined) {
206
- await this.executeFunction(...this._lastArgs)
211
+ await this.execute(...this._lastArgs)
207
212
  }
213
+ this._resolvePreviousPromise = null
208
214
  resolve(this._lastResult)
209
215
  }, timeoutDuration)
210
216
  }
@@ -212,7 +218,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
212
218
  }
213
219
  }
214
220
 
215
- private async executeFunction(
221
+ private async execute(
216
222
  ...args: Parameters<TFn>
217
223
  ): Promise<ReturnType<TFn> | undefined> {
218
224
  if (!this.getEnabled() || this._isExecuting) return undefined
@@ -241,6 +247,13 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
241
247
  return this._lastResult
242
248
  }
243
249
 
250
+ private resolvePreviousPromise(): void {
251
+ if (this._resolvePreviousPromise) {
252
+ this._resolvePreviousPromise(this._lastResult)
253
+ this._resolvePreviousPromise = null
254
+ }
255
+ }
256
+
244
257
  /**
245
258
  * Cancels any pending execution or aborts any execution in progress
246
259
  */
@@ -253,6 +266,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
253
266
  this._abortController.abort()
254
267
  this._abortController = null
255
268
  }
269
+ this.resolvePreviousPromise()
256
270
  this._lastArgs = undefined
257
271
  }
258
272
 
package/src/batcher.ts ADDED
@@ -0,0 +1,253 @@
1
+ import type { OptionalKeys } from './types'
2
+
3
+ /**
4
+ * Options for configuring a Batcher instance
5
+ */
6
+ export interface BatcherOptions<TValue> {
7
+ /**
8
+ * Custom function to determine if a batch should be processed
9
+ * Return true to process the batch immediately
10
+ */
11
+ getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean
12
+ /**
13
+ * Maximum number of items in a batch
14
+ * @default Infinity
15
+ */
16
+ maxSize?: number
17
+ /**
18
+ * Callback fired after a batch is processed
19
+ */
20
+ onExecute?: (batcher: Batcher<TValue>) => void
21
+ /**
22
+ * Callback fired when the batcher's running state changes
23
+ */
24
+ onIsRunningChange?: (batcher: Batcher<TValue>) => void
25
+ /**
26
+ * Callback fired after items are added to the batcher
27
+ */
28
+ onItemsChange?: (batcher: Batcher<TValue>) => void
29
+ /**
30
+ * Whether the batcher should start processing immediately
31
+ * @default true
32
+ */
33
+ started?: boolean
34
+ /**
35
+ * Maximum time in milliseconds to wait before processing a batch.
36
+ * If the wait duration has elapsed, the batch will be processed.
37
+ * If not provided, the batch will not be triggered by a timeout.
38
+ * @default Infinity
39
+ */
40
+ wait?: number
41
+ }
42
+
43
+ type BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<
44
+ Required<BatcherOptions<TValue>>,
45
+ 'onExecute' | 'onItemsChange' | 'onIsRunningChange'
46
+ >
47
+
48
+ const defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {
49
+ getShouldExecute: () => false,
50
+ maxSize: Infinity,
51
+ started: true,
52
+ wait: Infinity,
53
+ }
54
+
55
+ /**
56
+ * A class that collects items and processes them in batches.
57
+ *
58
+ * Batching is a technique for grouping multiple operations together to be processed as a single unit.
59
+ *
60
+ * The Batcher provides a flexible way to implement batching with configurable:
61
+ * - Maximum batch size (number of items per batch)
62
+ * - Time-based batching (process after X milliseconds)
63
+ * - Custom batch processing logic via getShouldExecute
64
+ * - Event callbacks for monitoring batch operations
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const batcher = new Batcher<number>(
69
+ * (items) => console.log('Processing batch:', items),
70
+ * {
71
+ * maxSize: 5,
72
+ * wait: 2000,
73
+ * onExecuteBatch: (items) => console.log('Batch executed:', items)
74
+ * }
75
+ * );
76
+ *
77
+ * batcher.addItem(1);
78
+ * batcher.addItem(2);
79
+ * // After 2 seconds or when 5 items are added, whichever comes first,
80
+ * // the batch will be processed
81
+ * // batcher.execute() // manually trigger a batch
82
+ * ```
83
+ */
84
+ export class Batcher<TValue> {
85
+ private _options: BatcherOptionsWithOptionalCallbacks<TValue>
86
+ private _batchExecutionCount = 0
87
+ private _itemExecutionCount = 0
88
+ private _items: Array<TValue> = []
89
+ private _running: boolean
90
+ private _timeoutId: NodeJS.Timeout | null = null
91
+
92
+ constructor(
93
+ private fn: (items: Array<TValue>) => void,
94
+ initialOptions: BatcherOptions<TValue>,
95
+ ) {
96
+ this._options = { ...defaultOptions, ...initialOptions }
97
+ this._running = this._options.started
98
+ }
99
+
100
+ /**
101
+ * Updates the batcher options
102
+ */
103
+ setOptions(newOptions: Partial<BatcherOptions<TValue>>): void {
104
+ this._options = { ...this._options, ...newOptions }
105
+ }
106
+
107
+ /**
108
+ * Returns the current batcher options
109
+ */
110
+ getOptions(): BatcherOptions<TValue> {
111
+ return this._options
112
+ }
113
+
114
+ /**
115
+ * Adds an item to the batcher
116
+ * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
117
+ */
118
+ addItem(item: TValue): void {
119
+ this._items.push(item)
120
+ this._options.onItemsChange?.(this)
121
+
122
+ const shouldProcess =
123
+ this._items.length >= this._options.maxSize ||
124
+ this._options.getShouldExecute(this._items, this)
125
+
126
+ if (shouldProcess) {
127
+ this.execute()
128
+ } else if (
129
+ this._running &&
130
+ !this._timeoutId &&
131
+ this._options.wait !== Infinity
132
+ ) {
133
+ this._timeoutId = setTimeout(() => this.execute(), this._options.wait)
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Processes the current batch of items.
139
+ * This method will automatically be triggered if the batcher is running and any of these conditions are met:
140
+ * - The number of items reaches batchSize
141
+ * - The wait duration has elapsed
142
+ * - The getShouldExecute function returns true upon adding an item
143
+ *
144
+ * You can also call this method manually to process the current batch at any time.
145
+ */
146
+ execute(): void {
147
+ if (this._timeoutId) {
148
+ clearTimeout(this._timeoutId)
149
+ this._timeoutId = null
150
+ }
151
+
152
+ if (this._items.length === 0) {
153
+ return
154
+ }
155
+
156
+ const batch = this.getAllItems() // copy of the items to be processed (to prevent race conditions)
157
+ this._items = [] // Clear items before processing to prevent race conditions
158
+ this._options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed
159
+
160
+ this.fn(batch)
161
+ this._batchExecutionCount++
162
+ this._itemExecutionCount += batch.length
163
+ this._options.onExecute?.(this)
164
+ }
165
+
166
+ /**
167
+ * Stops the batcher from processing batches
168
+ */
169
+ stop(): void {
170
+ this._running = false
171
+ this._options.onIsRunningChange?.(this)
172
+ if (this._timeoutId) {
173
+ clearTimeout(this._timeoutId)
174
+ this._timeoutId = null
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Starts the batcher and processes any pending items
180
+ */
181
+ start(): void {
182
+ this._running = true
183
+ this._options.onIsRunningChange?.(this)
184
+ if (this._items.length > 0 && !this._timeoutId) {
185
+ this._timeoutId = setTimeout(() => this.execute(), this._options.wait)
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Returns the current number of items in the batcher
191
+ */
192
+ getSize(): number {
193
+ return this._items.length
194
+ }
195
+
196
+ /**
197
+ * Returns true if the batcher is empty
198
+ */
199
+ getIsEmpty(): boolean {
200
+ return this._items.length === 0
201
+ }
202
+
203
+ /**
204
+ * Returns true if the batcher is running
205
+ */
206
+ getIsRunning(): boolean {
207
+ return this._running
208
+ }
209
+
210
+ /**
211
+ * Returns a copy of all items currently in the batcher
212
+ */
213
+ getAllItems(): Array<TValue> {
214
+ return [...this._items]
215
+ }
216
+
217
+ /**
218
+ * Returns the number of times batches have been processed
219
+ */
220
+ getBatchExecutionCount(): number {
221
+ return this._batchExecutionCount
222
+ }
223
+
224
+ /**
225
+ * Returns the total number of individual items that have been processed
226
+ */
227
+ getItemExecutionCount(): number {
228
+ return this._itemExecutionCount
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Creates a batcher that processes items in batches
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * const batchItems = batch<number>({
238
+ * batchSize: 3,
239
+ * processBatch: (items) => console.log('Processing:', items)
240
+ * });
241
+ *
242
+ * batchItems(1);
243
+ * batchItems(2);
244
+ * batchItems(3); // Triggers batch processing
245
+ * ```
246
+ */
247
+ export function batch<TValue>(
248
+ fn: (items: Array<TValue>) => void,
249
+ options: BatcherOptions<TValue>,
250
+ ) {
251
+ const batcher = new Batcher<TValue>(fn, options)
252
+ return batcher.addItem.bind(batcher)
253
+ }
package/src/debouncer.ts CHANGED
@@ -84,7 +84,6 @@ export class Debouncer<TFn extends AnyFunction> {
84
84
 
85
85
  /**
86
86
  * Updates the debouncer options
87
- * Returns the new options state
88
87
  */
89
88
  setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void {
90
89
  this._options = { ...this._options, ...newOptions }
@@ -127,7 +126,7 @@ export class Debouncer<TFn extends AnyFunction> {
127
126
  if (this._options.leading && this._canLeadingExecute) {
128
127
  this._canLeadingExecute = false
129
128
  _didLeadingExecute = true
130
- this.executeFunction(...args)
129
+ this.execute(...args)
131
130
  }
132
131
 
133
132
  // Start pending state to indicate that the debouncer is waiting for the trailing edge
@@ -142,12 +141,12 @@ export class Debouncer<TFn extends AnyFunction> {
142
141
  this._timeoutId = setTimeout(() => {
143
142
  this._canLeadingExecute = true
144
143
  if (this._options.trailing && !_didLeadingExecute) {
145
- this.executeFunction(...args)
144
+ this.execute(...args)
146
145
  }
147
146
  }, this.getWait())
148
147
  }
149
148
 
150
- private executeFunction(...args: Parameters<TFn>): void {
149
+ private execute(...args: Parameters<TFn>): void {
151
150
  if (!this.getEnabled()) return undefined
152
151
  this.fn(...args) // EXECUTE!
153
152
  this._isPending = false
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from './async-debouncer'
2
2
  export * from './async-queuer'
3
3
  export * from './async-rate-limiter'
4
4
  export * from './async-throttler'
5
+ export * from './batcher'
5
6
  export * from './compare'
6
7
  export * from './debouncer'
7
8
  export * from './queuer'