@tanstack/pacer 0.14.0 → 0.15.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 (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 +7 -1
  4. package/dist/cjs/async-debouncer.cjs +15 -3
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +10 -0
  7. package/dist/cjs/async-queuer.cjs +13 -1
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +10 -0
  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 +10 -0
  13. package/dist/cjs/async-throttler.cjs +13 -1
  14. package/dist/cjs/async-throttler.cjs.map +1 -1
  15. package/dist/cjs/async-throttler.d.cts +10 -0
  16. package/dist/cjs/batcher.cjs +8 -0
  17. package/dist/cjs/batcher.cjs.map +1 -1
  18. package/dist/cjs/batcher.d.cts +7 -1
  19. package/dist/cjs/debouncer.cjs +13 -1
  20. package/dist/cjs/debouncer.cjs.map +1 -1
  21. package/dist/cjs/debouncer.d.cts +10 -0
  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 +14 -2
  29. package/dist/cjs/queuer.cjs.map +1 -1
  30. package/dist/cjs/queuer.d.cts +10 -0
  31. package/dist/cjs/rate-limiter.cjs +13 -1
  32. package/dist/cjs/rate-limiter.cjs.map +1 -1
  33. package/dist/cjs/rate-limiter.d.cts +10 -0
  34. package/dist/cjs/throttler.cjs +13 -1
  35. package/dist/cjs/throttler.cjs.map +1 -1
  36. package/dist/cjs/throttler.d.cts +10 -0
  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 +7 -1
  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 +10 -0
  44. package/dist/esm/async-debouncer.js +16 -4
  45. package/dist/esm/async-debouncer.js.map +1 -1
  46. package/dist/esm/async-queuer.d.ts +10 -0
  47. package/dist/esm/async-queuer.js +14 -2
  48. package/dist/esm/async-queuer.js.map +1 -1
  49. package/dist/esm/async-rate-limiter.d.ts +10 -0
  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 +10 -0
  53. package/dist/esm/async-throttler.js +14 -2
  54. package/dist/esm/async-throttler.js.map +1 -1
  55. package/dist/esm/batcher.d.ts +7 -1
  56. package/dist/esm/batcher.js +9 -1
  57. package/dist/esm/batcher.js.map +1 -1
  58. package/dist/esm/debouncer.d.ts +10 -0
  59. package/dist/esm/debouncer.js +14 -2
  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 +10 -0
  68. package/dist/esm/queuer.js +15 -3
  69. package/dist/esm/queuer.js.map +1 -1
  70. package/dist/esm/rate-limiter.d.ts +10 -0
  71. package/dist/esm/rate-limiter.js +14 -2
  72. package/dist/esm/rate-limiter.js.map +1 -1
  73. package/dist/esm/throttler.d.ts +10 -0
  74. package/dist/esm/throttler.js +14 -2
  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 +22 -2
  81. package/src/async-debouncer.ts +27 -4
  82. package/src/async-queuer.ts +32 -2
  83. package/src/async-rate-limiter.ts +27 -3
  84. package/src/async-throttler.ts +24 -2
  85. package/src/batcher.ts +17 -2
  86. package/src/debouncer.ts +27 -2
  87. package/src/event-client.ts +63 -0
  88. package/src/index.ts +2 -0
  89. package/src/queuer.ts +31 -2
  90. package/src/rate-limiter.ts +26 -2
  91. package/src/throttler.ts +26 -2
  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 } 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,6 +217,7 @@ 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
 
@@ -213,6 +225,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
213
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
  */
@@ -55,6 +60,7 @@ function getDefaultAsyncThrottlerState<
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',
@@ -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
@@ -200,12 +212,18 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
200
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,6 +145,7 @@ 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
 
@@ -146,11 +153,18 @@ export class Batcher<TValue> {
146
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
  */
@@ -34,6 +39,7 @@ function getDefaultDebouncerState<
34
39
  isPending: false,
35
40
  lastArgs: undefined,
36
41
  status: 'idle',
42
+ maybeExecuteCount: 0,
37
43
  }
