@tanstack/pacer 0.8.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.
- package/dist/cjs/async-batcher.cjs +163 -0
- package/dist/cjs/async-batcher.cjs.map +1 -0
- package/dist/cjs/async-batcher.d.cts +273 -0
- package/dist/cjs/async-debouncer.cjs +149 -162
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +76 -57
- package/dist/cjs/async-queuer.cjs +282 -343
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +121 -100
- package/dist/cjs/async-rate-limiter.cjs +128 -185
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +72 -61
- package/dist/cjs/async-throttler.cjs +168 -178
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +97 -69
- package/dist/cjs/batcher.cjs +110 -119
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +76 -51
- package/dist/cjs/debouncer.cjs +97 -85
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +54 -26
- package/dist/cjs/index.cjs +3 -6
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/queuer.cjs +246 -294
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +102 -81
- package/dist/cjs/rate-limiter.cjs +97 -130
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +50 -37
- package/dist/cjs/throttler.cjs +107 -123
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +59 -35
- package/dist/cjs/utils.cjs +0 -13
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +0 -1
- package/dist/esm/async-batcher.d.ts +273 -0
- package/dist/esm/async-batcher.js +163 -0
- package/dist/esm/async-batcher.js.map +1 -0
- package/dist/esm/async-debouncer.d.ts +76 -57
- package/dist/esm/async-debouncer.js +149 -162
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +121 -100
- package/dist/esm/async-queuer.js +282 -343
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +72 -61
- package/dist/esm/async-rate-limiter.js +128 -185
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +97 -69
- package/dist/esm/async-throttler.js +168 -178
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +76 -51
- package/dist/esm/batcher.js +110 -119
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/debouncer.d.ts +54 -26
- package/dist/esm/debouncer.js +97 -85
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +4 -7
- package/dist/esm/queuer.d.ts +102 -81
- package/dist/esm/queuer.js +246 -294
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +50 -37
- package/dist/esm/rate-limiter.js +97 -130
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +59 -35
- package/dist/esm/throttler.js +107 -123
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.d.ts +0 -1
- package/dist/esm/utils.js +0 -13
- package/dist/esm/utils.js.map +1 -1
- package/package.json +14 -11
- package/src/async-batcher.ts +475 -0
- package/src/async-debouncer.ts +201 -121
- package/src/async-queuer.ts +337 -216
- package/src/async-rate-limiter.ts +176 -136
- package/src/async-throttler.ts +233 -139
- package/src/batcher.ts +158 -92
- package/src/debouncer.ts +135 -52
- package/src/index.ts +1 -1
- package/src/queuer.ts +348 -226
- package/src/rate-limiter.ts +125 -80
- package/src/throttler.ts +152 -78
- package/src/utils.ts +0 -15
- package/dist/cjs/compare.cjs +0 -72
- package/dist/cjs/compare.cjs.map +0 -1
- package/dist/cjs/compare.d.cts +0 -12
- package/dist/esm/compare.d.ts +0 -12
- package/dist/esm/compare.js +0 -72
- package/dist/esm/compare.js.map +0 -1
- 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
|
-
'
|
|
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
|
-
*
|
|
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.
|
|
140
|
+
* // batcher.flush() // manually trigger a batch
|
|
82
141
|
* ```
|
|
83
142
|
*/
|
|
84
143
|
export class Batcher<TValue> {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
97
|
-
|
|
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.
|
|
164
|
+
setOptions = (newOptions: Partial<BatcherOptions<TValue>>): void => {
|
|
165
|
+
this.options = { ...this.options, ...newOptions }
|
|
105
166
|
}
|
|
106
167
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
120
|
-
|
|
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.
|
|
124
|
-
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
|
|
128
|
-
} else if (
|
|
129
|
-
this
|
|
130
|
-
|
|
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.
|
|
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
227
|
const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
*
|
|
240
|
+
* Processes the current batch of items immediately
|
|
168
241
|
*/
|
|
169
|
-
|
|
170
|
-
this
|
|
171
|
-
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
|
-
*
|
|
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
|
-
|
|
193
|
-
|
|
250
|
+
stop = (): void => {
|
|
251
|
+
this.#setState({ isRunning: false })
|
|
252
|
+
this.#clearTimeout()
|
|
194
253
|
}
|
|
195
254
|
|
|
196
255
|
/**
|
|
197
|
-
*
|
|
256
|
+
* Starts the batcher and processes any pending items
|
|
198
257
|
*/
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
266
|
+
* Returns a copy of all items in the batcher
|
|
205
267
|
*/
|
|
206
|
-
|
|
207
|
-
return this.
|
|
268
|
+
peekAllItems = (): Array<TValue> => {
|
|
269
|
+
return [...this.store.state.items]
|
|
208
270
|
}
|
|
209
271
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
272
|
+
#clearTimeout = (): void => {
|
|
273
|
+
if (this.#timeoutId) {
|
|
274
|
+
clearTimeout(this.#timeoutId)
|
|
275
|
+
this.#timeoutId = null
|
|
276
|
+
}
|
|
215
277
|
}
|
|
216
278
|
|
|
217
279
|
/**
|
|
218
|
-
*
|
|
280
|
+
* Removes all items from the batcher
|
|
219
281
|
*/
|
|
220
|
-
|
|
221
|
-
|
|
282
|
+
clear = (): void => {
|
|
283
|
+
this.#setState({ items: [], isPending: false })
|
|
222
284
|
}
|
|
223
285
|
|
|
224
286
|
/**
|
|
225
|
-
*
|
|
287
|
+
* Resets the batcher state to its default values
|
|
226
288
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
*
|
|
239
|
-
*
|
|
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
|
|
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:
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
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.
|
|
139
|
+
setOptions = (newOptions: Partial<DebouncerOptions<TFn>>): void => {
|
|
140
|
+
this.options = { ...this.options, ...newOptions }
|
|
90
141
|
|
|
91
|
-
//
|
|
92
|
-
if (!this
|
|
93
|
-
this.
|
|
142
|
+
// Cancel pending execution if the debouncer is disabled
|
|
143
|
+
if (!this.#getEnabled()) {
|
|
144
|
+
this.cancel()
|
|
94
145
|
}
|
|
95
146
|
}
|
|
96
147
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
127
|
-
this
|
|
189
|
+
if (this.options.leading && this.store.state.canLeadingExecute) {
|
|
190
|
+
this.#setState({ canLeadingExecute: false })
|
|
128
191
|
_didLeadingExecute = true
|
|
129
|
-
this
|
|
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.
|
|
134
|
-
this
|
|
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
|
|
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
|
|
142
|
-
this
|
|
143
|
-
if (this.
|
|
144
|
-
this
|
|
204
|
+
this.#timeoutId = setTimeout(() => {
|
|
205
|
+
this.#setState({ canLeadingExecute: true })
|
|
206
|
+
if (this.options.trailing && !_didLeadingExecute) {
|
|
207
|
+
this.#execute(...args)
|
|
145
208
|
}
|
|
146
|
-
}, this
|
|
209
|
+
}, this.#getWait())
|
|
147
210
|
}
|
|
148
211
|
|
|
149
|
-
|
|
150
|
-
if (!this
|
|
212
|
+
#execute = (...args: Parameters<TFn>): void => {
|
|
213
|
+
if (!this.#getEnabled()) return undefined
|
|
151
214
|
this.fn(...args) // EXECUTE!
|
|
152
|
-
this
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
*
|
|
223
|
+
* Processes the current pending execution immediately
|
|
159
224
|
*/
|
|
160
|
-
|
|
161
|
-
if (this.
|
|
162
|
-
clearTimeout(
|
|
163
|
-
this.
|
|
164
|
-
|
|
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
|
-
*
|
|
240
|
+
* Cancels any pending execution
|
|
170
241
|
*/
|
|
171
|
-
|
|
172
|
-
|
|
242
|
+
cancel = (): void => {
|
|
243
|
+
this.#clearTimeout()
|
|
244
|
+
this.#setState({
|
|
245
|
+
canLeadingExecute: true,
|
|
246
|
+
isPending: false,
|
|
247
|
+
})
|
|
173
248
|
}
|
|
174
249
|
|
|
175
250
|
/**
|
|
176
|
-
*
|
|
251
|
+
* Resets the debouncer state to its default values
|
|
177
252
|
*/
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
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'
|