@tanstack/pacer 0.13.0 → 0.15.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 (92) hide show
  1. package/dist/cjs/async-batcher.cjs +8 -0
  2. package/dist/cjs/async-batcher.cjs.map +1 -1
  3. package/dist/cjs/async-batcher.d.cts +8 -2
  4. package/dist/cjs/async-debouncer.cjs +17 -5
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +11 -1
  7. package/dist/cjs/async-queuer.cjs +14 -2
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +11 -1
  10. package/dist/cjs/async-rate-limiter.cjs +14 -2
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +11 -1
  13. package/dist/cjs/async-throttler.cjs +15 -3
  14. package/dist/cjs/async-throttler.cjs.map +1 -1
  15. package/dist/cjs/async-throttler.d.cts +11 -1
  16. package/dist/cjs/batcher.cjs +8 -0
  17. package/dist/cjs/batcher.cjs.map +1 -1
  18. package/dist/cjs/batcher.d.cts +8 -2
  19. package/dist/cjs/debouncer.cjs +15 -3
  20. package/dist/cjs/debouncer.cjs.map +1 -1
  21. package/dist/cjs/debouncer.d.cts +11 -1
  22. package/dist/cjs/event-client.cjs +18 -0
  23. package/dist/cjs/event-client.cjs.map +1 -0
  24. package/dist/cjs/event-client.d.cts +42 -0
  25. package/dist/cjs/index.cjs +3 -0
  26. package/dist/cjs/index.cjs.map +1 -1
  27. package/dist/cjs/index.d.cts +2 -0
  28. package/dist/cjs/queuer.cjs +13 -1
  29. package/dist/cjs/queuer.cjs.map +1 -1
  30. package/dist/cjs/queuer.d.cts +11 -1
  31. package/dist/cjs/rate-limiter.cjs +15 -3
  32. package/dist/cjs/rate-limiter.cjs.map +1 -1
  33. package/dist/cjs/rate-limiter.d.cts +11 -1
  34. package/dist/cjs/throttler.cjs +15 -3
  35. package/dist/cjs/throttler.cjs.map +1 -1
  36. package/dist/cjs/throttler.d.cts +11 -1
  37. package/dist/cjs/utils.cjs +10 -0
  38. package/dist/cjs/utils.cjs.map +1 -1
  39. package/dist/cjs/utils.d.cts +1 -0
  40. package/dist/esm/async-batcher.d.ts +8 -2
  41. package/dist/esm/async-batcher.js +9 -1
  42. package/dist/esm/async-batcher.js.map +1 -1
  43. package/dist/esm/async-debouncer.d.ts +11 -1
  44. package/dist/esm/async-debouncer.js +18 -6
  45. package/dist/esm/async-debouncer.js.map +1 -1
  46. package/dist/esm/async-queuer.d.ts +11 -1
  47. package/dist/esm/async-queuer.js +15 -3
  48. package/dist/esm/async-queuer.js.map +1 -1
  49. package/dist/esm/async-rate-limiter.d.ts +11 -1
  50. package/dist/esm/async-rate-limiter.js +15 -3
  51. package/dist/esm/async-rate-limiter.js.map +1 -1
  52. package/dist/esm/async-throttler.d.ts +11 -1
  53. package/dist/esm/async-throttler.js +16 -4
  54. package/dist/esm/async-throttler.js.map +1 -1
  55. package/dist/esm/batcher.d.ts +8 -2
  56. package/dist/esm/batcher.js +9 -1
  57. package/dist/esm/batcher.js.map +1 -1
  58. package/dist/esm/debouncer.d.ts +11 -1
  59. package/dist/esm/debouncer.js +16 -4
  60. package/dist/esm/debouncer.js.map +1 -1
  61. package/dist/esm/event-client.d.ts +42 -0
  62. package/dist/esm/event-client.js +18 -0
  63. package/dist/esm/event-client.js.map +1 -0
  64. package/dist/esm/index.d.ts +2 -0
  65. package/dist/esm/index.js +4 -1
  66. package/dist/esm/index.js.map +1 -1
  67. package/dist/esm/queuer.d.ts +11 -1
  68. package/dist/esm/queuer.js +14 -2
  69. package/dist/esm/queuer.js.map +1 -1
  70. package/dist/esm/rate-limiter.d.ts +11 -1
  71. package/dist/esm/rate-limiter.js +16 -4
  72. package/dist/esm/rate-limiter.js.map +1 -1
  73. package/dist/esm/throttler.d.ts +11 -1
  74. package/dist/esm/throttler.js +16 -4
  75. package/dist/esm/throttler.js.map +1 -1
  76. package/dist/esm/utils.d.ts +1 -0
  77. package/dist/esm/utils.js +10 -0
  78. package/dist/esm/utils.js.map +1 -1
  79. package/package.json +12 -1
  80. package/src/async-batcher.ts +23 -3
  81. package/src/async-debouncer.ts +30 -7
  82. package/src/async-queuer.ts +29 -4
  83. package/src/async-rate-limiter.ts +28 -4
  84. package/src/async-throttler.ts +27 -5
  85. package/src/batcher.ts +18 -3
  86. package/src/debouncer.ts +30 -5
  87. package/src/event-client.ts +63 -0
  88. package/src/index.ts +2 -0
  89. package/src/queuer.ts +26 -2
  90. package/src/rate-limiter.ts +29 -5
  91. package/src/throttler.ts +29 -5
  92. package/src/utils.ts +15 -0
