@tanstack/pacer 0.7.0 → 0.9.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 (91) hide show
  1. package/dist/cjs/async-batcher.cjs +163 -0
  2. package/dist/cjs/async-batcher.cjs.map +1 -0
  3. package/dist/cjs/async-batcher.d.cts +273 -0
  4. package/dist/cjs/async-debouncer.cjs +149 -162
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +76 -57
  7. package/dist/cjs/async-queuer.cjs +282 -343
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +123 -102
  10. package/dist/cjs/async-rate-limiter.cjs +128 -185
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +72 -61
  13. package/dist/cjs/async-throttler.cjs +168 -178
  14. package/dist/cjs/async-throttler.cjs.map +1 -1
  15. package/dist/cjs/async-throttler.d.cts +97 -69
  16. package/dist/cjs/batcher.cjs +110 -119
  17. package/dist/cjs/batcher.cjs.map +1 -1
  18. package/dist/cjs/batcher.d.cts +76 -51
  19. package/dist/cjs/debouncer.cjs +97 -85
  20. package/dist/cjs/debouncer.cjs.map +1 -1
  21. package/dist/cjs/debouncer.d.cts +54 -26
  22. package/dist/cjs/index.cjs +3 -6
  23. package/dist/cjs/index.cjs.map +1 -1
  24. package/dist/cjs/index.d.cts +1 -1
  25. package/dist/cjs/queuer.cjs +246 -294
  26. package/dist/cjs/queuer.cjs.map +1 -1
  27. package/dist/cjs/queuer.d.cts +105 -84
  28. package/dist/cjs/rate-limiter.cjs +97 -130
  29. package/dist/cjs/rate-limiter.cjs.map +1 -1
  30. package/dist/cjs/rate-limiter.d.cts +50 -37
  31. package/dist/cjs/throttler.cjs +107 -123
  32. package/dist/cjs/throttler.cjs.map +1 -1
  33. package/dist/cjs/throttler.d.cts +59 -35
  34. package/dist/cjs/utils.cjs +0 -13
  35. package/dist/cjs/utils.cjs.map +1 -1
  36. package/dist/cjs/utils.d.cts +0 -1
  37. package/dist/esm/async-batcher.d.ts +273 -0
  38. package/dist/esm/async-batcher.js +163 -0
  39. package/dist/esm/async-batcher.js.map +1 -0
  40. package/dist/esm/async-debouncer.d.ts +76 -57
  41. package/dist/esm/async-debouncer.js +149 -162
  42. package/dist/esm/async-debouncer.js.map +1 -1
  43. package/dist/esm/async-queuer.d.ts +123 -102
  44. package/dist/esm/async-queuer.js +282 -343
  45. package/dist/esm/async-queuer.js.map +1 -1
  46. package/dist/esm/async-rate-limiter.d.ts +72 -61
  47. package/dist/esm/async-rate-limiter.js +128 -185
  48. package/dist/esm/async-rate-limiter.js.map +1 -1
  49. package/dist/esm/async-throttler.d.ts +97 -69
  50. package/dist/esm/async-throttler.js +168 -178
  51. package/dist/esm/async-throttler.js.map +1 -1
  52. package/dist/esm/batcher.d.ts +76 -51
  53. package/dist/esm/batcher.js +110 -119
  54. package/dist/esm/batcher.js.map +1 -1
  55. package/dist/esm/debouncer.d.ts +54 -26
  56. package/dist/esm/debouncer.js +97 -85
  57. package/dist/esm/debouncer.js.map +1 -1
  58. package/dist/esm/index.d.ts +1 -1
  59. package/dist/esm/index.js +4 -7
  60. package/dist/esm/queuer.d.ts +105 -84
  61. package/dist/esm/queuer.js +246 -294
  62. package/dist/esm/queuer.js.map +1 -1
  63. package/dist/esm/rate-limiter.d.ts +50 -37
  64. package/dist/esm/rate-limiter.js +97 -130
  65. package/dist/esm/rate-limiter.js.map +1 -1
  66. package/dist/esm/throttler.d.ts +59 -35
  67. package/dist/esm/throttler.js +107 -123
  68. package/dist/esm/throttler.js.map +1 -1
  69. package/dist/esm/utils.d.ts +0 -1
  70. package/dist/esm/utils.js +0 -13
  71. package/dist/esm/utils.js.map +1 -1
  72. package/package.json +14 -11
  73. package/src/async-batcher.ts +475 -0
  74. package/src/async-debouncer.ts +201 -121
  75. package/src/async-queuer.ts +341 -220
  76. package/src/async-rate-limiter.ts +176 -136
  77. package/src/async-throttler.ts +233 -139
  78. package/src/batcher.ts +159 -93
  79. package/src/debouncer.ts +135 -52
  80. package/src/index.ts +1 -1
  81. package/src/queuer.ts +349 -227
  82. package/src/rate-limiter.ts +125 -80
  83. package/src/throttler.ts +152 -78
  84. package/src/utils.ts +0 -15
  85. package/dist/cjs/compare.cjs +0 -72
  86. package/dist/cjs/compare.cjs.map +0 -1
  87. package/dist/cjs/compare.d.cts +0 -12
  88. package/dist/esm/compare.d.ts +0 -12
  89. package/dist/esm/compare.js +0 -72
  90. package/dist/esm/compare.js.map +0 -1
  91. package/src/compare.ts +0 -105
