@tanstack/pacer 0.3.0 → 0.5.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-debouncer.cjs +16 -3
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +30 -12
- package/dist/cjs/async-queuer.cjs +20 -6
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +21 -8
- package/dist/cjs/async-rate-limiter.cjs +55 -8
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +64 -12
- package/dist/cjs/async-throttler.cjs +19 -5
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +31 -12
- package/dist/cjs/debouncer.cjs +16 -3
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +14 -4
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/queuer.cjs +13 -5
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +9 -3
- package/dist/cjs/rate-limiter.cjs +41 -8
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +42 -8
- package/dist/cjs/throttler.cjs +19 -5
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +15 -4
- package/dist/cjs/utils.cjs +18 -7
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +3 -0
- package/dist/esm/async-debouncer.d.ts +30 -12
- package/dist/esm/async-debouncer.js +16 -3
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +21 -8
- package/dist/esm/async-queuer.js +20 -6
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +64 -12
- package/dist/esm/async-rate-limiter.js +55 -8
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +31 -12
- package/dist/esm/async-throttler.js +19 -5
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/debouncer.d.ts +14 -4
- package/dist/esm/debouncer.js +16 -3
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/queuer.d.ts +9 -3
- package/dist/esm/queuer.js +13 -5
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +42 -8
- package/dist/esm/rate-limiter.js +41 -8
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +15 -4
- package/dist/esm/throttler.js +19 -5
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.d.ts +3 -0
- package/dist/esm/utils.js +19 -8
- package/dist/esm/utils.js.map +1 -1
- package/package.json +9 -3
- package/src/async-debouncer.ts +40 -15
- package/src/async-queuer.ts +32 -13
- package/src/async-rate-limiter.ts +107 -19
- package/src/async-throttler.ts +44 -17
- package/src/debouncer.ts +24 -7
- package/src/queuer.ts +19 -7
- package/src/rate-limiter.ts +76 -16
- package/src/throttler.ts +28 -9
- package/src/utils.ts +19 -5
package/src/debouncer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
1
2
|
import type { AnyFunction } from './types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -6,9 +7,10 @@ import type { AnyFunction } from './types'
|
|
|
6
7
|
export interface DebouncerOptions<TFn extends AnyFunction> {
|
|
7
8
|
/**
|
|
8
9
|
* Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
10
|
+
* Can be a boolean or a function that returns a boolean.
|
|
9
11
|
* Defaults to true.
|
|
10
12
|
*/
|
|
11
|
-
enabled?: boolean
|
|
13
|
+
enabled?: boolean | ((debouncer: Debouncer<TFn>) => boolean)
|
|
12
14
|
/**
|
|
13
15
|
* Whether to execute on the leading edge of the timeout.
|
|
14
16
|
* The first call will execute immediately and the rest will wait the delay.
|
|
@@ -25,10 +27,11 @@ export interface DebouncerOptions<TFn extends AnyFunction> {
|
|
|
25
27
|
*/
|
|
26
28
|
trailing?: boolean
|
|
27
29
|
/**
|
|
28
|
-
* Delay in milliseconds before executing the function
|
|
30
|
+
* Delay in milliseconds before executing the function.
|
|
31
|
+
* Can be a number or a function that returns a number.
|
|
29
32
|
* Defaults to 0ms
|
|
30
33
|
*/
|
|
31
|
-
wait: number
|
|
34
|
+
wait: number | ((debouncer: Debouncer<TFn>) => number)
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
const defaultOptions: Required<DebouncerOptions<any>> = {
|
|
@@ -99,6 +102,20 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
99
102
|
return this._options
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Returns the current enabled state of the debouncer
|
|
107
|
+
*/
|
|
108
|
+
getEnabled(): boolean {
|
|
109
|
+
return parseFunctionOrValue(this._options.enabled, this)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns the current wait time in milliseconds
|
|
114
|
+
*/
|
|
115
|
+
getWait(): number {
|
|
116
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
/**
|
|
103
120
|
* Attempts to execute the debounced function
|
|
104
121
|
* If a call is already in progress, it will be queued
|
|
@@ -127,11 +144,11 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
127
144
|
if (this._options.trailing && !_didLeadingExecute) {
|
|
128
145
|
this.executeFunction(...args)
|
|
129
146
|
}
|
|
130
|
-
}, this.
|
|
147
|
+
}, this.getWait())
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
private executeFunction(...args: Parameters<TFn>): void {
|
|
134
|
-
if (!this.
|
|
151
|
+
if (!this.getEnabled()) return undefined
|
|
135
152
|
this.fn(...args) // EXECUTE!
|
|
136
153
|
this._isPending = false
|
|
137
154
|
this._executionCount++
|
|
@@ -160,7 +177,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
160
177
|
* Returns `true` if debouncing
|
|
161
178
|
*/
|
|
162
179
|
getIsPending(): boolean {
|
|
163
|
-
return this.
|
|
180
|
+
return this.getEnabled() && this._isPending
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
183
|
|
|
@@ -186,7 +203,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
186
203
|
*/
|
|
187
204
|
export function debounce<TFn extends AnyFunction>(
|
|
188
205
|
fn: TFn,
|
|
189
|
-
initialOptions:
|
|
206
|
+
initialOptions: DebouncerOptions<TFn>,
|
|
190
207
|
): (...args: Parameters<TFn>) => void {
|
|
191
208
|
const debouncer = new Debouncer(fn, initialOptions)
|
|
192
209
|
return debouncer.maybeExecute.bind(debouncer)
|
package/src/queuer.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Options for configuring a Queuer instance
|
|
3
5
|
*/
|
|
@@ -60,9 +62,11 @@ export interface QueuerOptions<TValue> {
|
|
|
60
62
|
*/
|
|
61
63
|
started?: boolean
|
|
62
64
|
/**
|
|
63
|
-
* Time in milliseconds to wait between processing items
|
|
65
|
+
* Time in milliseconds to wait between processing items.
|
|
66
|
+
* Can be a number or a function that returns a number.
|
|
67
|
+
* @default 0
|
|
64
68
|
*/
|
|
65
|
-
wait?: number
|
|
69
|
+
wait?: number | ((queuer: Queuer<TValue>) => number)
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
const defaultOptions: Required<QueuerOptions<any>> = {
|
|
@@ -78,7 +82,7 @@ const defaultOptions: Required<QueuerOptions<any>> = {
|
|
|
78
82
|
onItemsChange: () => {},
|
|
79
83
|
onReject: () => {},
|
|
80
84
|
onExpire: () => {},
|
|
81
|
-
started:
|
|
85
|
+
started: true,
|
|
82
86
|
wait: 0,
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -178,6 +182,13 @@ export class Queuer<TValue> {
|
|
|
178
182
|
return this._options
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Returns the current wait time in milliseconds
|
|
187
|
+
*/
|
|
188
|
+
getWait(): number {
|
|
189
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
190
|
+
}
|
|
191
|
+
|
|
181
192
|
/**
|
|
182
193
|
* Processes items in the queuer
|
|
183
194
|
*/
|
|
@@ -197,9 +208,10 @@ export class Queuer<TValue> {
|
|
|
197
208
|
}
|
|
198
209
|
this._onItemsChanges.forEach((cb) => cb(nextItem))
|
|
199
210
|
|
|
200
|
-
|
|
211
|
+
const wait = this.getWait()
|
|
212
|
+
if (wait > 0) {
|
|
201
213
|
// Use setTimeout to wait before processing next item
|
|
202
|
-
setTimeout(() => this.tick(),
|
|
214
|
+
setTimeout(() => this.tick(), wait)
|
|
203
215
|
return
|
|
204
216
|
}
|
|
205
217
|
|
|
@@ -493,7 +505,7 @@ export class Queuer<TValue> {
|
|
|
493
505
|
* processPriority(3) // Processed before 1
|
|
494
506
|
* ```
|
|
495
507
|
*/
|
|
496
|
-
export function queue<TValue>(options: QueuerOptions<TValue>
|
|
497
|
-
const queuer = new Queuer<TValue>(
|
|
508
|
+
export function queue<TValue>(options: QueuerOptions<TValue>) {
|
|
509
|
+
const queuer = new Queuer<TValue>(options)
|
|
498
510
|
return queuer.addItem.bind(queuer)
|
|
499
511
|
}
|
package/src/rate-limiter.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
1
2
|
import type { AnyFunction } from './types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -8,11 +9,12 @@ export interface RateLimiterOptions<TFn extends AnyFunction> {
|
|
|
8
9
|
* Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
9
10
|
* Defaults to true.
|
|
10
11
|
*/
|
|
11
|
-
enabled?: boolean
|
|
12
|
+
enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)
|
|
12
13
|
/**
|
|
13
|
-
* Maximum number of executions allowed within the time window
|
|
14
|
+
* Maximum number of executions allowed within the time window.
|
|
15
|
+
* Can be a number or a callback function that receives the rate limiter instance and returns a number.
|
|
14
16
|
*/
|
|
15
|
-
limit: number
|
|
17
|
+
limit: number | ((rateLimiter: RateLimiter<TFn>) => number)
|
|
16
18
|
/**
|
|
17
19
|
* Callback function that is called after the function is executed
|
|
18
20
|
*/
|
|
@@ -22,9 +24,17 @@ export interface RateLimiterOptions<TFn extends AnyFunction> {
|
|
|
22
24
|
*/
|
|
23
25
|
onReject?: (rateLimiter: RateLimiter<TFn>) => void
|
|
24
26
|
/**
|
|
25
|
-
* Time window in milliseconds within which the limit applies
|
|
27
|
+
* Time window in milliseconds within which the limit applies.
|
|
28
|
+
* Can be a number or a callback function that receives the rate limiter instance and returns a number.
|
|
26
29
|
*/
|
|
27
|
-
window: number
|
|
30
|
+
window: number | ((rateLimiter: RateLimiter<TFn>) => number)
|
|
31
|
+
/**
|
|
32
|
+
* Type of window to use for rate limiting
|
|
33
|
+
* - 'fixed': Uses a fixed window that resets after the window period
|
|
34
|
+
* - 'sliding': Uses a sliding window that allows executions as old ones expire
|
|
35
|
+
* Defaults to 'fixed'
|
|
36
|
+
*/
|
|
37
|
+
windowType?: 'fixed' | 'sliding'
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
const defaultOptions: Required<RateLimiterOptions<any>> = {
|
|
@@ -33,6 +43,7 @@ const defaultOptions: Required<RateLimiterOptions<any>> = {
|
|
|
33
43
|
onExecute: () => {},
|
|
34
44
|
onReject: () => {},
|
|
35
45
|
window: 0,
|
|
46
|
+
windowType: 'fixed',
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
/**
|
|
@@ -42,6 +53,12 @@ const defaultOptions: Required<RateLimiterOptions<any>> = {
|
|
|
42
53
|
* then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where
|
|
43
54
|
* all executions happen immediately, followed by a complete block.
|
|
44
55
|
*
|
|
56
|
+
* The rate limiter supports two types of windows:
|
|
57
|
+
* - 'fixed': A strict window that resets after the window period. All executions within the window count
|
|
58
|
+
* towards the limit, and the window resets completely after the period.
|
|
59
|
+
* - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
|
|
60
|
+
* consistent rate of execution over time.
|
|
61
|
+
*
|
|
45
62
|
* For smoother execution patterns, consider using:
|
|
46
63
|
* - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)
|
|
47
64
|
* - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)
|
|
@@ -53,7 +70,7 @@ const defaultOptions: Required<RateLimiterOptions<any>> = {
|
|
|
53
70
|
* ```ts
|
|
54
71
|
* const rateLimiter = new RateLimiter(
|
|
55
72
|
* (id: string) => api.getData(id),
|
|
56
|
-
* { limit: 5, window: 1000 } // 5 calls per second
|
|
73
|
+
* { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window
|
|
57
74
|
* );
|
|
58
75
|
*
|
|
59
76
|
* // Will execute immediately until limit reached, then block
|
|
@@ -91,6 +108,27 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
91
108
|
return this._options as Required<RateLimiterOptions<TFn>>
|
|
92
109
|
}
|
|
93
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Returns the current enabled state of the rate limiter
|
|
113
|
+
*/
|
|
114
|
+
getEnabled(): boolean {
|
|
115
|
+
return parseFunctionOrValue(this._options.enabled, this)!
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Returns the current limit of executions allowed within the time window
|
|
120
|
+
*/
|
|
121
|
+
getLimit(): number {
|
|
122
|
+
return parseFunctionOrValue(this._options.limit, this)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns the current time window in milliseconds
|
|
127
|
+
*/
|
|
128
|
+
getWindow(): number {
|
|
129
|
+
return parseFunctionOrValue(this._options.window, this)
|
|
130
|
+
}
|
|
131
|
+
|
|
94
132
|
/**
|
|
95
133
|
* Attempts to execute the rate-limited function if within the configured limits.
|
|
96
134
|
* Will reject execution if the number of calls in the current window exceeds the limit.
|
|
@@ -109,18 +147,30 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
109
147
|
maybeExecute(...args: Parameters<TFn>): boolean {
|
|
110
148
|
this.cleanupOldExecutions()
|
|
111
149
|
|
|
112
|
-
if (this.
|
|
113
|
-
|
|
114
|
-
|
|
150
|
+
if (this._options.windowType === 'sliding') {
|
|
151
|
+
// For sliding window, we can execute if we have capacity in the current window
|
|
152
|
+
if (this._executionTimes.length < this.getLimit()) {
|
|
153
|
+
this.executeFunction(...args)
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// For fixed window, we need to check if we're in a new window
|
|
158
|
+
const now = Date.now()
|
|
159
|
+
const oldestExecution = Math.min(...this._executionTimes)
|
|
160
|
+
const isNewWindow = oldestExecution + this.getWindow() <= now
|
|
161
|
+
|
|
162
|
+
if (isNewWindow || this._executionTimes.length < this.getLimit()) {
|
|
163
|
+
this.executeFunction(...args)
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
115
166
|
}
|
|
116
167
|
|
|
117
168
|
this.rejectFunction()
|
|
118
|
-
|
|
119
169
|
return false
|
|
120
170
|
}
|
|
121
171
|
|
|
122
172
|
private executeFunction(...args: Parameters<TFn>): void {
|
|
123
|
-
if (!this.
|
|
173
|
+
if (!this.getEnabled()) return
|
|
124
174
|
const now = Date.now()
|
|
125
175
|
this._executionCount++
|
|
126
176
|
this._executionTimes.push(now)
|
|
@@ -137,7 +187,7 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
137
187
|
|
|
138
188
|
private cleanupOldExecutions(): void {
|
|
139
189
|
const now = Date.now()
|
|
140
|
-
const windowStart = now - this.
|
|
190
|
+
const windowStart = now - this.getWindow()
|
|
141
191
|
this._executionTimes = this._executionTimes.filter(
|
|
142
192
|
(time) => time > windowStart,
|
|
143
193
|
)
|
|
@@ -162,15 +212,18 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
162
212
|
*/
|
|
163
213
|
getRemainingInWindow(): number {
|
|
164
214
|
this.cleanupOldExecutions()
|
|
165
|
-
return Math.max(0, this.
|
|
215
|
+
return Math.max(0, this.getLimit() - this._executionTimes.length)
|
|
166
216
|
}
|
|
167
217
|
|
|
168
218
|
/**
|
|
169
219
|
* Returns the number of milliseconds until the next execution will be possible
|
|
170
220
|
*/
|
|
171
221
|
getMsUntilNextWindow(): number {
|
|
222
|
+
if (this.getRemainingInWindow() > 0) {
|
|
223
|
+
return 0
|
|
224
|
+
}
|
|
172
225
|
const oldestExecution = Math.min(...this._executionTimes)
|
|
173
|
-
return oldestExecution + this.
|
|
226
|
+
return oldestExecution + this.getWindow() - Date.now()
|
|
174
227
|
}
|
|
175
228
|
|
|
176
229
|
/**
|
|
@@ -191,15 +244,22 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
191
244
|
* - A throttler ensures even spacing between executions, which can be better for consistent performance
|
|
192
245
|
* - A debouncer collapses multiple calls into one, which is better for handling bursts of events
|
|
193
246
|
*
|
|
247
|
+
* The rate limiter supports two types of windows:
|
|
248
|
+
* - 'fixed': A strict window that resets after the window period. All executions within the window count
|
|
249
|
+
* towards the limit, and the window resets completely after the period.
|
|
250
|
+
* - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
|
|
251
|
+
* consistent rate of execution over time.
|
|
252
|
+
*
|
|
194
253
|
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
|
|
195
254
|
* need to enforce a hard limit on the number of executions within a time period.
|
|
196
255
|
*
|
|
197
256
|
* @example
|
|
198
257
|
* ```ts
|
|
199
|
-
* // Rate limit to 5 calls per minute
|
|
258
|
+
* // Rate limit to 5 calls per minute with a sliding window
|
|
200
259
|
* const rateLimited = rateLimit(makeApiCall, {
|
|
201
260
|
* limit: 5,
|
|
202
261
|
* window: 60000,
|
|
262
|
+
* windowType: 'sliding',
|
|
203
263
|
* onReject: (rateLimiter) => {
|
|
204
264
|
* console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
|
|
205
265
|
* }
|
|
@@ -215,7 +275,7 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
215
275
|
*/
|
|
216
276
|
export function rateLimit<TFn extends AnyFunction>(
|
|
217
277
|
fn: TFn,
|
|
218
|
-
initialOptions:
|
|
278
|
+
initialOptions: RateLimiterOptions<TFn>,
|
|
219
279
|
) {
|
|
220
280
|
const rateLimiter = new RateLimiter(fn, initialOptions)
|
|
221
281
|
return rateLimiter.maybeExecute.bind(rateLimiter)
|
package/src/throttler.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
1
2
|
import type { AnyFunction } from './types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -6,9 +7,10 @@ import type { AnyFunction } from './types'
|
|
|
6
7
|
export interface ThrottlerOptions<TFn extends AnyFunction> {
|
|
7
8
|
/**
|
|
8
9
|
* Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
10
|
+
* Can be a boolean or a function that returns a boolean.
|
|
9
11
|
* Defaults to true.
|
|
10
12
|
*/
|
|
11
|
-
enabled?: boolean
|
|
13
|
+
enabled?: boolean | ((throttler: Throttler<TFn>) => boolean)
|
|
12
14
|
/**
|
|
13
15
|
* Whether to execute on the leading edge of the timeout.
|
|
14
16
|
* Defaults to true.
|
|
@@ -24,9 +26,11 @@ export interface ThrottlerOptions<TFn extends AnyFunction> {
|
|
|
24
26
|
*/
|
|
25
27
|
trailing?: boolean
|
|
26
28
|
/**
|
|
27
|
-
* Time window in milliseconds during which the function can only be executed once
|
|
29
|
+
* Time window in milliseconds during which the function can only be executed once.
|
|
30
|
+
* Can be a number or a function that returns a number.
|
|
31
|
+
* Defaults to 0ms
|
|
28
32
|
*/
|
|
29
|
-
wait: number
|
|
33
|
+
wait: number | ((throttler: Throttler<TFn>) => number)
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
const defaultOptions: Required<ThrottlerOptions<any>> = {
|
|
@@ -101,6 +105,20 @@ export class Throttler<TFn extends AnyFunction> {
|
|
|
101
105
|
return this._options
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Returns the current enabled state of the throttler
|
|
110
|
+
*/
|
|
111
|
+
getEnabled(): boolean {
|
|
112
|
+
return parseFunctionOrValue(this._options.enabled, this)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns the current wait time in milliseconds
|
|
117
|
+
*/
|
|
118
|
+
getWait(): number {
|
|
119
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
120
|
+
}
|
|
121
|
+
|
|
104
122
|
/**
|
|
105
123
|
* Attempts to execute the throttled function. The execution behavior depends on the throttler options:
|
|
106
124
|
*
|
|
@@ -126,9 +144,10 @@ export class Throttler<TFn extends AnyFunction> {
|
|
|
126
144
|
maybeExecute(...args: Parameters<TFn>): void {
|
|
127
145
|
const now = Date.now()
|
|
128
146
|
const timeSinceLastExecution = now - this._lastExecutionTime
|
|
147
|
+
const wait = this.getWait()
|
|
129
148
|
|
|
130
149
|
// Handle leading execution
|
|
131
|
-
if (this._options.leading && timeSinceLastExecution >=
|
|
150
|
+
if (this._options.leading && timeSinceLastExecution >= wait) {
|
|
132
151
|
this.executeFunction(...args)
|
|
133
152
|
} else {
|
|
134
153
|
// Store the most recent arguments for potential trailing execution
|
|
@@ -139,7 +158,7 @@ export class Throttler<TFn extends AnyFunction> {
|
|
|
139
158
|
const _timeSinceLastExecution = this._lastExecutionTime
|
|
140
159
|
? now - this._lastExecutionTime
|
|
141
160
|
: 0
|
|
142
|
-
const timeoutDuration =
|
|
161
|
+
const timeoutDuration = wait - _timeSinceLastExecution
|
|
143
162
|
this._timeoutId = setTimeout(() => {
|
|
144
163
|
if (this._lastArgs !== undefined) {
|
|
145
164
|
this.executeFunction(...this._lastArgs)
|
|
@@ -150,7 +169,7 @@ export class Throttler<TFn extends AnyFunction> {
|
|
|
150
169
|
}
|
|
151
170
|
|
|
152
171
|
private executeFunction(...args: Parameters<TFn>): void {
|
|
153
|
-
if (!this.
|
|
172
|
+
if (!this.getEnabled()) return
|
|
154
173
|
this.fn(...args) // EXECUTE!
|
|
155
174
|
this._executionCount++
|
|
156
175
|
this._lastExecutionTime = Date.now()
|
|
@@ -187,7 +206,7 @@ export class Throttler<TFn extends AnyFunction> {
|
|
|
187
206
|
* Returns the next execution time
|
|
188
207
|
*/
|
|
189
208
|
getNextExecutionTime(): number {
|
|
190
|
-
return this._lastExecutionTime + this.
|
|
209
|
+
return this._lastExecutionTime + this.getWait()
|
|
191
210
|
}
|
|
192
211
|
|
|
193
212
|
/**
|
|
@@ -201,7 +220,7 @@ export class Throttler<TFn extends AnyFunction> {
|
|
|
201
220
|
* Returns `true` if there is a pending execution
|
|
202
221
|
*/
|
|
203
222
|
getIsPending(): boolean {
|
|
204
|
-
return this.
|
|
223
|
+
return this.getEnabled() && !!this._timeoutId
|
|
205
224
|
}
|
|
206
225
|
}
|
|
207
226
|
|
|
@@ -233,7 +252,7 @@ export class Throttler<TFn extends AnyFunction> {
|
|
|
233
252
|
*/
|
|
234
253
|
export function throttle<TFn extends AnyFunction>(
|
|
235
254
|
fn: TFn,
|
|
236
|
-
initialOptions:
|
|
255
|
+
initialOptions: ThrottlerOptions<TFn>,
|
|
237
256
|
) {
|
|
238
257
|
const throttler = new Throttler(fn, initialOptions)
|
|
239
258
|
return throttler.maybeExecute.bind(throttler)
|
package/src/utils.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
|
+
import type { AnyFunction } from './types'
|
|
2
|
+
|
|
3
|
+
export function isFunction<T extends AnyFunction>(value: any): value is T {
|
|
4
|
+
return typeof value === 'function'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function parseFunctionOrValue<T, TArgs extends Array<any>>(
|
|
8
|
+
value: T | ((...args: TArgs) => T),
|
|
9
|
+
...args: TArgs
|
|
10
|
+
): T {
|
|
11
|
+
return isFunction(value) ? value(...args) : value
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
export function bindInstanceMethods<T extends Record<string, any>>(
|
|
2
15
|
instance: T,
|
|
3
16
|
): T {
|
|
4
|
-
return Object.getOwnPropertyNames(Object.getPrototypeOf(instance))
|
|
5
|
-
|
|
6
|
-
.reduce((acc: any, key) => {
|
|
17
|
+
return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(
|
|
18
|
+
(acc: any, key) => {
|
|
7
19
|
const method = instance[key as keyof T]
|
|
8
|
-
if (
|
|
20
|
+
if (isFunction(method)) {
|
|
9
21
|
acc[key] = method.bind(instance)
|
|
10
22
|
}
|
|
11
23
|
return acc
|
|
12
|
-
},
|
|
24
|
+
},
|
|
25
|
+
instance,
|
|
26
|
+
)
|
|
13
27
|
}
|