@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.
- 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 +123 -102
- 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 +105 -84
- 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 +123 -102
- 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 +105 -84
- 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 +341 -220
- package/src/async-rate-limiter.ts +176 -136
- package/src/async-throttler.ts +233 -139
- package/src/batcher.ts +159 -93
- package/src/debouncer.ts +135 -52
- package/src/index.ts +1 -1
- package/src/queuer.ts +349 -227
- 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/rate-limiter.ts
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
|
+
import { Store } from '@tanstack/store'
|
|
1
2
|
import { parseFunctionOrValue } from './utils'
|
|
2
3
|
import type { AnyFunction } from './types'
|
|
3
4
|
|
|
5
|
+
export interface RateLimiterState {
|
|
6
|
+
/**
|
|
7
|
+
* Number of function executions that have been completed
|
|
8
|
+
*/
|
|
9
|
+
executionCount: number
|
|
10
|
+
/**
|
|
11
|
+
* Array of timestamps when executions occurred for rate limiting calculations
|
|
12
|
+
*/
|
|
13
|
+
executionTimes: Array<number>
|
|
14
|
+
/**
|
|
15
|
+
* Number of function executions that have been rejected due to rate limiting
|
|
16
|
+
*/
|
|
17
|
+
rejectionCount: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDefaultRateLimiterState(): RateLimiterState {
|
|
21
|
+
return structuredClone({
|
|
22
|
+
executionCount: 0,
|
|
23
|
+
executionTimes: [],
|
|
24
|
+
rejectionCount: 0,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
4
28
|
/**
|
|
5
29
|
* Options for configuring a rate-limited function
|
|
6
30
|
*/
|
|
@@ -10,6 +34,10 @@ export interface RateLimiterOptions<TFn extends AnyFunction> {
|
|
|
10
34
|
* Defaults to true.
|
|
11
35
|
*/
|
|
12
36
|
enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)
|
|
37
|
+
/**
|
|
38
|
+
* Initial state for the rate limiter
|
|
39
|
+
*/
|
|
40
|
+
initialState?: Partial<RateLimiterState>
|
|
13
41
|
/**
|
|
14
42
|
* Maximum number of executions allowed within the time window.
|
|
15
43
|
* Can be a number or a callback function that receives the rate limiter instance and returns a number.
|
|
@@ -37,11 +65,12 @@ export interface RateLimiterOptions<TFn extends AnyFunction> {
|
|
|
37
65
|
windowType?: 'fixed' | 'sliding'
|
|
38
66
|
}
|
|
39
67
|
|
|
40
|
-
const defaultOptions:
|
|
68
|
+
const defaultOptions: Omit<
|
|
69
|
+
Required<RateLimiterOptions<any>>,
|
|
70
|
+
'initialState' | 'onExecute' | 'onReject'
|
|
71
|
+
> = {
|
|
41
72
|
enabled: true,
|
|
42
73
|
limit: 1,
|
|
43
|
-
onExecute: () => {},
|
|
44
|
-
onReject: () => {},
|
|
45
74
|
window: 0,
|
|
46
75
|
windowType: 'fixed',
|
|
47
76
|
}
|
|
@@ -66,11 +95,24 @@ const defaultOptions: Required<RateLimiterOptions<any>> = {
|
|
|
66
95
|
* Rate limiting is best used for hard API limits or resource constraints. For UI updates or
|
|
67
96
|
* smoothing out frequent events, throttling or debouncing usually provide better user experience.
|
|
68
97
|
*
|
|
98
|
+
* State Management:
|
|
99
|
+
* - Uses TanStack Store for reactive state management
|
|
100
|
+
* - Use `initialState` to provide initial state values when creating the rate limiter
|
|
101
|
+
* - Use `onExecute` callback to react to function execution and implement custom logic
|
|
102
|
+
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
|
|
103
|
+
* - The state includes execution count, execution times, and rejection count
|
|
104
|
+
* - State can be accessed via `rateLimiter.store.state` when using the class directly
|
|
105
|
+
* - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`
|
|
106
|
+
*
|
|
69
107
|
* @example
|
|
70
108
|
* ```ts
|
|
71
109
|
* const rateLimiter = new RateLimiter(
|
|
72
110
|
* (id: string) => api.getData(id),
|
|
73
|
-
* {
|
|
111
|
+
* {
|
|
112
|
+
* limit: 5,
|
|
113
|
+
* window: 1000,
|
|
114
|
+
* windowType: 'sliding',
|
|
115
|
+
* }
|
|
74
116
|
* );
|
|
75
117
|
*
|
|
76
118
|
* // Will execute immediately until limit reached, then block
|
|
@@ -78,54 +120,57 @@ const defaultOptions: Required<RateLimiterOptions<any>> = {
|
|
|
78
120
|
* ```
|
|
79
121
|
*/
|
|
80
122
|
export class RateLimiter<TFn extends AnyFunction> {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
private _options: RateLimiterOptions<TFn>
|
|
123
|
+
readonly store: Store<Readonly<RateLimiterState>> =
|
|
124
|
+
new Store<RateLimiterState>(getDefaultRateLimiterState())
|
|
125
|
+
options: RateLimiterOptions<TFn>
|
|
85
126
|
|
|
86
127
|
constructor(
|
|
87
128
|
private fn: TFn,
|
|
88
129
|
initialOptions: RateLimiterOptions<TFn>,
|
|
89
130
|
) {
|
|
90
|
-
this.
|
|
131
|
+
this.options = {
|
|
91
132
|
...defaultOptions,
|
|
92
133
|
...initialOptions,
|
|
93
134
|
}
|
|
135
|
+
this.#setState(this.options.initialState ?? {})
|
|
94
136
|
}
|
|
95
137
|
|
|
96
138
|
/**
|
|
97
139
|
* Updates the rate limiter options
|
|
98
140
|
*/
|
|
99
|
-
setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {
|
|
100
|
-
this.
|
|
141
|
+
setOptions = (newOptions: Partial<RateLimiterOptions<TFn>>): void => {
|
|
142
|
+
this.options = { ...this.options, ...newOptions }
|
|
101
143
|
}
|
|
102
144
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
145
|
+
#setState = (newState: Partial<RateLimiterState>): void => {
|
|
146
|
+
this.store.setState((state) => {
|
|
147
|
+
const combinedState = {
|
|
148
|
+
...state,
|
|
149
|
+
...newState,
|
|
150
|
+
}
|
|
151
|
+
return combinedState
|
|
152
|
+
})
|
|
108
153
|
}
|
|
109
154
|
|
|
110
155
|
/**
|
|
111
156
|
* Returns the current enabled state of the rate limiter
|
|
112
157
|
*/
|
|
113
|
-
getEnabled(): boolean {
|
|
114
|
-
return parseFunctionOrValue(this.
|
|
158
|
+
#getEnabled = (): boolean => {
|
|
159
|
+
return !!parseFunctionOrValue(this.options.enabled, this)
|
|
115
160
|
}
|
|
116
161
|
|
|
117
162
|
/**
|
|
118
163
|
* Returns the current limit of executions allowed within the time window
|
|
119
164
|
*/
|
|
120
|
-
getLimit(): number {
|
|
121
|
-
return parseFunctionOrValue(this.
|
|
165
|
+
#getLimit = (): number => {
|
|
166
|
+
return parseFunctionOrValue(this.options.limit, this)
|
|
122
167
|
}
|
|
123
168
|
|
|
124
169
|
/**
|
|
125
170
|
* Returns the current time window in milliseconds
|
|
126
171
|
*/
|
|
127
|
-
getWindow(): number {
|
|
128
|
-
return parseFunctionOrValue(this.
|
|
172
|
+
#getWindow = (): number => {
|
|
173
|
+
return parseFunctionOrValue(this.options.window, this)
|
|
129
174
|
}
|
|
130
175
|
|
|
131
176
|
/**
|
|
@@ -143,95 +188,86 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
143
188
|
* rateLimiter.maybeExecute('arg1', 'arg2'); // false
|
|
144
189
|
* ```
|
|
145
190
|
*/
|
|
146
|
-
maybeExecute(...args: Parameters<TFn>): boolean {
|
|
147
|
-
this
|
|
191
|
+
maybeExecute = (...args: Parameters<TFn>): boolean => {
|
|
192
|
+
this.#cleanupOldExecutions()
|
|
148
193
|
|
|
149
|
-
|
|
150
|
-
// For sliding window, we can execute if we have capacity in the current window
|
|
151
|
-
if (this._executionTimes.length < this.getLimit()) {
|
|
152
|
-
this.execute(...args)
|
|
153
|
-
return true
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
// For fixed window, we need to check if we're in a new window
|
|
157
|
-
const now = Date.now()
|
|
158
|
-
const oldestExecution = Math.min(...this._executionTimes)
|
|
159
|
-
const isNewWindow = oldestExecution + this.getWindow() <= now
|
|
194
|
+
const relevantExecutionTimes = this.#getRelevantExecutionTimes()
|
|
160
195
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
196
|
+
if (relevantExecutionTimes.length < this.#getLimit()) {
|
|
197
|
+
this.#execute(...args)
|
|
198
|
+
return true
|
|
165
199
|
}
|
|
166
200
|
|
|
167
|
-
this
|
|
201
|
+
this.#setState({
|
|
202
|
+
rejectionCount: this.store.state.rejectionCount + 1,
|
|
203
|
+
})
|
|
204
|
+
this.options.onReject?.(this)
|
|
168
205
|
return false
|
|
169
206
|
}
|
|
170
207
|
|
|
171
|
-
|
|
172
|
-
if (!this
|
|
208
|
+
#execute = (...args: Parameters<TFn>): void => {
|
|
209
|
+
if (!this.#getEnabled()) return
|
|
173
210
|
const now = Date.now()
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
176
|
-
this
|
|
177
|
-
|
|
211
|
+
this.fn(...args) // EXECUTE!
|
|
212
|
+
this.store.state.executionTimes.push(now) // mutate state directly for performance
|
|
213
|
+
this.#setState({
|
|
214
|
+
executionCount: this.store.state.executionCount + 1,
|
|
215
|
+
})
|
|
216
|
+
this.options.onExecute?.(this)
|
|
178
217
|
}
|
|
179
218
|
|
|
180
|
-
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
this.
|
|
219
|
+
#getRelevantExecutionTimes = (): Array<number> => {
|
|
220
|
+
if (this.options.windowType === 'sliding') {
|
|
221
|
+
// For sliding window, return all executions within the current window
|
|
222
|
+
return this.store.state.executionTimes.filter(
|
|
223
|
+
(time) => time > Date.now() - this.#getWindow(),
|
|
224
|
+
)
|
|
225
|
+
} else {
|
|
226
|
+
// For fixed window, return all executions in the current window
|
|
227
|
+
// The window starts from the oldest execution time
|
|
228
|
+
const oldestExecution = Math.min(...this.store.state.executionTimes)
|
|
229
|
+
const windowStart = oldestExecution
|
|
230
|
+
return this.store.state.executionTimes.filter(
|
|
231
|
+
(time) =>
|
|
232
|
+
time >= windowStart && time <= windowStart + this.#getWindow(),
|
|
233
|
+
)
|
|
184
234
|
}
|
|
185
235
|
}
|
|
186
236
|
|
|
187
|
-
|
|
237
|
+
#cleanupOldExecutions = (): void => {
|
|
188
238
|
const now = Date.now()
|
|
189
|
-
const windowStart = now - this
|
|
190
|
-
this
|
|
191
|
-
(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Returns the number of times the function has been executed
|
|
197
|
-
*/
|
|
198
|
-
getExecutionCount(): number {
|
|
199
|
-
return this._executionCount
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Returns the number of times the function has been rejected
|
|
204
|
-
*/
|
|
205
|
-
getRejectionCount(): number {
|
|
206
|
-
return this._rejectionCount
|
|
239
|
+
const windowStart = now - this.#getWindow()
|
|
240
|
+
this.#setState({
|
|
241
|
+
executionTimes: this.store.state.executionTimes.filter(
|
|
242
|
+
(time) => time > windowStart,
|
|
243
|
+
),
|
|
244
|
+
})
|
|
207
245
|
}
|
|
208
246
|
|
|
209
247
|
/**
|
|
210
248
|
* Returns the number of remaining executions allowed in the current window
|
|
211
249
|
*/
|
|
212
|
-
getRemainingInWindow(): number {
|
|
213
|
-
this
|
|
214
|
-
return Math.max(0, this
|
|
250
|
+
getRemainingInWindow = (): number => {
|
|
251
|
+
const relevantExecutionTimes = this.#getRelevantExecutionTimes()
|
|
252
|
+
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)
|
|
215
253
|
}
|
|
216
254
|
|
|
217
255
|
/**
|
|
218
256
|
* Returns the number of milliseconds until the next execution will be possible
|
|
219
257
|
*/
|
|
220
|
-
getMsUntilNextWindow(): number {
|
|
258
|
+
getMsUntilNextWindow = (): number => {
|
|
221
259
|
if (this.getRemainingInWindow() > 0) {
|
|
222
260
|
return 0
|
|
223
261
|
}
|
|
224
|
-
const oldestExecution =
|
|
225
|
-
return oldestExecution + this
|
|
262
|
+
const oldestExecution = this.store.state.executionTimes[0] ?? Infinity
|
|
263
|
+
return oldestExecution + this.#getWindow() - Date.now()
|
|
226
264
|
}
|
|
227
265
|
|
|
228
266
|
/**
|
|
229
267
|
* Resets the rate limiter state
|
|
230
268
|
*/
|
|
231
|
-
reset(): void {
|
|
232
|
-
this
|
|
233
|
-
this._executionCount = 0
|
|
234
|
-
this._rejectionCount = 0
|
|
269
|
+
reset = (): void => {
|
|
270
|
+
this.#setState(getDefaultRateLimiterState())
|
|
235
271
|
}
|
|
236
272
|
}
|
|
237
273
|
|
|
@@ -249,6 +285,15 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
249
285
|
* - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
|
|
250
286
|
* consistent rate of execution over time.
|
|
251
287
|
*
|
|
288
|
+
* State Management:
|
|
289
|
+
* - Uses TanStack Store for reactive state management
|
|
290
|
+
* - Use `initialState` to provide initial state values when creating the rate limiter
|
|
291
|
+
* - Use `onExecute` callback to react to function execution and implement custom logic
|
|
292
|
+
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
|
|
293
|
+
* - The state includes execution count, execution times, and rejection count
|
|
294
|
+
* - State can be accessed via the underlying RateLimiter instance's `store.state` property
|
|
295
|
+
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
|
|
296
|
+
*
|
|
252
297
|
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
|
|
253
298
|
* need to enforce a hard limit on the number of executions within a time period.
|
|
254
299
|
*
|
|
@@ -277,5 +322,5 @@ export function rateLimit<TFn extends AnyFunction>(
|
|
|
277
322
|
initialOptions: RateLimiterOptions<TFn>,
|
|
278
323
|
) {
|
|
279
324
|
const rateLimiter = new RateLimiter(fn, initialOptions)
|
|
280
|
-
return rateLimiter.maybeExecute
|
|
325
|
+
return rateLimiter.maybeExecute
|
|
281
326
|
}
|
package/src/throttler.ts
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
|
+
import { Store } from '@tanstack/store'
|
|
1
2
|
import { parseFunctionOrValue } from './utils'
|
|
2
3
|
import type { AnyFunction } from './types'
|
|
3
4
|
|
|
5
|
+
export interface ThrottlerState<TFn extends AnyFunction> {
|
|
6
|
+
/**
|
|
7
|
+
* Number of function executions that have been completed
|
|
8
|
+
*/
|
|
9
|
+
executionCount: number
|
|
10
|
+
/**
|
|
11
|
+
* The arguments from the most recent call to maybeExecute
|
|
12
|
+
*/
|
|
13
|
+
lastArgs: Parameters<TFn> | undefined
|
|
14
|
+
/**
|
|
15
|
+
* Timestamp of the last function execution in milliseconds
|
|
16
|
+
*/
|
|
17
|
+
lastExecutionTime: number
|
|
18
|
+
/**
|
|
19
|
+
* Timestamp when the next execution can occur in milliseconds
|
|
20
|
+
*/
|
|
21
|
+
nextExecutionTime: number
|
|
22
|
+
/**
|
|
23
|
+
* Whether the throttler is waiting for the timeout to trigger execution
|
|
24
|
+
*/
|
|
25
|
+
isPending: boolean
|
|
26
|
+
/**
|
|
27
|
+
* Current execution status - 'idle' when not active, 'pending' when waiting for timeout
|
|
28
|
+
*/
|
|
29
|
+
status: 'disabled' | 'idle' | 'pending'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getDefaultThrottlerState<
|
|
33
|
+
TFn extends AnyFunction,
|
|
34
|
+
>(): ThrottlerState<TFn> {
|
|
35
|
+
return structuredClone({
|
|
36
|
+
executionCount: 0,
|
|
37
|
+
isPending: false,
|
|
38
|
+
lastArgs: undefined,
|
|
39
|
+
lastExecutionTime: 0,
|
|
40
|
+
nextExecutionTime: 0,
|
|
41
|
+
status: 'idle',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
4
45
|
/**
|
|
5
46
|
* Options for configuring a throttled function
|
|
6
47
|
*/
|
|
@@ -11,6 +52,10 @@ export interface ThrottlerOptions<TFn extends AnyFunction> {
|
|
|
11
52
|
* Defaults to true.
|
|
12
53
|
*/
|
|
13
54
|
enabled?: boolean | ((throttler: Throttler<TFn>) => boolean)
|
|
55
|
+
/**
|
|
56
|
+
* Initial state for the throttler
|
|
57
|
+
*/
|
|
58
|
+
initialState?: Partial<ThrottlerState<TFn>>
|
|
14
59
|
/**
|
|
15
60
|
* Whether to execute on the leading edge of the timeout.
|
|
16
61
|
* Defaults to true.
|
|
@@ -33,10 +78,12 @@ export interface ThrottlerOptions<TFn extends AnyFunction> {
|
|
|
33
78
|
wait: number | ((throttler: Throttler<TFn>) => number)
|
|
34
79
|
}
|
|
35
80
|
|
|
36
|
-
const defaultOptions:
|
|
81
|
+
const defaultOptions: Omit<
|
|
82
|
+
Required<ThrottlerOptions<any>>,
|
|
83
|
+
'initialState' | 'onExecute'
|
|
84
|
+
> = {
|
|
37
85
|
enabled: true,
|
|
38
86
|
leading: true,
|
|
39
|
-
onExecute: () => {},
|
|
40
87
|
trailing: true,
|
|
41
88
|
wait: 0,
|
|
42
89
|
}
|
|
@@ -54,6 +101,14 @@ const defaultOptions: Required<ThrottlerOptions<any>> = {
|
|
|
54
101
|
*
|
|
55
102
|
* For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.
|
|
56
103
|
*
|
|
104
|
+
* State Management:
|
|
105
|
+
* - Uses TanStack Store for reactive state management
|
|
106
|
+
* - Use `initialState` to provide initial state values when creating the throttler
|
|
107
|
+
* - Use `onExecute` callback to react to function execution and implement custom logic
|
|
108
|
+
* - The state includes execution count, last execution time, pending status, and more
|
|
109
|
+
* - State can be accessed via `throttler.store.state` when using the class directly
|
|
110
|
+
* - When using framework adapters (React/Solid), state is accessed from `throttler.state`
|
|
111
|
+
*
|
|
57
112
|
* @example
|
|
58
113
|
* ```ts
|
|
59
114
|
* const throttler = new Throttler(
|
|
@@ -69,53 +124,59 @@ const defaultOptions: Required<ThrottlerOptions<any>> = {
|
|
|
69
124
|
* ```
|
|
70
125
|
*/
|
|
71
126
|
export class Throttler<TFn extends AnyFunction> {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
127
|
+
readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(
|
|
128
|
+
getDefaultThrottlerState(),
|
|
129
|
+
)
|
|
130
|
+
options: ThrottlerOptions<TFn>
|
|
131
|
+
#timeoutId: NodeJS.Timeout | undefined
|
|
77
132
|
|
|
78
133
|
constructor(
|
|
79
134
|
private fn: TFn,
|
|
80
135
|
initialOptions: ThrottlerOptions<TFn>,
|
|
81
136
|
) {
|
|
82
|
-
this.
|
|
137
|
+
this.options = {
|
|
83
138
|
...defaultOptions,
|
|
84
139
|
...initialOptions,
|
|
85
140
|
}
|
|
141
|
+
this.#setState(this.options.initialState ?? {})
|
|
86
142
|
}
|
|
87
143
|
|
|
88
144
|
/**
|
|
89
145
|
* Updates the throttler options
|
|
90
146
|
*/
|
|
91
|
-
setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void {
|
|
92
|
-
this.
|
|
147
|
+
setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {
|
|
148
|
+
this.options = { ...this.options, ...newOptions }
|
|
93
149
|
|
|
94
|
-
//
|
|
95
|
-
if (!this
|
|
150
|
+
// Cancel pending execution if the throttler is disabled
|
|
151
|
+
if (!this.#getEnabled()) {
|
|
96
152
|
this.cancel()
|
|
97
153
|
}
|
|
98
154
|
}
|
|
99
155
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
156
|
+
#setState = (newState: Partial<ThrottlerState<TFn>>): void => {
|
|
157
|
+
this.store.setState((state) => {
|
|
158
|
+
const combinedState = {
|
|
159
|
+
...state,
|
|
160
|
+
...newState,
|
|
161
|
+
}
|
|
162
|
+
const { isPending } = combinedState
|
|
163
|
+
return {
|
|
164
|
+
...combinedState,
|
|
165
|
+
status: !this.#getEnabled()
|
|
166
|
+
? 'disabled'
|
|
167
|
+
: isPending
|
|
168
|
+
? 'pending'
|
|
169
|
+
: 'idle',
|
|
170
|
+
}
|
|
171
|
+
})
|
|
105
172
|
}
|
|
106
173
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
*/
|
|
110
|
-
getEnabled(): boolean {
|
|
111
|
-
return parseFunctionOrValue(this._options.enabled, this)
|
|
174
|
+
#getEnabled = (): boolean => {
|
|
175
|
+
return !!parseFunctionOrValue(this.options.enabled, this)
|
|
112
176
|
}
|
|
113
177
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*/
|
|
117
|
-
getWait(): number {
|
|
118
|
-
return parseFunctionOrValue(this._options.wait, this)
|
|
178
|
+
#getWait = (): number => {
|
|
179
|
+
return parseFunctionOrValue(this.options.wait, this)
|
|
119
180
|
}
|
|
120
181
|
|
|
121
182
|
/**
|
|
@@ -140,86 +201,91 @@ export class Throttler<TFn extends AnyFunction> {
|
|
|
140
201
|
* throttled.maybeExecute('c', 'd');
|
|
141
202
|
* ```
|
|
142
203
|
*/
|
|
143
|
-
maybeExecute(...args: Parameters<TFn>): void {
|
|
204
|
+
maybeExecute = (...args: Parameters<TFn>): void => {
|
|
144
205
|
const now = Date.now()
|
|
145
|
-
const timeSinceLastExecution = now - this.
|
|
146
|
-
const wait = this
|
|
206
|
+
const timeSinceLastExecution = now - this.store.state.lastExecutionTime
|
|
207
|
+
const wait = this.#getWait()
|
|
147
208
|
|
|
148
209
|
// Handle leading execution
|
|
149
|
-
if (this.
|
|
150
|
-
this
|
|
210
|
+
if (this.options.leading && timeSinceLastExecution >= wait) {
|
|
211
|
+
this.#execute(...args)
|
|
151
212
|
} else {
|
|
152
213
|
// Store the most recent arguments for potential trailing execution
|
|
153
|
-
this
|
|
154
|
-
|
|
214
|
+
this.#setState({
|
|
215
|
+
lastArgs: args,
|
|
216
|
+
})
|
|
155
217
|
// Set up trailing execution if not already scheduled
|
|
156
|
-
if (!this
|
|
157
|
-
|
|
158
|
-
|
|
218
|
+
if (!this.#timeoutId && this.options.trailing) {
|
|
219
|
+
// prevent large number if lastExecutionTime is undefined
|
|
220
|
+
const _timeSinceLastExecution = this.store.state.lastExecutionTime
|
|
221
|
+
? now - this.store.state.lastExecutionTime
|
|
159
222
|
: 0
|
|
160
223
|
const timeoutDuration = wait - _timeSinceLastExecution
|
|
161
|
-
this
|
|
162
|
-
|
|
163
|
-
|
|
224
|
+
this.#setState({ isPending: true })
|
|
225
|
+
this.#timeoutId = setTimeout(() => {
|
|
226
|
+
const { lastArgs } = this.store.state
|
|
227
|
+
if (lastArgs !== undefined) {
|
|
228
|
+
this.#execute(...lastArgs)
|
|
164
229
|
}
|
|
165
230
|
}, timeoutDuration)
|
|
166
231
|
}
|
|
167
232
|
}
|
|
168
233
|
}
|
|
169
234
|
|
|
170
|
-
|
|
171
|
-
if (!this
|
|
235
|
+
#execute = (...args: Parameters<TFn>): void => {
|
|
236
|
+
if (!this.#getEnabled()) return
|
|
172
237
|
this.fn(...args) // EXECUTE!
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
this
|
|
176
|
-
this
|
|
177
|
-
|
|
238
|
+
const lastExecutionTime = Date.now()
|
|
239
|
+
const nextExecutionTime = lastExecutionTime + this.#getWait()
|
|
240
|
+
this.#clearTimeout()
|
|
241
|
+
this.#setState({
|
|
242
|
+
executionCount: this.store.state.executionCount + 1,
|
|
243
|
+
lastExecutionTime,
|
|
244
|
+
nextExecutionTime,
|
|
245
|
+
isPending: false,
|
|
246
|
+
lastArgs: undefined,
|
|
247
|
+
})
|
|
248
|
+
this.options.onExecute?.(this)
|
|
178
249
|
}
|
|
179
250
|
|
|
180
251
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* If a trailing execution is scheduled (due to throttling with trailing=true),
|
|
184
|
-
* this will prevent that execution from occurring. The internal timeout and
|
|
185
|
-
* stored arguments will be cleared.
|
|
186
|
-
*
|
|
187
|
-
* Has no effect if there is no pending execution.
|
|
252
|
+
* Processes the current pending execution immediately
|
|
188
253
|
*/
|
|
189
|
-
|
|
190
|
-
if (this.
|
|
191
|
-
|
|
192
|
-
this._timeoutId = undefined
|
|
193
|
-
this._lastArgs = undefined
|
|
254
|
+
flush = (): void => {
|
|
255
|
+
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
256
|
+
this.#execute(...this.store.state.lastArgs)
|
|
194
257
|
}
|
|
195
258
|
}
|
|
196
259
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Returns the next execution time
|
|
206
|
-
*/
|
|
207
|
-
getNextExecutionTime(): number {
|
|
208
|
-
return this._lastExecutionTime + this.getWait()
|
|
260
|
+
#clearTimeout = (): void => {
|
|
261
|
+
if (this.#timeoutId) {
|
|
262
|
+
clearTimeout(this.#timeoutId)
|
|
263
|
+
this.#timeoutId = undefined
|
|
264
|
+
}
|
|
209
265
|
}
|
|
210
266
|
|
|
211
267
|
/**
|
|
212
|
-
*
|
|
268
|
+
* Cancels any pending trailing execution and clears internal state.
|
|
269
|
+
*
|
|
270
|
+
* If a trailing execution is scheduled (due to throttling with trailing=true),
|
|
271
|
+
* this will prevent that execution from occurring. The internal timeout and
|
|
272
|
+
* stored arguments will be cleared.
|
|
273
|
+
*
|
|
274
|
+
* Has no effect if there is no pending execution.
|
|
213
275
|
*/
|
|
214
|
-
|
|
215
|
-
|
|
276
|
+
cancel = (): void => {
|
|
277
|
+
this.#clearTimeout()
|
|
278
|
+
this.#setState({
|
|
279
|
+
lastArgs: undefined,
|
|
280
|
+
isPending: false,
|
|
281
|
+
})
|
|
216
282
|
}
|
|
217
283
|
|
|
218
284
|
/**
|
|
219
|
-
*
|
|
285
|
+
* Resets the throttler state to its default values
|
|
220
286
|
*/
|
|
221
|
-
|
|
222
|
-
|
|
287
|
+
reset = (): void => {
|
|
288
|
+
this.#setState(getDefaultThrottlerState<TFn>())
|
|
223
289
|
}
|
|
224
290
|
}
|
|
225
291
|
|
|
@@ -236,6 +302,14 @@ export class Throttler<TFn extends AnyFunction> {
|
|
|
236
302
|
* For handling bursts of events, consider using debounce() instead. For hard execution
|
|
237
303
|
* limits, consider using rateLimit().
|
|
238
304
|
*
|
|
305
|
+
* State Management:
|
|
306
|
+
* - Uses TanStack Store for reactive state management
|
|
307
|
+
* - Use `initialState` to provide initial state values when creating the throttler
|
|
308
|
+
* - Use `onExecute` callback to react to function execution and implement custom logic
|
|
309
|
+
* - The state includes execution count, last execution time, pending status, and more
|
|
310
|
+
* - State can be accessed via the underlying Throttler instance's `store.state` property
|
|
311
|
+
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
|
|
312
|
+
*
|
|
239
313
|
* @example
|
|
240
314
|
* ```ts
|
|
241
315
|
* // Basic throttling - max once per second
|
|
@@ -254,5 +328,5 @@ export function throttle<TFn extends AnyFunction>(
|
|
|
254
328
|
initialOptions: ThrottlerOptions<TFn>,
|
|
255
329
|
) {
|
|
256
330
|
const throttler = new Throttler(fn, initialOptions)
|
|
257
|
-
return throttler.maybeExecute
|
|
331
|
+
return throttler.maybeExecute
|
|
258
332
|
}
|
package/src/utils.ts
CHANGED
|
@@ -10,18 +10,3 @@ export function parseFunctionOrValue<T, TArgs extends Array<any>>(
|
|
|
10
10
|
): T {
|
|
11
11
|
return isFunction(value) ? value(...args) : value
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
export function bindInstanceMethods<T extends Record<string, any>>(
|
|
15
|
-
instance: T,
|
|
16
|
-
): T {
|
|
17
|
-
return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(
|
|
18
|
-
(acc: any, key) => {
|
|
19
|
-
const method = instance[key as keyof T]
|
|
20
|
-
if (isFunction(method)) {
|
|
21
|
-
acc[key] = method.bind(instance)
|
|
22
|
-
}
|
|
23
|
-
return acc
|
|
24
|
-
},
|
|
25
|
-
instance,
|
|
26
|
-
)
|
|
27
|
-
}
|