package/src/batcher.ts CHANGED
@@ -1,5 +1,55 @@
1
+ import { Store } from '@tanstack/store'
2
+ import { parseFunctionOrValue } from './utils'
1
3
  import type { OptionalKeys } from './types'
2
4
 
5
+ export interface BatcherState<TValue> {
6
+ /**
7
+ * Number of batch executions that have been completed
8
+ */
9
+ executionCount: number
10
+ /**
11
+ * Whether the batcher has no items to process (items array is empty)
12
+ */
13
+ isEmpty: boolean
14
+ /**
15
+ * Whether the batcher is waiting for the timeout to trigger batch processing
16
+ */
17
+ isPending: boolean
18
+ /**
19
+ * Whether the batcher is active and will process items automatically
20
+ */
21
+ isRunning: boolean
22
+ /**
23
+ * Total number of items that have been processed across all batches
24
+ */
25
+ totalItemsProcessed: number
26
+ /**
27
+ * Array of items currently queued for batch processing
28
+ */
29
+ items: Array<TValue>
30
+ /**
31
+ * Number of items currently in the batch queue
32
+ */
33
+ size: number
34
+ /**
35
+ * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout
36
+ */
37
+ status: 'idle' | 'pending'
38
+ }
39
+
40
+ function getDefaultBatcherState<TValue>(): BatcherState<TValue> {
41
+ return {
42
+ executionCount: 0,
43
+ isEmpty: true,
44
+ isPending: false,
45
+ isRunning: true,
46
+ totalItemsProcessed: 0,
47
+ items: [],
48
+ size: 0,
49
+ status: 'idle',
50
+ }
51
+ }
52
+
3
53
  /**
4
54
  * Options for configuring a Batcher instance
5
55
  */