38
44
  }
39
45
 
@@ -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,6 +130,7 @@ 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
 
@@ -126,11 +138,18 @@ export class Debouncer<TFn extends AnyFunction> {
126
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'
package/src/queuer.ts CHANGED
@@ -1,7 +1,12 @@
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
 
4
5
  export interface QueuerState<TValue> {
6
+ /**
7
+ * Number of times addItem has been called (for reduction calculations)
8
+ */
9
+ addItemCount: number
5
10
  /**
6
11
  * Number of items that have been processed by the queuer
7
12
  */
@@ -66,6 +71,7 @@ function getDefaultQueuerState<TValue>(): QueuerState<TValue> {
66
71
  rejectionCount: 0,
67
72
  size: 0,
68
73
  status: 'idle',
74
+ addItemCount: 0,
69
75
  }
70
76
  }
71
77
 
@@ -108,6 +114,11 @@ export interface QueuerOptions<TValue> {
108
114
  * Initial state for the queuer
109
115
  */
110
116
  initialState?: Partial<QueuerState<TValue>>
117
+ /**
118
+ * Optional key to identify this queuer instance.
119
+ * If provided, the queuer will be identified by this key in the devtools and PacerProvider if applicable.
120
+ */
121
+ key?: string
111
122
  /**
112
123
  * Maximum number of items allowed in the queuer
113
124
  */
@@ -148,6 +159,7 @@ const defaultOptions: Omit<
148
159
  | 'onItemsChange'
149
160
  | 'onReject'
150
161
  | 'onExpire'
162
+ | 'key'
151
163
  > = {
152
164
  addItemsTo: 'back',
153
165
  getItemsFrom: 'front',
@@ -244,6 +256,7 @@ export class Queuer<TValue> {
244
256
  readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(
245
257
  getDefaultQueuerState<TValue>(),
246
258
  )
259
+ key: string
247
260
  options: QueuerOptions<TValue>
248
261
  #timeoutId: NodeJS.Timeout | null = null
249
262
 
@@ -251,6 +264,7 @@ export class Queuer<TValue> {
251
264
  public fn: (item: TValue) => void,
252
265
  initialOptions: QueuerOptions<TValue> = {},
253
266
  ) {
267
+ this.key = createKey(initialOptions.key)
254
268
  this.options = {
255
269
  ...defaultOptions,
256
270
  ...initialOptions,
@@ -273,6 +287,11 @@ export class Queuer<TValue> {
273
287
  this.addItem(item, this.options.addItemsTo ?? 'back', isLast)
274
288
  }
275
289
  }
290
+ pacerEventClient.on('d-Queuer', (event) => {
291
+ if (event.payload.key !== this.key) return
292
+ this.#setState(event.payload.store.state)
293
+ this.setOptions(event.payload.options)
294
+ })
276
295
  }
277
296
 
278
297
  /**
@@ -307,6 +326,7 @@ export class Queuer<TValue> {
307
326
  status,
308
327
  }
309
328
  })
329
+ emitChange('Queuer', this)
310
330
  }
311
331
 
312
332
  /**
@@ -366,6 +386,10 @@ export class Queuer<TValue> {
366
386
  position: QueuePosition = this.options.addItemsTo ?? 'back',
367
387
  runOnItemsChange: boolean = true,
368
388
  ): boolean => {
389
+ this.#setState({
390
+ addItemCount: this.store.state.addItemCount + 1,
391
+ })
392
+
369
393
  if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
370
394
  this.#setState({
371
395
  rejectionCount: this.store.state.rejectionCount + 1,
@@ -447,7 +471,12 @@ export class Queuer<TValue> {
447
471
  const { items, itemTimestamps } = this.store.state
448
472
  let item: TValue | undefined
449
473
 
450
- if (position === 'front') {
474
+ // When priority function is provided, always get from front (highest priority)
475
+ // Priority takes precedence over FIFO/LIFO behavior
476
+ if (
477
+ this.options.getPriority !== defaultOptions.getPriority ||
478
+ position === 'front'
479
+ ) {
451
480
  item = items[0]
452
481
  if (item !== undefined) {
453
482
  this.#setState({
@@ -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 RateLimiterState {
@@ -15,6 +16,10 @@ export interface RateLimiterState {
15
16
  * Whether the rate limiter has exceeded the limit
16
17
  */
17
18
  isExceeded: boolean
19
+ /**
20
+ * Number of times maybeExecute has been called (for reduction calculations)
21
+ */
22
+ maybeExecuteCount: number
18
23
  /**
19
24
  * Number of function executions that have been rejected due to rate limiting
20
25
  */
@@ -32,6 +37,7 @@ function getDefaultRateLimiterState(): RateLimiterState {
32
37
  isExceeded: false,
33
38
  rejectionCount: 0,
34
39
  status: 'idle',
40
+ maybeExecuteCount: 0,
35
41
  }
36
42
  }
37
43
 
@@ -48,6 +54,11 @@ export interface RateLimiterOptions<TFn extends AnyFunction> {
48
54
  * Initial state for the rate limiter
49
55
  */
50
56
  initialState?: Partial<RateLimiterState>
57
+ /**
58
+ * Optional key to identify this rate limiter instance.
59
+ * If provided, the rate limiter will be identified by this key in the devtools and PacerProvider if applicable.
60
+ */
61
+ key?: string
51
62
  /**
52
63
  * Maximum number of executions allowed within the time window.
53
64
  * Can be a number or a callback function that receives the rate limiter instance and returns a number.
@@ -77,7 +88,7 @@ export interface RateLimiterOptions<TFn extends AnyFunction> {
77
88
 
78
89
  const defaultOptions: Omit<
79
90
  Required<RateLimiterOptions<any>>,
80
- 'initialState' | 'onExecute' | 'onReject'
91
+ 'initialState' | 'onExecute' | 'onReject' | 'key'
81
92
  > = {
82
93
  enabled: true,
83
94
  limit: 1,
@@ -132,6 +143,7 @@ const defaultOptions: Omit<
132
143
  export class RateLimiter<TFn extends AnyFunction> {
133
144
  readonly store: Store<Readonly<RateLimiterState>> =
134
145
  new Store<RateLimiterState>(getDefaultRateLimiterState())
146
+ key: string
135
147
  options: RateLimiterOptions<TFn>
136
148
  #timeoutIds: Set<NodeJS.Timeout> = new Set()
137
149
 
@@ -139,6 +151,7 @@ export class RateLimiter<TFn extends AnyFunction> {
139
151
  public fn: TFn,
140
152
  initialOptions: RateLimiterOptions<TFn>,
141
153
  ) {
154
+ this.key = createKey(initialOptions.key)
142
155
  this.options = {
143
156
  ...defaultOptions,
144
157
  ...initialOptions,
@@ -147,6 +160,12 @@ export class RateLimiter<TFn extends AnyFunction> {
147
160
  for (const executionTime of this.#getExecutionTimesInWindow()) {
148
161
  this.#setCleanupTimeout(executionTime)
149
162
  }
163
+
164
+ pacerEventClient.on('d-RateLimiter', (event) => {
165
+ if (event.payload.key !== this.key) return
166
+ this.#setState(event.payload.store.state)
167
+ this.setOptions(event.payload.options)
168
+ })
150
169
  }
151
170
 
152
171
  /**
@@ -174,6 +193,7 @@ export class RateLimiter<TFn extends AnyFunction> {
174
193
  status,
175
194
  }
176
195
  })
196
+ emitChange('RateLimiter', this)
177
197
  }
178
198
 
179
199
  /**
@@ -213,6 +233,10 @@ export class RateLimiter<TFn extends AnyFunction> {
213
233
  * ```
214
234
  */
215
235
  maybeExecute = (...args: Parameters<TFn>): boolean => {
236
+ this.#setState({
237
+ maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
238
+ })
239
+
216
240
  this.#cleanupOldExecutions()
217
241
 
218
242
  const relevantExecutionTimes = this.#getExecutionTimesInWindow()
package/src/throttler.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 ThrottlerState<TFn extends AnyFunction> {
@@ -19,6 +20,10 @@ export interface ThrottlerState<TFn extends AnyFunction> {
19
20
  * Timestamp of the last function execution in milliseconds
20
21
  */
21
22
  lastExecutionTime: number
23
+ /**
24
+ * Number of times maybeExecute has been called (for reduction calculations)
25
+ */
26
+ maybeExecuteCount: number
22
27
  /**
23
28
  * Timestamp when the next execution can occur in milliseconds
24
29
  */
@@ -39,6 +44,7 @@ function getDefaultThrottlerState<
39
44
  lastExecutionTime: 0,
40
45
  nextExecutionTime: 0,
41
46
  status: 'idle',
47
+ maybeExecuteCount: 0,
42
48
  }
43
49
  }
44
50
 
@@ -56,6 +62,11 @@ export interface ThrottlerOptions<TFn extends AnyFunction> {
56
62
  * Initial state for the throttler
57
63
  */
58
64
  initialState?: Partial<ThrottlerState<TFn>>
65
+ /**
66
+ * A key to identify the throttler.
67
+ * If provided, the throttler will be identified by this key in the devtools and PacerProvider if applicable.
68
+ */
69
+ key?: string
59
70
  /**
60
71
  * Whether to execute on the leading edge of the timeout.
61
72
  * Defaults to true.
@@ -80,7 +91,7 @@ export interface ThrottlerOptions<TFn extends AnyFunction> {
80
91
 
81
92
  const defaultOptions: Omit<
82
93
  Required<ThrottlerOptions<any>>,
83
- 'initialState' | 'onExecute'
94
+ 'initialState' | 'onExecute' | 'key'
84
95
  > = {
85
96
  enabled: true,
86
97
  leading: true,
@@ -127,6 +138,7 @@ export class Throttler<TFn extends AnyFunction> {
127
138
  readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(
128
139
  getDefaultThrottlerState(),
129
140
  )
141
+ key: string | undefined
130
142
  options: ThrottlerOptions<TFn>
131
143
  #timeoutId: NodeJS.Timeout | undefined
132
144
 
@@ -134,11 +146,18 @@ export class Throttler<TFn extends AnyFunction> {
134
146
  public fn: TFn,
135
147
  initialOptions: ThrottlerOptions<TFn>,
136
148
  ) {
149
+ this.key = createKey(initialOptions.key)
137
150
  this.options = {
138
151
  ...defaultOptions,
139
152
  ...initialOptions,
140
153
  }
141
154
  this.#setState(this.options.initialState ?? {})
155
+
156
+ pacerEventClient.on('d-Throttler', (event) => {
157
+ if (event.payload.key !== this.key) return
158
+ this.#setState(event.payload.store.state as ThrottlerState<TFn>)
159
+ this.setOptions(event.payload.options)
160
+ })
142
161
  }
143
162
 
144
163
  /**
@@ -169,6 +188,7 @@ export class Throttler<TFn extends AnyFunction> {
169
188
  : 'idle',
170
189
  }
171
190
  })
191
+ emitChange('Throttler', this)
172
192
  }
173
193
 
174
194
  #getEnabled = (): boolean => {
@@ -202,6 +222,10 @@ export class Throttler<TFn extends AnyFunction> {
202
222
  * ```
203
223
  */
204
224
  maybeExecute = (...args: Parameters<TFn>): void => {
225
+ this.#setState({
226
+ maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
227
+ })
228
+
205
229
  const now = Date.now()
206
230
  const timeSinceLastExecution = now - this.store.state.lastExecutionTime
207
231
  const wait = this.#getWait()