@@ -1,5 +1,6 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { parseFunctionOrValue } from './utils'
2
+ import { createKey, parseFunctionOrValue } from './utils'
3
+ import { emitChange, pacerEventClient } from './event-client'
3
4
  import type { AnyAsyncFunction, OptionalKeys } from './types'
4
5
 
5
6
  export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {
@@ -27,6 +28,10 @@ export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {
27
28
  * The result from the most recent successful function execution
28
29
  */
29
30
  lastResult: ReturnType<TFn> | undefined
31
+ /**
32
+ * Number of times maybeExecute has been called (for reduction calculations)
33
+ */
34
+ maybeExecuteCount: number
30
35
  /**
31
36
  * Number of function executions that have completed (either successfully or with errors)
32
37
  */
@@ -44,17 +49,18 @@ export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {
44
49
  function getDefaultAsyncDebouncerState<
45
50
  TFn extends AnyAsyncFunction,
46
51
  >(): AsyncDebouncerState<TFn> {
47
- return structuredClone({
52
+ return {
48
53
  canLeadingExecute: true,
49
54
  errorCount: 0,
50
55
  isExecuting: false,
51
56
  isPending: false,
52
57
  lastArgs: undefined,
53
58
  lastResult: undefined,
59
+ maybeExecuteCount: 0,
54
60
  settleCount: 0,
55
- successCount: 0,
56
61
  status: 'idle',
57
- })
62
+ successCount: 0,
63
+ }
58
64
  }
59
65
 
60
66
  /**
@@ -71,6 +77,11 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
71
77
  * Initial state for the async debouncer
72
78
  */
73
79
  initialState?: Partial<AsyncDebouncerState<TFn>>
80
+ /**
81
+ * Optional key to identify this async debouncer instance.
82
+ * If provided, the async debouncer will be identified by this key in the devtools and PacerProvider if applicable.
83
+ */
84
+ key?: string
74
85
  /**
75
86
  * Whether to execute on the leading edge of the timeout.
76
87
  * Defaults to false.
@@ -119,7 +130,7 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
119
130
 
120
131
  type AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<
121
132
  AsyncDebouncerOptions<any>,
122
- 'initialState' | 'onError' | 'onSettled' | 'onSuccess'
133
+ 'initialState' | 'onError' | 'onSettled' | 'onSuccess' | 'key'
123
134
  >
124
135
 
125
136
  const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {
@@ -178,6 +189,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
178
189
  readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<
179
190
  AsyncDebouncerState<TFn>
180
191
  >(getDefaultAsyncDebouncerState<TFn>())
192
+ key: string
181
193
  options: AsyncDebouncerOptions<TFn>
182
194
  #abortController: AbortController | null = null
183
195
  #timeoutId: NodeJS.Timeout | null = null
@@ -186,15 +198,22 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
186
198
  | null = null
187
199
 
188
200
  constructor(
189
- private fn: TFn,
201
+ public fn: TFn,
190
202
  initialOptions: AsyncDebouncerOptions<TFn>,
191
203
  ) {
204
+ this.key = createKey(initialOptions.key)
192
205
  this.options = {
193
206
  ...defaultOptions,
194
207
  ...initialOptions,
195
208
  throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
196
209
  }
197
210
  this.#setState(this.options.initialState ?? {})
211
+
212
+ pacerEventClient.on('d-AsyncDebouncer', (event) => {
213
+ if (event.payload.key !== this.key) return
214
+ this.#setState(event.payload.store.state as AsyncDebouncerState<TFn>)
215
+ this.setOptions(event.payload.options)
216
+ })
198
217
  }
199
218
 
200
219
  /**
@@ -229,6 +248,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
229
248
  : 'idle',
230
249
  }
231
250
  })
251
+ emitChange('AsyncDebouncer', this)
232
252
  }
233
253
 
234
254
  /**
@@ -264,7 +284,10 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
264
284
  ): Promise<ReturnType<TFn> | undefined> => {
265
285
  if (!this.#getEnabled()) return undefined
266
286
  this.#cancelPendingExecution()
267
- this.#setState({ lastArgs: args })
287
+ this.#setState({
288
+ lastArgs: args,
289
+ maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
290
+ })
268
291
 
269
292
  // Handle leading execution
270
293
  if (this.options.leading && this.store.state.canLeadingExecute) {
@@ -1,5 +1,6 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { parseFunctionOrValue } from './utils'
2
+ import { createKey, parseFunctionOrValue } from './utils'
3
+ import { emitChange, pacerEventClient } from './event-client'
3
4
  import type { OptionalKeys } from './types'
4
5
  import type { QueuePosition } from './queuer'
5
6
 
@@ -8,6 +9,10 @@ export interface AsyncQueuerState<TValue> {
8
9
  * Items currently being processed by the queuer
9
10
  */
10
11
  activeItems: Array<TValue>
12
+ /**
13
+ * Number of times addItem has been called (for reduction calculations)
14
+ */
15
+ addItemCount: number
11
16
  /**
12
17
  * Number of task executions that have resulted in errors
13
18
  */
@@ -71,8 +76,9 @@ export interface AsyncQueuerState<TValue> {
71
76
  }
72
77
 
73
78
  function getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {
74
- return structuredClone({
79
+ return {
75
80
  activeItems: [],
81
+ addItemCount: 0,
76
82
  errorCount: 0,
77
83
  expirationCount: 0,
78
84
  isEmpty: true,
@@ -88,7 +94,7 @@ function getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {
88
94
  size: 0,
89
95
  status: 'idle',
90
96
  successCount: 0,
91
- })
97
+ }
92
98
  }
93
99
 
94
100
  export interface AsyncQueuerOptions<TValue> {
@@ -132,6 +138,11 @@ export interface AsyncQueuerOptions<TValue> {
132
138
  * Initial state for the async queuer
133
139
  */
134
140
  initialState?: Partial<AsyncQueuerState<TValue>>
141
+ /**
142
+ * Optional key to identify this async queuer instance.
143
+ * If provided, the async queuer will be identified by this key in the devtools and PacerProvider if applicable.
144
+ */
145
+ key?: string
135
146
  /**
136
147
  * Maximum number of items allowed in the queuer
137
148
  */
@@ -190,6 +201,7 @@ type AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<
190
201
  | 'onItemsChange'
191
202
  | 'onExpire'
192
203
  | 'onError'
204
+ | 'key'
193
205
  >
194
206
 
195
207
  const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {
@@ -260,13 +272,15 @@ export class AsyncQueuer<TValue> {
260
272
  readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<
261
273
  AsyncQueuerState<TValue>
262
274
  >(getDefaultAsyncQueuerState<TValue>())
275
+ key: string
263
276
  options: AsyncQueuerOptions<TValue>
264
277
  #timeoutIds: Set<NodeJS.Timeout> = new Set()
265
278
 
266
279
  constructor(
267
- private fn: (item: TValue) => Promise<any>,
280
+ public fn: (item: TValue) => Promise<any>,
268
281
  initialOptions: AsyncQueuerOptions<TValue> = {},
269
282
  ) {
283
+ this.key = createKey(initialOptions.key)
270
284
  this.options = {
271
285
  ...defaultOptions,
272
286
  ...initialOptions,
@@ -290,6 +304,12 @@ export class AsyncQueuer<TValue> {
290
304
  this.addItem(item, this.options.addItemsTo ?? 'back', isLast)
291
305
  }
292
306
  }
307
+
308
+ pacerEventClient.on('d-AsyncQueuer', (e) => {
309
+ if (e.payload.key !== this.key) return
310
+ this.#setState(e.payload.store.state)
311
+ this.setOptions(e.payload.options)
312
+ })
293
313
  }
294
314
 
295
315
  /**
@@ -324,6 +344,7 @@ export class AsyncQueuer<TValue> {
324
344
  status,
325
345
  }
326
346
  })
347
+ emitChange('AsyncQueuer', this)
327
348
  }
328
349
 
329
350
  /**
@@ -402,6 +423,10 @@ export class AsyncQueuer<TValue> {
402
423
  position: QueuePosition = this.options.addItemsTo ?? 'back',
403
424
  runOnItemsChange: boolean = true,
404
425
  ): boolean => {
426
+ this.#setState({
427
+ addItemCount: this.store.state.addItemCount + 1,
428
+ })
429
+
405
430
  if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
406
431
  this.#setState({
407
432
  rejectionCount: this.store.state.rejectionCount + 1,
@@ -1,5 +1,6 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { parseFunctionOrValue } from './utils'
2
+ import { createKey, parseFunctionOrValue } from './utils'
3
+ import { emitChange, pacerEventClient } from './event-client'
3
4
  import type { AnyAsyncFunction } from './types'
4
5
 
5
6
  export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
@@ -39,6 +40,10 @@ export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
39
40
  * Number of function executions that have completed successfully
40
41
  */
41
42
  successCount: number
43
+ /**
44
+ * Number of times maybeExecute has been called (for reduction calculations)
45
+ */
46
+ maybeExecuteCount: number
42
47
  }
43
48
 
44
49
  function getDefaultAsyncRateLimiterState<
@@ -50,10 +55,11 @@ function getDefaultAsyncRateLimiterState<
50
55
  isExceeded: false,
51
56
  isExecuting: false,
52
57
  lastResult: undefined,
58
+ maybeExecuteCount: 0,
53
59
  rejectionCount: 0,
54
60
  settleCount: 0,
55
- successCount: 0,
56
61
  status: 'idle',
62
+ successCount: 0,
57
63
  }
58
64
  }
59
65
 
@@ -71,6 +77,11 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
71
77
  * Initial state for the rate limiter
72
78
  */
73
79
  initialState?: Partial<AsyncRateLimiterState<TFn>>
80
+ /**
81
+ * Optional key to identify this async rate limiter instance.
82
+ * If provided, the async rate limiter will be identified by this key in the devtools and PacerProvider if applicable.
83
+ */
84
+ key?: string
74
85
  /**
75
86
  * Maximum number of executions allowed within the time window.
76
87
  * Can be a number or a function that returns a number.
@@ -127,7 +138,7 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
127
138
 
128
139
  const defaultOptions: Omit<
129
140
  Required<AsyncRateLimiterOptions<any>>,
130
- 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess'
141
+ 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess' | 'key'
131
142
  > = {
132
143
  enabled: true,
133
144
  limit: 1,
@@ -206,13 +217,15 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
206
217
  readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<
207
218
  AsyncRateLimiterState<TFn>
208
219
  >(getDefaultAsyncRateLimiterState<TFn>())
220
+ key: string
209
221
  options: AsyncRateLimiterOptions<TFn>
210
222
  #timeoutIds: Set<NodeJS.Timeout> = new Set()
211
223
 
212
224
  constructor(
213
- private fn: TFn,
225
+ public fn: TFn,
214
226
  initialOptions: AsyncRateLimiterOptions<TFn>,
215
227
  ) {
228
+ this.key = createKey(initialOptions.key)
216
229
  this.options = {
217
230
  ...defaultOptions,
218
231
  ...initialOptions,
@@ -222,6 +235,12 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
222
235
  for (const executionTime of this.#getExecutionTimesInWindow()) {
223
236
  this.#setCleanupTimeout(executionTime)
224
237
  }
238
+
239
+ pacerEventClient.on('d-AsyncRateLimiter', (event) => {
240
+ if (event.payload.key !== this.key) return
241
+ this.#setState(event.payload.store.state)
242
+ this.setOptions(event.payload.options)
243
+ })
225
244
  }
226
245
 
227
246
  /**
@@ -251,6 +270,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
251
270
  status,
252
271
  }
253
272
  })
273
+ emitChange('AsyncRateLimiter', this)
254
274
  }
255
275
 
256
276
  /**
@@ -302,6 +322,10 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
302
322
  maybeExecute = async (
303
323
  ...args: Parameters<TFn>
304
324
  ): Promise<ReturnType<TFn> | undefined> => {
325
+ this.#setState({
326
+ maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
327
+ })
328
+
305
329
  this.#cleanupOldExecutions()
306
330
 
307
331
  const relevantExecutionTimes = this.#getExecutionTimesInWindow()
@@ -1,5 +1,6 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { parseFunctionOrValue } from './utils'
2
+ import { createKey, parseFunctionOrValue } from './utils'
3
+ import { emitChange, pacerEventClient } from './event-client'
3
4
  import type { AnyAsyncFunction, OptionalKeys } from './types'
4
5
 
5
6
  export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
@@ -27,6 +28,10 @@ export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
27
28
  * The result from the most recent successful function execution
28
29
  */
29
30
  lastResult: ReturnType<TFn> | undefined
31
+ /**
32
+ * Number of times maybeExecute has been called (for reduction calculations)
33
+ */
34
+ maybeExecuteCount: number
30
35
  /**
31
36
  * Timestamp when the next execution can occur in milliseconds
32
37
  */
@@ -48,18 +53,19 @@ export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
48
53
  function getDefaultAsyncThrottlerState<
49
54
  TFn extends AnyAsyncFunction,
50
55
  >(): AsyncThrottlerState<TFn> {
51
- return structuredClone({
56
+ return {
52
57
  errorCount: 0,
53
58
  isExecuting: false,
54
59
  isPending: false,
55
60
  lastArgs: undefined,
56
61
  lastExecutionTime: 0,
57
62
  lastResult: undefined,
63
+ maybeExecuteCount: 0,
58
64
  nextExecutionTime: undefined,
59
65
  settleCount: 0,
60
66
  status: 'idle',
61
67
  successCount: 0,
62
- })
68
+ }
63
69
  }
64
70
 
65
71
  /**
@@ -76,6 +82,11 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
76
82
  * Initial state for the async throttler
77
83
  */
78
84
  initialState?: Partial<AsyncThrottlerState<TFn>>
85
+ /**
86
+ * Optional key to identify this async throttler instance.
87
+ * If provided, the async throttler will be identified by this key in the devtools and PacerProvider if applicable.
88
+ */
89
+ key?: string
79
90
  /**
80
91
  * Whether to execute the function immediately when called
81
92
  * Defaults to true
@@ -189,6 +200,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
189
200
  readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<
190
201
  AsyncThrottlerState<TFn>
191
202
  >(getDefaultAsyncThrottlerState<TFn>())
203
+ key: string
192
204
  options: AsyncThrottlerOptions<TFn>
193
205
  #abortController: AbortController | null = null
194
206
  #timeoutId: NodeJS.Timeout | null = null
@@ -197,15 +209,21 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
197
209
  | null = null
198
210
 
199
211
  constructor(
200
- private fn: TFn,
212
+ public fn: TFn,
201
213
  initialOptions: AsyncThrottlerOptions<TFn>,
202
214
  ) {
215
+ this.key = createKey(initialOptions.key)
203
216
  this.options = {
204
217
  ...defaultOptions,
205
218
  ...initialOptions,
206
219
  throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
207
220
  }
208
221
  this.#setState(this.options.initialState ?? {})
222
+ pacerEventClient.on('d-AsyncThrottler', (event) => {
223
+ if (event.payload.key !== this.key) return
224
+ this.#setState(event.payload.store.state as AsyncThrottlerState<TFn>)
225
+ this.setOptions(event.payload.options)
226
+ })
209
227
  }
210
228
 
211
229
  /**
@@ -240,6 +258,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
240
258
  : 'idle',
241
259
  }
242
260
  })
261
+ emitChange('AsyncThrottler', this)
243
262
  }
244
263
 
245
264
  /**
@@ -286,7 +305,10 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
286
305
  const timeSinceLastExecution = now - this.store.state.lastExecutionTime
287
306
  const wait = this.#getWait()
288
307
  // Store the most recent arguments for potential trailing execution
289
- this.#setState({ lastArgs: args })
308
+ this.#setState({
309
+ lastArgs: args,
310
+ maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
311
+ })
290
312
 
291
313
  this.#resolvePreviousPromiseInternal()
292
314
 
package/src/batcher.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { parseFunctionOrValue } from './utils'
2
+ import { createKey, parseFunctionOrValue } from './utils'
3
+ import { emitChange, pacerEventClient } from './event-client'
3
4
  import type { OptionalKeys } from './types'
4
5
 
5
6
  export interface BatcherState<TValue> {
@@ -58,6 +59,11 @@ export interface BatcherOptions<TValue> {
58
59
  * Initial state for the batcher
59
60
  */
60
61
  initialState?: Partial<BatcherState<TValue>>
62
+ /**
63
+ * Optional key to identify this batcher instance.
64
+ * If provided, the batcher will be identified by this key in the devtools and PacerProvider if applicable.
65
+ */
66
+ key?: string
61
67
  /**
62
68
  * Maximum number of items in a batch
63
69
  * @default Infinity
@@ -87,7 +93,7 @@ export interface BatcherOptions<TValue> {
87
93
 
88
94
  type BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<
89
95
  Required<BatcherOptions<TValue>>,
90
- 'initialState' | 'onExecute' | 'onItemsChange'
96
+ 'initialState' | 'onExecute' | 'onItemsChange' | 'key'
91
97
  >
92
98
 
93
99
  const defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {
@@ -139,18 +145,26 @@ export class Batcher<TValue> {
139
145
  readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(
140
146
  getDefaultBatcherState<TValue>(),
141
147
  )
148
+ key: string
142
149
  options: BatcherOptionsWithOptionalCallbacks<TValue>
143
150
  #timeoutId: NodeJS.Timeout | null = null
144
151
 
145
152
  constructor(
146
- private fn: (items: Array<TValue>) => void,
153
+ public fn: (items: Array<TValue>) => void,
147
154
  initialOptions: BatcherOptions<TValue>,
148
155
  ) {
156
+ this.key = createKey(initialOptions.key)
149
157
  this.options = {
150
158
  ...defaultOptions,
151
159
  ...initialOptions,
152
160
  }
153
161
  this.#setState(this.options.initialState ?? {})
162
+
163
+ pacerEventClient.on('d-Batcher', (event) => {
164
+ if (event.payload.key !== this.key) return
165
+ this.#setState(event.payload.store.state)
166
+ this.setOptions(event.payload.options)
167
+ })
154
168
  }
155
169
 
156
170
  /**
@@ -176,6 +190,7 @@ export class Batcher<TValue> {
176
190
  status: isPending ? 'pending' : 'idle',
177
191
  }
178
192
  })
193
+ emitChange('Batcher', this)
179
194
  }
180
195
 
181
196
  #getWait = (): number => {
package/src/debouncer.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { parseFunctionOrValue } from './utils'
2
+ import { createKey, parseFunctionOrValue } from './utils'
3
+ import { emitChange, pacerEventClient } from './event-client'
3
4
  import type { AnyFunction } from './types'
4
5
 
5
6
  export interface DebouncerState<TFn extends AnyFunction> {
@@ -19,6 +20,10 @@ export interface DebouncerState<TFn extends AnyFunction> {
19
20
  * The arguments from the most recent call to maybeExecute
20
21
  */
21
22
  lastArgs: Parameters<TFn> | undefined
23
+ /**
24
+ * Number of times maybeExecute has been called (for reduction calculations)
25
+ */
26
+ maybeExecuteCount: number
22
27
  /**
23
28
  * Current execution status - 'idle' when not active, 'pending' when waiting for timeout
24
29
  */
@@ -28,13 +33,14 @@ export interface DebouncerState<TFn extends AnyFunction> {
28
33
  function getDefaultDebouncerState<
29
34
  TFn extends AnyFunction,
30
35
  >(): DebouncerState<TFn> {
31
- return structuredClone({
36
+ return {
32
37
  canLeadingExecute: true,
33
38
  executionCount: 0,
34
39
  isPending: false,
35
40
  lastArgs: undefined,
36
41
  status: 'idle',
37
- })
42
+ maybeExecuteCount: 0,
43
+ }
38
44
  }
39
45
 
40
46
  /**
@@ -51,6 +57,11 @@ export interface DebouncerOptions<TFn extends AnyFunction> {
51
57
  * Initial state for the debouncer
52
58
  */
53
59
  initialState?: Partial<DebouncerState<TFn>>
60
+ /**
61
+ * A key to identify the debouncer.
62
+ * If provided, the debouncer will be identified by this key in the devtools and PacerProvider if applicable.
63
+ */
64
+ key?: string
54
65
  /**
55
66
  * Whether to execute on the leading edge of the timeout.
56
67
  * The first call will execute immediately and the rest will wait the delay.
@@ -76,7 +87,7 @@ export interface DebouncerOptions<TFn extends AnyFunction> {
76
87
 
77
88
  const defaultOptions: Omit<
78
89
  Required<DebouncerOptions<any>>,
79
- 'initialState' | 'onExecute'
90
+ 'initialState' | 'onExecute' | 'key'
80
91
  > = {
81
92
  enabled: true,
82
93
  leading: false,
@@ -119,18 +130,26 @@ export class Debouncer<TFn extends AnyFunction> {
119
130
  readonly store: Store<Readonly<DebouncerState<TFn>>> = new Store(
120
131
  getDefaultDebouncerState<TFn>(),
121
132
  )
133
+ key: string
122
134
  options: DebouncerOptions<TFn>
123
135
  #timeoutId: NodeJS.Timeout | undefined
124
136
 
125
137
  constructor(
126
- private fn: TFn,
138
+ public fn: TFn,
127
139
  initialOptions: DebouncerOptions<TFn>,
128
140
  ) {
141
+ this.key = createKey(initialOptions.key)
129
142
  this.options = {
130
143
  ...defaultOptions,
131
144
  ...initialOptions,
132
145
  }
133
146
  this.#setState(this.options.initialState ?? {})
147
+
148
+ pacerEventClient.on('d-Debouncer', (event) => {
149
+ if (event.payload.key !== this.key) return
150
+ this.#setState(event.payload.store.state as DebouncerState<TFn>)
151
+ this.setOptions(event.payload.options)
152
+ })
134
153
  }
135
154
 
136
155
  /**
@@ -161,6 +180,7 @@ export class Debouncer<TFn extends AnyFunction> {
161
180
  : 'idle',
162
181
  }
163
182
  })
183
+ emitChange('Debouncer', this)
164
184
  }
165
185
 
166
186
  /**
@@ -183,6 +203,11 @@ export class Debouncer<TFn extends AnyFunction> {
183
203
  */
184
204
  maybeExecute = (...args: Parameters<TFn>): void => {
185
205
  if (!this.#getEnabled()) return undefined
206
+
207
+ this.#setState({
208
+ maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
209
+ })
210
+
186
211
  let _didLeadingExecute = false
187
212
 
188
213
  // Handle leading execution
@@ -0,0 +1,63 @@
1
+ import { EventClient } from '@tanstack/devtools-event-client'
2
+ import type { AsyncBatcher } from './async-batcher'
3
+ import type { AsyncDebouncer } from './async-debouncer'
4
+ import type { AsyncQueuer } from './async-queuer'
5
+ import type { AsyncRateLimiter } from './async-rate-limiter'
6
+ import type { AsyncThrottler } from './async-throttler'
7
+ import type { Debouncer } from './debouncer'
8
+ import type { Batcher } from './batcher'
9
+ import type { Queuer } from './queuer'
10
+ import type { RateLimiter } from './rate-limiter'
11
+ import type { Throttler } from './throttler'
12
+
13
+ export interface PacerEventMap {
14
+ 'pacer:d-AsyncBatcher': AsyncBatcher<any>
15
+ 'pacer:d-AsyncDebouncer': AsyncDebouncer<any>
16
+ 'pacer:d-AsyncQueuer': AsyncQueuer<any>
17
+ 'pacer:d-AsyncRateLimiter': AsyncRateLimiter<any>
18
+ 'pacer:d-AsyncThrottler': AsyncThrottler<any>
19
+ 'pacer:d-Batcher': Batcher<any>
20
+ 'pacer:d-Debouncer': Debouncer<any>
21
+ 'pacer:d-Queuer': Queuer<any>
22
+ 'pacer:d-RateLimiter': RateLimiter<any>
23
+ 'pacer:d-Throttler': Throttler<any>
24
+ 'pacer:AsyncBatcher': AsyncBatcher<any>
25
+ 'pacer:AsyncDebouncer': AsyncDebouncer<any>
26
+ 'pacer:AsyncQueuer': AsyncQueuer<any>
27
+ 'pacer:AsyncRateLimiter': AsyncRateLimiter<any>
28
+ 'pacer:AsyncThrottler': AsyncThrottler<any>
29
+ 'pacer:Batcher': Batcher<any>
30
+ 'pacer:Debouncer': Debouncer<any>
31
+ 'pacer:Queuer': Queuer<any>
32
+ 'pacer:RateLimiter': RateLimiter<any>
33
+ 'pacer:Throttler': Throttler<any>
34
+ }
35
+
36
+ export type PacerEventName = keyof PacerEventMap extends `pacer:${infer T}`
37
+ ? T
38
+ : never
39
+
40
+ class PacerEventClient extends EventClient<PacerEventMap> {
41
+ constructor(props?: { debug: boolean }) {
42
+ super({
43
+ pluginId: 'pacer',
44
+ debug: props?.debug,
45
+ })
46
+ }
47
+ }
48
+
49
+ export const emitChange = <
50
+ TSuffix extends Extract<
51
+ keyof PacerEventMap,
52
+ `${string}:${string}`
53
+ > extends `${string}:${infer S}`
54
+ ? S
55
+ : never,
56
+ >(
57
+ event: TSuffix,
58
+ payload: PacerEventMap[`pacer:${TSuffix}`],
59
+ ) => {
60
+ pacerEventClient.emit(event, payload)
61
+ }
62
+
63
+ export const pacerEventClient = new PacerEventClient()
package/src/index.ts CHANGED
@@ -10,3 +10,5 @@ export * from './rate-limiter'
10
10
  export * from './throttler'
11
11
  export * from './types'
12
12
  export * from './utils'
13
+ export { pacerEventClient } from './event-client'
14
+ export type { PacerEventMap, PacerEventName } from './event-client'