@@ -9,6 +59,10 @@ export interface BatcherOptions<TValue> {
9
59
  * Return true to process the batch immediately
10
60
  */
11
61
  getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean
62
+ /**
63
+ * Initial state for the batcher
64
+ */
65
+ initialState?: Partial<BatcherState<TValue>>
12
66
  /**
13
67
  * Maximum number of items in a batch
14
68
  * @default Infinity
@@ -18,10 +72,6 @@ export interface BatcherOptions<TValue> {
18
72
  * Callback fired after a batch is processed
19
73
  */
20
74
  onExecute?: (batcher: Batcher<TValue>) => void
21
- /**
22
- * Callback fired when the batcher's running state changes
23
- */
24
- onIsRunningChange?: (batcher: Batcher<TValue>) => void
25
75
  /**
26
76
  * Callback fired after items are added to the batcher
27
77
  */
@@ -37,12 +87,12 @@ export interface BatcherOptions<TValue> {
37
87
  * If not provided, the batch will not be triggered by a timeout.
38
88
  * @default Infinity
39
89
  */
40
- wait?: number
90
+ wait?: number | ((batcher: Batcher<TValue>) => number)
41
91
  }
42
92
 
43
93
  type BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<
44
94
  Required<BatcherOptions<TValue>>,
45
- 'onExecute' | 'onItemsChange' | 'onIsRunningChange'
95
+ 'initialState' | 'onExecute' | 'onItemsChange'
46
96
  >
47
97
 
48
98
  const defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {
@@ -63,6 +113,15 @@ const defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {
63
113
  * - Custom batch processing logic via getShouldExecute
64
114
  * - Event callbacks for monitoring batch operations
65
115
  *
116
+ * State Management:
117
+ * - Uses TanStack Store for reactive state management
118
+ * - Use `initialState` to provide initial state values when creating the batcher
119
+ * - Use `onExecute` callback to react to batch execution and implement custom logic
120
+ * - Use `onItemsChange` callback to react to items being added or removed from the batcher
121
+ * - The state includes batch execution count, total items processed, items, and running status
122
+ * - State can be accessed via `batcher.store.state` when using the class directly
123
+ * - When using framework adapters (React/Solid), state is accessed from `batcher.state`
124
+ *
66
125
  * @example
67
126
  * ```ts
68
127
  * const batcher = new Batcher<number>(
@@ -70,7 +129,7 @@ const defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {
70
129
  * {
71
130
  * maxSize: 5,
72
131
  * wait: 2000,
73
- * onExecuteBatch: (items) => console.log('Batch executed:', items)
132
+ * onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems())
74
133
  * }
75
134
  * );
76
135
  *
@@ -78,59 +137,76 @@ const defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {
78
137
  * batcher.addItem(2);
79
138
  * // After 2 seconds or when 5 items are added, whichever comes first,
80
139
  * // the batch will be processed
81
- * // batcher.execute() // manually trigger a batch
140
+ * // batcher.flush() // manually trigger a batch
82
141
  * ```
83
142
  */
84
143
  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
144
+ readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(
145
+ getDefaultBatcherState<TValue>(),
146
+ )
147
+ options: BatcherOptionsWithOptionalCallbacks<TValue>
148
+ #timeoutId: NodeJS.Timeout | null = null
91
149
 
92
150
  constructor(
93
151
  private fn: (items: Array<TValue>) => void,
94
152
  initialOptions: BatcherOptions<TValue>,
95
153
  ) {
96
- this._options = { ...defaultOptions, ...initialOptions }
97
- this._running = this._options.started
154
+ this.options = {
155
+ ...defaultOptions,
156
+ ...initialOptions,
157
+ }
158
+ this.#setState(this.options.initialState ?? {})
98
159
  }
99
160
 
100
161
  /**
101
162
  * Updates the batcher options
102
163
  */
103
- setOptions(newOptions: Partial<BatcherOptions<TValue>>): void {
104
- this._options = { ...this._options, ...newOptions }
164
+ setOptions = (newOptions: Partial<BatcherOptions<TValue>>): void => {
165
+ this.options = { ...this.options, ...newOptions }
105
166
  }
106
167
 
107
- /**
108
- * Returns the current batcher options
109
- */
110
- getOptions(): BatcherOptions<TValue> {
111
- return this._options
168
+ #setState = (newState: Partial<BatcherState<TValue>>): void => {
169
+ this.store.setState((state) => {
170
+ const combinedState = {
171
+ ...state,
172
+ ...newState,
173
+ }
174
+ const { isPending, items } = combinedState
175
+ const size = items.length
176
+ const isEmpty = size === 0
177
+ return {
178
+ ...combinedState,
179
+ isEmpty,
180
+ size,
181
+ status: isPending ? 'pending' : 'idle',
182
+ }
183
+ })
184
+ }
185
+
186
+ #getWait = (): number => {
187
+ return parseFunctionOrValue(this.options.wait, this)
112
188
  }
113
189
 
114
190
  /**
115
191
  * Adds an item to the batcher
116
192
  * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
117
193
  */
118
- addItem(item: TValue): void {
119
- this._items.push(item)
120
- this._options.onItemsChange?.(this)
194
+ addItem = (item: TValue): void => {
195
+ this.#setState({
196
+ items: [...this.store.state.items, item],
197
+ isPending: this.options.wait !== Infinity,
198
+ })
199
+ this.options.onItemsChange?.(this)
121
200
 
122
201
  const shouldProcess =
123
- this._items.length >= this._options.maxSize ||
124
- this._options.getShouldExecute(this._items, this)
202
+ this.store.state.items.length >= this.options.maxSize ||
203
+ this.options.getShouldExecute(this.store.state.items, this)
125
204
 
126
205
  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)
206
+ this.#execute()
207
+ } else if (this.store.state.isRunning && this.options.wait !== Infinity) {
208
+ this.#clearTimeout() // clear any pending timeout to replace it with a new one
209
+ this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
134
210
  }
135
211
  }
136
212
 
@@ -143,89 +219,76 @@ export class Batcher<TValue> {
143
219
  *
144
220
  * You can also call this method manually to process the current batch at any time.
145
221
  */
146
- execute(): void {
147
- if (this._timeoutId) {
148
- clearTimeout(this._timeoutId)
149
- this._timeoutId = null
150
- }
151
-
152
- if (this._items.length === 0) {
222
+ #execute = (): void => {
223
+ if (this.store.state.items.length === 0) {
153
224
  return
154
225
  }
155
226
 
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
227
+ const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)
228
+ this.clear() // Clear items before processing to prevent race conditions
229
+ this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed
159
230
 
160
- this.fn(batch)
161
- this._batchExecutionCount++
162
- this._itemExecutionCount += batch.length
163
- this._options.onExecute?.(this)
231
+ this.fn(batch) // EXECUTE
232
+ this.#setState({
233
+ executionCount: this.store.state.executionCount + 1,
234
+ totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,
235
+ })
236
+ this.options.onExecute?.(this)
164
237
  }
165
238
 
166
239
  /**
167
- * Stops the batcher from processing batches
240
+ * Processes the current batch of items immediately
168
241
  */
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
- }
242
+ flush = (): void => {
243
+ this.#clearTimeout() // clear any pending timeout
244
+ this.#execute() // execute immediately
176
245
  }
177
246
 
178
247
  /**
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
248
+ * Stops the batcher from processing batches
191
249
  */
192
- getSize(): number {
193
- return this._items.length
250
+ stop = (): void => {
251
+ this.#setState({ isRunning: false })
252
+ this.#clearTimeout()
194
253
  }
195
254
 
196
255
  /**
197
- * Returns true if the batcher is empty
256
+ * Starts the batcher and processes any pending items
198
257
  */
199
- getIsEmpty(): boolean {
200
- return this._items.length === 0
258
+ start = (): void => {
259
+ this.#setState({ isRunning: true })
260
+ if (this.store.state.items.length > 0) {
261
+ this.#execute()
262
+ }
201
263
  }
202
264
 
203
265
  /**
204
- * Returns true if the batcher is running
266
+ * Returns a copy of all items in the batcher
205
267
  */
206
- getIsRunning(): boolean {
207
- return this._running
268
+ peekAllItems = (): Array<TValue> => {
269
+ return [...this.store.state.items]
208
270
  }
209
271
 
210
- /**
211
- * Returns a copy of all items currently in the batcher
212
- */
213
- getAllItems(): Array<TValue> {
214
- return [...this._items]
272
+ #clearTimeout = (): void => {
273
+ if (this.#timeoutId) {
274
+ clearTimeout(this.#timeoutId)
275
+ this.#timeoutId = null
276
+ }
215
277
  }
216
278
 
217
279
  /**
218
- * Returns the number of times batches have been processed
280
+ * Removes all items from the batcher
219
281
  */
220
- getBatchExecutionCount(): number {
221
- return this._batchExecutionCount
282
+ clear = (): void => {
283
+ this.#setState({ items: [], isPending: false })
222
284
  }
223
285
 
224
286
  /**
225
- * Returns the total number of individual items that have been processed
287
+ * Resets the batcher state to its default values
226
288
  */
227
- getItemExecutionCount(): number {
228
- return this._itemExecutionCount
289
+ reset = (): void => {
290
+ this.#setState(getDefaultBatcherState<TValue>())
291
+ this.options.onItemsChange?.(this)
229
292
  }
230
293
  }
231
294
 
@@ -234,10 +297,13 @@ export class Batcher<TValue> {
234
297
  *
235
298
  * @example
236
299
  * ```ts
237
- * const batchItems = batch<number>({
238
- * batchSize: 3,
239
- * processBatch: (items) => console.log('Processing:', items)
240
- * });
300
+ * const batchItems = batch<number>(
301
+ * (items) => console.log('Processing:', items),
302
+ * {
303
+ * maxSize: 3,
304
+ * onExecute: (batcher) => console.log('Batch executed')
305
+ * }
306
+ * );
241
307
  *
242
308
  * batchItems(1);
243
309
  * batchItems(2);
@@ -249,5 +315,5 @@ export function batch<TValue>(
249
315
  options: BatcherOptions<TValue>,
250
316
  ) {
251
317
  const batcher = new Batcher<TValue>(fn, options)
252
- return batcher.addItem.bind(batcher)
318
+ return batcher.addItem
253
319
  }
package/src/debouncer.ts CHANGED
@@ -1,6 +1,42 @@
1
+ import { Store } from '@tanstack/store'
1
2
  import { parseFunctionOrValue } from './utils'
2
3
  import type { AnyFunction } from './types'
3
4
 
5
+ export interface DebouncerState<TFn extends AnyFunction> {
6
+ /**
7
+ * Whether the debouncer can execute on the leading edge of the timeout
8
+ */
9
+ canLeadingExecute: boolean
10
+ /**
11
+ * Number of function executions that have been completed
12
+ */
13
+ executionCount: number
14
+ /**
15
+ * Whether the debouncer is waiting for the timeout to trigger execution
16
+ */
17
+ isPending: boolean
18
+ /**
19
+ * The arguments from the most recent call to maybeExecute
20
+ */
21
+ lastArgs: Parameters<TFn> | undefined
22
+ /**
23
+ * Current execution status - 'idle' when not active, 'pending' when waiting for timeout
24
+ */
25
+ status: 'disabled' | 'idle' | 'pending'
26
+ }
27
+
28
+ function getDefaultDebouncerState<
29
+ TFn extends AnyFunction,
30
+ >(): DebouncerState<TFn> {
31
+ return structuredClone({
32
+ canLeadingExecute: true,
33
+ executionCount: 0,
34
+ isPending: false,
35
+ lastArgs: undefined,
36
+ status: 'idle',
37
+ })
38
+ }
39
+
4
40
  /**
5
41
  * Options for configuring a debounced function
6
42
  */
@@ -11,6 +47,10 @@ export interface DebouncerOptions<TFn extends AnyFunction> {
11
47
  * Defaults to true.
12
48
  */
13
49
  enabled?: boolean | ((debouncer: Debouncer<TFn>) => boolean)
50
+ /**
51
+ * Initial state for the debouncer
52
+ */
53
+ initialState?: Partial<DebouncerState<TFn>>
14
54
  /**
15
55
  * Whether to execute on the leading edge of the timeout.
16
56
  * The first call will execute immediately and the rest will wait the delay.
@@ -34,10 +74,12 @@ export interface DebouncerOptions<TFn extends AnyFunction> {
34
74
  wait: number | ((debouncer: Debouncer<TFn>) => number)
35
75
  }
36
76
 
37
- const defaultOptions: Required<DebouncerOptions<any>> = {
77
+ const defaultOptions: Omit<
78
+ Required<DebouncerOptions<any>>,
79
+ 'initialState' | 'onExecute'
80
+ > = {
38
81
  enabled: true,
39
82
  leading: false,
40
- onExecute: () => {},
41
83
  trailing: true,
42
84
  wait: 0,
43
85
  }
@@ -53,6 +95,14 @@ const defaultOptions: Required<DebouncerOptions<any>> = {
53
95
  * (leading edge) or at the end (trailing edge, default). Each new call during the wait period
54
96
  * will reset the timer.
55
97
  *
98
+ * State Management:
99
+ * - Uses TanStack Store for reactive state management
100
+ * - Use `initialState` to provide initial state values when creating the debouncer
101
+ * - Use `onExecute` callback to react to function execution and implement custom logic
102
+ * - The state includes canLeadingExecute, execution count, and isPending status
103
+ * - State can be accessed via `debouncer.store.state` when using the class directly
104
+ * - When using framework adapters (React/Solid), state is accessed from `debouncer.state`
105
+ *
56
106
  * @example
57
107
  * ```ts
58
108
  * const debouncer = new Debouncer((value: string) => {
@@ -66,117 +116,142 @@ const defaultOptions: Required<DebouncerOptions<any>> = {
66
116
  * ```
67
117
  */
68
118
  export class Debouncer<TFn extends AnyFunction> {
69
- private _canLeadingExecute = true
70
- private _executionCount = 0
71
- private _isPending = false
72
- private _options: Required<DebouncerOptions<TFn>>
73
- private _timeoutId: NodeJS.Timeout | undefined
119
+ readonly store: Store<Readonly<DebouncerState<TFn>>> = new Store(
120
+ getDefaultDebouncerState<TFn>(),
121
+ )
122
+ options: DebouncerOptions<TFn>
123
+ #timeoutId: NodeJS.Timeout | undefined
74
124
 
75
125
  constructor(
76
126
  private fn: TFn,
77
127
  initialOptions: DebouncerOptions<TFn>,
78
128
  ) {
79
- this._options = {
129
+ this.options = {
80
130
  ...defaultOptions,
81
131
  ...initialOptions,
82
132
  }
133
+ this.#setState(this.options.initialState ?? {})
83
134
  }
84
135
 
85
136
  /**
86
137
  * Updates the debouncer options
87
138
  */
88
- setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void {
89
- this._options = { ...this._options, ...newOptions }
139
+ setOptions = (newOptions: Partial<DebouncerOptions<TFn>>): void => {
140
+ this.options = { ...this.options, ...newOptions }
90
141
 
91
- // End the pending state if the debouncer is disabled
92
- if (!this._options.enabled) {
93
- this._isPending = false
142
+ // Cancel pending execution if the debouncer is disabled
143
+ if (!this.#getEnabled()) {
144
+ this.cancel()
94
145
  }
95
146
  }
96
147
 
97
- /**
98
- * Returns the current debouncer options
99
- */
100
- getOptions(): Required<DebouncerOptions<TFn>> {
101
- return this._options
148
+ #setState = (newState: Partial<DebouncerState<TFn>>): void => {
149
+ this.store.setState((state) => {
150
+ const combinedState = {
151
+ ...state,
152
+ ...newState,
153
+ }
154
+ const { isPending } = combinedState
155
+ return {
156
+ ...combinedState,
157
+ status: !this.#getEnabled()
158
+ ? 'disabled'
159
+ : isPending
160
+ ? 'pending'
161
+ : 'idle',
162
+ }
163
+ })
102
164
  }
103
165
 
104
166
  /**
105
167
  * Returns the current enabled state of the debouncer
106
168
  */
107
- getEnabled(): boolean {
108
- return parseFunctionOrValue(this._options.enabled, this)
169
+ #getEnabled = (): boolean => {
170
+ return !!parseFunctionOrValue(this.options.enabled, this)
109
171
  }
110
172
 
111
173
  /**
112
174
  * Returns the current wait time in milliseconds
113
175
  */
114
- getWait(): number {
115
- return parseFunctionOrValue(this._options.wait, this)
176
+ #getWait = (): number => {
177
+ return parseFunctionOrValue(this.options.wait, this)
116
178
  }
117
179
 
118
180
  /**
119
181
  * Attempts to execute the debounced function
120
182
  * If a call is already in progress, it will be queued
121
183
  */
122
- maybeExecute(...args: Parameters<TFn>): void {
184
+ maybeExecute = (...args: Parameters<TFn>): void => {
185
+ if (!this.#getEnabled()) return undefined
123
186
  let _didLeadingExecute = false
124
187
 
125
188
  // Handle leading execution
126
- if (this._options.leading && this._canLeadingExecute) {
127
- this._canLeadingExecute = false
189
+ if (this.options.leading && this.store.state.canLeadingExecute) {
190
+ this.#setState({ canLeadingExecute: false })
128
191
  _didLeadingExecute = true
129
- this.execute(...args)
192
+ this.#execute(...args)
130
193
  }
131
194
 
132
195
  // Start pending state to indicate that the debouncer is waiting for the trailing edge
133
- if (this._options.trailing) {
134
- this._isPending = true
196
+ if (this.options.trailing) {
197
+ this.#setState({ isPending: true, lastArgs: args })
135
198
  }
136
199
 
137
200
  // Clear any existing timeout
138
- if (this._timeoutId) clearTimeout(this._timeoutId)
201
+ if (this.#timeoutId) clearTimeout(this.#timeoutId)
139
202
 
140
203
  // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading
141
- this._timeoutId = setTimeout(() => {
142
- this._canLeadingExecute = true
143
- if (this._options.trailing && !_didLeadingExecute) {
144
- this.execute(...args)
204
+ this.#timeoutId = setTimeout(() => {
205
+ this.#setState({ canLeadingExecute: true })
206
+ if (this.options.trailing && !_didLeadingExecute) {
207
+ this.#execute(...args)
145
208
  }
146
- }, this.getWait())
209
+ }, this.#getWait())
147
210
  }
148
211
 
149
- private execute(...args: Parameters<TFn>): void {
150
- if (!this.getEnabled()) return undefined
212
+ #execute = (...args: Parameters<TFn>): void => {
213
+ if (!this.#getEnabled()) return undefined
151
214
  this.fn(...args) // EXECUTE!
152
- this._isPending = false
153
- this._executionCount++
154
- this._options.onExecute(this)
215
+ this.#setState({
216
+ isPending: false,
217
+ executionCount: this.store.state.executionCount + 1,
218
+ })
219
+ this.options.onExecute?.(this)
155
220
  }
156
221
 
157
222
  /**
158
- * Cancels any pending execution
223
+ * Processes the current pending execution immediately
159
224
  */
160
- cancel(): void {
161
- if (this._timeoutId) {
162
- clearTimeout(this._timeoutId)
163
- this._canLeadingExecute = true
164
- this._isPending = false
225
+ flush = (): void => {
226
+ if (this.store.state.isPending && this.store.state.lastArgs) {
227
+ this.#clearTimeout() // clear any pending timeout
228
+ this.#execute(...this.store.state.lastArgs) // execute immediately
229
+ }
230
+ }
231
+
232
+ #clearTimeout = (): void => {
233
+ if (this.#timeoutId) {
234
+ clearTimeout(this.#timeoutId)
235
+ this.#timeoutId = undefined
165
236
  }
166
237
  }
167
238
 
168
239
  /**
169
- * Returns the number of times the function has been executed
240
+ * Cancels any pending execution
170
241
  */
171
- getExecutionCount(): number {
172
- return this._executionCount
242
+ cancel = (): void => {
243
+ this.#clearTimeout()
244
+ this.#setState({
245
+ canLeadingExecute: true,
246
+ isPending: false,
247
+ })
173
248
  }
174
249
 
175
250
  /**
176
- * Returns `true` if debouncing
251
+ * Resets the debouncer state to its default values
177
252
  */
178
- getIsPending(): boolean {
179
- return this.getEnabled() && this._isPending
253
+ reset = (): void => {
254
+ this.#setState(getDefaultDebouncerState<TFn>())
180
255
  }
181
256
  }
182
257
 
@@ -190,6 +265,14 @@ export class Debouncer<TFn extends AnyFunction> {
190
265
  * If leading option is true, the function will execute immediately on the first call, then wait the delay
191
266
  * before allowing another execution.
192
267
  *
268
+ * State Management:
269
+ * - Uses TanStack Store for reactive state management
270
+ * - Use `initialState` to provide initial state values when creating the debouncer
271
+ * - Use `onExecute` callback to react to function execution and implement custom logic
272
+ * - The state includes canLeadingExecute, execution count, and isPending status
273
+ * - State can be accessed via the underlying Debouncer instance's `store.state` property
274
+ * - When using framework adapters (React/Solid), state is accessed from the hook's state property
275
+ *
193
276
  * @example
194
277
  * ```ts
195
278
  * const debounced = debounce(() => {
@@ -205,5 +288,5 @@ export function debounce<TFn extends AnyFunction>(
205
288
  initialOptions: DebouncerOptions<TFn>,
206
289
  ): (...args: Parameters<TFn>) => void {
207
290
  const debouncer = new Debouncer(fn, initialOptions)
208
- return debouncer.maybeExecute.bind(debouncer)
291
+ return debouncer.maybeExecute
209
292
  }
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
+ export * from './async-batcher'
1
2
  export * from './async-debouncer'
2
3
  export * from './async-queuer'
3
4
  export * from './async-rate-limiter'
4
5
  export * from './async-throttler'
5
6
  export * from './batcher'
6
- export * from './compare'
7
7
  export * from './debouncer'
8
8
  export * from './queuer'
9
9
  export * from './rate-limiter'