@tanstack/pacer 0.1.0 → 0.2.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 +60 -44
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +37 -24
- package/dist/cjs/async-queuer.cjs +149 -125
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +65 -48
- package/dist/cjs/async-rate-limiter.cjs +63 -46
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +39 -27
- package/dist/cjs/async-throttler.cjs +70 -47
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +43 -25
- package/dist/cjs/debouncer.cjs +46 -22
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +25 -11
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/queuer.cjs +114 -104
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +53 -40
- package/dist/cjs/rate-limiter.cjs +54 -42
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +37 -45
- package/dist/cjs/throttler.cjs +61 -41
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +35 -22
- package/dist/cjs/types.d.cts +12 -0
- package/dist/cjs/utils.cjs +13 -0
- package/dist/cjs/utils.cjs.map +1 -0
- package/dist/cjs/utils.d.cts +1 -0
- package/dist/esm/async-debouncer.d.ts +37 -24
- package/dist/esm/async-debouncer.js +60 -44
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +65 -48
- package/dist/esm/async-queuer.js +149 -125
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +39 -27
- package/dist/esm/async-rate-limiter.js +63 -46
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +43 -25
- package/dist/esm/async-throttler.js +70 -47
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/debouncer.d.ts +25 -11
- package/dist/esm/debouncer.js +46 -22
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +53 -40
- package/dist/esm/queuer.js +114 -104
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +37 -45
- package/dist/esm/rate-limiter.js +54 -42
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +35 -22
- package/dist/esm/throttler.js +61 -41
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/types.d.ts +12 -0
- package/dist/esm/utils.d.ts +1 -0
- package/dist/esm/utils.js +13 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +8 -1
- package/src/async-debouncer.ts +90 -62
- package/src/async-queuer.ts +178 -145
- package/src/async-rate-limiter.ts +93 -67
- package/src/async-throttler.ts +98 -63
- package/src/debouncer.ts +71 -35
- package/src/index.ts +2 -0
- package/src/queuer.ts +135 -118
- package/src/rate-limiter.ts +79 -81
- package/src/throttler.ts +87 -61
- package/src/types.ts +17 -0
- package/src/utils.ts +13 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AnyAsyncFunction } from './types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for configuring an async rate-limited function
|
|
5
5
|
*/
|
|
6
|
-
export interface AsyncRateLimiterOptions
|
|
6
|
+
export interface AsyncRateLimiterOptions<
|
|
7
|
+
TFn extends AnyAsyncFunction,
|
|
8
|
+
TArgs extends Parameters<TFn>,
|
|
9
|
+
> {
|
|
7
10
|
/**
|
|
8
11
|
* Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
9
12
|
* Defaults to true.
|
|
@@ -14,17 +17,30 @@ export interface AsyncRateLimiterOptions {
|
|
|
14
17
|
*/
|
|
15
18
|
limit: number
|
|
16
19
|
/**
|
|
17
|
-
*
|
|
20
|
+
* Optional error handler for when the rate-limited function throws
|
|
18
21
|
*/
|
|
19
|
-
|
|
22
|
+
onError?: (error: unknown) => void
|
|
23
|
+
/**
|
|
24
|
+
* Optional function to call when the rate-limited function is executed
|
|
25
|
+
*/
|
|
26
|
+
onExecute?: (rateLimiter: AsyncRateLimiter<TFn, TArgs>) => void
|
|
20
27
|
/**
|
|
21
28
|
* Optional callback function that is called when an execution is rejected due to rate limiting
|
|
22
29
|
*/
|
|
23
|
-
onReject?: (
|
|
30
|
+
onReject?: (rateLimiter: AsyncRateLimiter<TFn, TArgs>) => void
|
|
24
31
|
/**
|
|
25
|
-
*
|
|
32
|
+
* Time window in milliseconds within which the limit applies
|
|
26
33
|
*/
|
|
27
|
-
|
|
34
|
+
window: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const defaultOptions: Required<
|
|
38
|
+
Omit<AsyncRateLimiterOptions<any, any>, 'limit' | 'window'>
|
|
39
|
+
> = {
|
|
40
|
+
enabled: true,
|
|
41
|
+
onReject: () => {},
|
|
42
|
+
onError: () => {},
|
|
43
|
+
onExecute: () => {},
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
/**
|
|
@@ -53,20 +69,20 @@ export interface AsyncRateLimiterOptions {
|
|
|
53
69
|
* ```
|
|
54
70
|
*/
|
|
55
71
|
export class AsyncRateLimiter<
|
|
56
|
-
TFn extends
|
|
72
|
+
TFn extends AnyAsyncFunction,
|
|
57
73
|
TArgs extends Parameters<TFn>,
|
|
58
74
|
> {
|
|
59
|
-
private
|
|
60
|
-
private
|
|
61
|
-
private
|
|
62
|
-
private
|
|
75
|
+
private _executionCount = 0
|
|
76
|
+
private _executionTimes: Array<number> = []
|
|
77
|
+
private _options: AsyncRateLimiterOptions<TFn, TArgs>
|
|
78
|
+
private _rejectionCount = 0
|
|
63
79
|
|
|
64
80
|
constructor(
|
|
65
81
|
private fn: TFn,
|
|
66
|
-
initialOptions: AsyncRateLimiterOptions,
|
|
82
|
+
initialOptions: AsyncRateLimiterOptions<TFn, TArgs>,
|
|
67
83
|
) {
|
|
68
|
-
this.
|
|
69
|
-
|
|
84
|
+
this._options = {
|
|
85
|
+
...defaultOptions,
|
|
70
86
|
...initialOptions,
|
|
71
87
|
}
|
|
72
88
|
}
|
|
@@ -76,35 +92,20 @@ export class AsyncRateLimiter<
|
|
|
76
92
|
* Returns the new options state
|
|
77
93
|
*/
|
|
78
94
|
setOptions(
|
|
79
|
-
newOptions: Partial<AsyncRateLimiterOptions
|
|
80
|
-
): AsyncRateLimiterOptions {
|
|
81
|
-
this.
|
|
82
|
-
...this.
|
|
95
|
+
newOptions: Partial<AsyncRateLimiterOptions<TFn, TArgs>>,
|
|
96
|
+
): AsyncRateLimiterOptions<TFn, TArgs> {
|
|
97
|
+
this._options = {
|
|
98
|
+
...this._options,
|
|
83
99
|
...newOptions,
|
|
84
100
|
}
|
|
85
|
-
return this.
|
|
101
|
+
return this._options
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
/**
|
|
89
|
-
* Returns the
|
|
105
|
+
* Returns the current rate limiter options
|
|
90
106
|
*/
|
|
91
|
-
|
|
92
|
-
return this.
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Returns the number of times the function has been rejected
|
|
97
|
-
*/
|
|
98
|
-
getRejectionCount(): number {
|
|
99
|
-
return this.rejectionCount
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Returns the number of remaining executions allowed in the current window
|
|
104
|
-
*/
|
|
105
|
-
getRemainingInWindow(): number {
|
|
106
|
-
this.cleanupOldExecutions()
|
|
107
|
-
return Math.max(0, this.options.limit - this.executionTimes.length)
|
|
107
|
+
getOptions(): Required<AsyncRateLimiterOptions<TFn, TArgs>> {
|
|
108
|
+
return this._options as Required<AsyncRateLimiterOptions<TFn, TArgs>>
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
/**
|
|
@@ -126,7 +127,7 @@ export class AsyncRateLimiter<
|
|
|
126
127
|
async maybeExecute(...args: TArgs): Promise<boolean> {
|
|
127
128
|
this.cleanupOldExecutions()
|
|
128
129
|
|
|
129
|
-
if (this.
|
|
130
|
+
if (this._executionTimes.length < this._options.limit) {
|
|
130
131
|
await this.executeFunction(...args)
|
|
131
132
|
return true
|
|
132
133
|
}
|
|
@@ -136,57 +137,78 @@ export class AsyncRateLimiter<
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
private async executeFunction(...args: TArgs): Promise<void> {
|
|
139
|
-
if (!this.
|
|
140
|
+
if (!this._options.enabled) return
|
|
140
141
|
const now = Date.now()
|
|
141
|
-
this.
|
|
142
|
-
this.
|
|
142
|
+
this._executionCount++
|
|
143
|
+
this._executionTimes.push(now)
|
|
143
144
|
|
|
144
145
|
try {
|
|
145
146
|
await this.fn(...args)
|
|
146
147
|
} catch (error) {
|
|
147
|
-
if (this.
|
|
148
|
+
if (this._options.onError) {
|
|
148
149
|
try {
|
|
149
|
-
this.
|
|
150
|
-
} catch {
|
|
151
|
-
|
|
150
|
+
this._options.onError(error)
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Error in onError handler', error)
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
throw error
|
|
155
156
|
} finally {
|
|
157
|
+
this._options.onExecute?.(this)
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
private rejectFunction(): void {
|
|
160
|
-
this.
|
|
161
|
-
if (this.
|
|
162
|
-
|
|
163
|
-
const msUntilNextWindow =
|
|
164
|
-
oldestExecution + this.options.window - Date.now()
|
|
165
|
-
|
|
166
|
-
this.options.onReject({
|
|
167
|
-
msUntilNextWindow,
|
|
168
|
-
currentExecutions: this.executionTimes.length,
|
|
169
|
-
limit: this.options.limit,
|
|
170
|
-
rejectionCount: this.rejectionCount,
|
|
171
|
-
})
|
|
162
|
+
this._rejectionCount++
|
|
163
|
+
if (this._options.onReject) {
|
|
164
|
+
this._options.onReject(this)
|
|
172
165
|
}
|
|
173
166
|
}
|
|
174
167
|
|
|
175
168
|
private cleanupOldExecutions(): void {
|
|
176
169
|
const now = Date.now()
|
|
177
|
-
const windowStart = now - this.
|
|
178
|
-
this.
|
|
170
|
+
const windowStart = now - this._options.window
|
|
171
|
+
this._executionTimes = this._executionTimes.filter(
|
|
179
172
|
(time) => time > windowStart,
|
|
180
173
|
)
|
|
181
174
|
}
|
|
182
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Returns the number of times the function has been executed
|
|
178
|
+
*/
|
|
179
|
+
getExecutionCount(): number {
|
|
180
|
+
return this._executionCount
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Returns the number of times the function has been rejected
|
|
185
|
+
*/
|
|
186
|
+
getRejectionCount(): number {
|
|
187
|
+
return this._rejectionCount
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Returns the number of remaining executions allowed in the current window
|
|
192
|
+
*/
|
|
193
|
+
getRemainingInWindow(): number {
|
|
194
|
+
this.cleanupOldExecutions()
|
|
195
|
+
return Math.max(0, this._options.limit - this._executionTimes.length)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Returns the number of milliseconds until the next execution will be possible
|
|
200
|
+
*/
|
|
201
|
+
getMsUntilNextWindow(): number {
|
|
202
|
+
return this.getRemainingInWindow() * this._options.window
|
|
203
|
+
}
|
|
204
|
+
|
|
183
205
|
/**
|
|
184
206
|
* Resets the rate limiter state
|
|
185
207
|
*/
|
|
186
208
|
reset(): void {
|
|
187
|
-
this.
|
|
188
|
-
this.
|
|
189
|
-
this.
|
|
209
|
+
this._executionTimes = []
|
|
210
|
+
this._executionCount = 0
|
|
211
|
+
this._rejectionCount = 0
|
|
190
212
|
}
|
|
191
213
|
}
|
|
192
214
|
|
|
@@ -207,8 +229,8 @@ export class AsyncRateLimiter<
|
|
|
207
229
|
* const rateLimited = asyncRateLimit(makeApiCall, {
|
|
208
230
|
* limit: 5,
|
|
209
231
|
* window: 60000,
|
|
210
|
-
* onReject: (
|
|
211
|
-
* console.log(`Rate limit exceeded. Try again in ${
|
|
232
|
+
* onReject: (rateLimiter) => {
|
|
233
|
+
* console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
|
|
212
234
|
* }
|
|
213
235
|
* });
|
|
214
236
|
*
|
|
@@ -221,8 +243,12 @@ export class AsyncRateLimiter<
|
|
|
221
243
|
* ```
|
|
222
244
|
*/
|
|
223
245
|
export function asyncRateLimit<
|
|
224
|
-
TFn extends
|
|
225
|
-
|
|
246
|
+
TFn extends AnyAsyncFunction,
|
|
247
|
+
TArgs extends Parameters<TFn>,
|
|
248
|
+
>(
|
|
249
|
+
fn: TFn,
|
|
250
|
+
initialOptions: Omit<AsyncRateLimiterOptions<TFn, TArgs>, 'enabled'>,
|
|
251
|
+
) {
|
|
226
252
|
const rateLimiter = new AsyncRateLimiter(fn, initialOptions)
|
|
227
253
|
return rateLimiter.maybeExecute.bind(rateLimiter)
|
|
228
254
|
}
|
package/src/async-throttler.ts
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
|
+
import type { AnyAsyncFunction } from './types'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Options for configuring an async throttled function
|
|
3
5
|
*/
|
|
4
|
-
export interface AsyncThrottlerOptions
|
|
6
|
+
export interface AsyncThrottlerOptions<
|
|
7
|
+
TFn extends AnyAsyncFunction,
|
|
8
|
+
TArgs extends Parameters<TFn>,
|
|
9
|
+
> {
|
|
5
10
|
/**
|
|
6
11
|
* Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
7
12
|
* Defaults to true.
|
|
8
13
|
*/
|
|
9
14
|
enabled?: boolean
|
|
15
|
+
/**
|
|
16
|
+
* Optional error handler for when the throttled function throws
|
|
17
|
+
*/
|
|
18
|
+
onError?: (error: unknown) => void
|
|
19
|
+
/**
|
|
20
|
+
* Optional function to call when the throttled function is executed
|
|
21
|
+
*/
|
|
22
|
+
onExecute?: (throttler: AsyncThrottler<TFn, TArgs>) => void
|
|
10
23
|
/**
|
|
11
24
|
* Time window in milliseconds during which the function can only be executed once
|
|
12
25
|
* Defaults to 0ms
|
|
13
26
|
*/
|
|
14
27
|
wait: number
|
|
15
|
-
/**
|
|
16
|
-
* Optional error handler for when the throttled function throws
|
|
17
|
-
*/
|
|
18
|
-
onError?: (error: unknown) => void
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
const defaultOptions: Required<AsyncThrottlerOptions
|
|
30
|
+
const defaultOptions: Required<AsyncThrottlerOptions<any, any>> = {
|
|
22
31
|
enabled: true,
|
|
23
32
|
onError: () => {},
|
|
33
|
+
onExecute: () => {},
|
|
24
34
|
wait: 0,
|
|
25
35
|
}
|
|
26
36
|
|
|
@@ -47,22 +57,23 @@ const defaultOptions: Required<AsyncThrottlerOptions> = {
|
|
|
47
57
|
* ```
|
|
48
58
|
*/
|
|
49
59
|
export class AsyncThrottler<
|
|
50
|
-
TFn extends
|
|
60
|
+
TFn extends AnyAsyncFunction,
|
|
51
61
|
TArgs extends Parameters<TFn>,
|
|
52
62
|
> {
|
|
53
|
-
private
|
|
54
|
-
private
|
|
55
|
-
private
|
|
56
|
-
private
|
|
57
|
-
private
|
|
58
|
-
private
|
|
59
|
-
private
|
|
63
|
+
private _options: Required<AsyncThrottlerOptions<TFn, TArgs>>
|
|
64
|
+
private _abortController: AbortController | null = null
|
|
65
|
+
private _executionCount = 0
|
|
66
|
+
private _isExecuting = false
|
|
67
|
+
private _isPending = false
|
|
68
|
+
private _lastArgs: TArgs | undefined
|
|
69
|
+
private _lastExecutionTime = 0
|
|
70
|
+
private _nextExecutionTime = 0
|
|
60
71
|
|
|
61
72
|
constructor(
|
|
62
73
|
private fn: TFn,
|
|
63
|
-
initialOptions: AsyncThrottlerOptions,
|
|
74
|
+
initialOptions: AsyncThrottlerOptions<TFn, TArgs>,
|
|
64
75
|
) {
|
|
65
|
-
this.
|
|
76
|
+
this._options = {
|
|
66
77
|
...defaultOptions,
|
|
67
78
|
...initialOptions,
|
|
68
79
|
}
|
|
@@ -73,39 +84,20 @@ export class AsyncThrottler<
|
|
|
73
84
|
* Returns the new options state
|
|
74
85
|
*/
|
|
75
86
|
setOptions(
|
|
76
|
-
newOptions: Partial<AsyncThrottlerOptions
|
|
77
|
-
): Required<AsyncThrottlerOptions
|
|
78
|
-
this.
|
|
79
|
-
...this.
|
|
87
|
+
newOptions: Partial<AsyncThrottlerOptions<TFn, TArgs>>,
|
|
88
|
+
): Required<AsyncThrottlerOptions<TFn, TArgs>> {
|
|
89
|
+
this._options = {
|
|
90
|
+
...this._options,
|
|
80
91
|
...newOptions,
|
|
81
92
|
}
|
|
82
|
-
return this.
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Returns the number of times the function has been executed
|
|
87
|
-
*/
|
|
88
|
-
getExecutionCount(): number {
|
|
89
|
-
return this.executionCount
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Returns the next execution time
|
|
94
|
-
*/
|
|
95
|
-
getNextExecutionTime(): number {
|
|
96
|
-
return this.nextExecutionTime
|
|
93
|
+
return this._options
|
|
97
94
|
}
|
|
98
95
|
|
|
99
96
|
/**
|
|
100
|
-
*
|
|
97
|
+
* Returns the current options
|
|
101
98
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this.abortController.abort()
|
|
105
|
-
this.abortController = null
|
|
106
|
-
}
|
|
107
|
-
this.isScheduled = false
|
|
108
|
-
this.lastArgs = undefined
|
|
99
|
+
getOptions(): Required<AsyncThrottlerOptions<TFn, TArgs>> {
|
|
100
|
+
return this._options
|
|
109
101
|
}
|
|
110
102
|
|
|
111
103
|
/**
|
|
@@ -113,39 +105,40 @@ export class AsyncThrottler<
|
|
|
113
105
|
* If a call is already in progress, it may be blocked or queued depending on the `wait` option
|
|
114
106
|
*/
|
|
115
107
|
async maybeExecute(...args: TArgs): Promise<void> {
|
|
116
|
-
this.
|
|
117
|
-
if (this.
|
|
118
|
-
this.
|
|
108
|
+
this._lastArgs = args
|
|
109
|
+
if (this._isPending) return
|
|
110
|
+
this._isPending = true
|
|
119
111
|
|
|
120
|
-
this.
|
|
121
|
-
const signal = this.
|
|
112
|
+
this._abortController = new AbortController()
|
|
113
|
+
const signal = this._abortController.signal
|
|
122
114
|
|
|
123
115
|
try {
|
|
124
|
-
while (this.
|
|
125
|
-
await this.delay(this.
|
|
116
|
+
while (this._isExecuting) {
|
|
117
|
+
await this.delay(this._options.wait, signal)
|
|
126
118
|
}
|
|
127
119
|
|
|
128
|
-
while (Date.now() < this.
|
|
129
|
-
await this.delay(this.
|
|
120
|
+
while (Date.now() < this._nextExecutionTime) {
|
|
121
|
+
await this.delay(this._nextExecutionTime - Date.now(), signal)
|
|
130
122
|
}
|
|
131
123
|
|
|
132
|
-
this.
|
|
133
|
-
this.
|
|
124
|
+
this._isPending = false
|
|
125
|
+
this._isExecuting = true
|
|
134
126
|
|
|
135
|
-
await this.executeFunction(...this.
|
|
127
|
+
await this.executeFunction(...this._lastArgs)
|
|
136
128
|
} catch (error) {
|
|
137
129
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
138
130
|
return // Silent return on cancellation
|
|
139
131
|
}
|
|
140
132
|
try {
|
|
141
|
-
this.
|
|
133
|
+
this._options.onError(error)
|
|
142
134
|
} catch {
|
|
143
135
|
// Ignore errors from error handler
|
|
144
136
|
}
|
|
145
137
|
} finally {
|
|
146
|
-
this.
|
|
147
|
-
this.
|
|
148
|
-
this.
|
|
138
|
+
this._lastExecutionTime = Date.now()
|
|
139
|
+
this._nextExecutionTime = this._lastExecutionTime + this._options.wait
|
|
140
|
+
this._isExecuting = false
|
|
141
|
+
this._abortController = null
|
|
149
142
|
}
|
|
150
143
|
}
|
|
151
144
|
|
|
@@ -164,9 +157,50 @@ export class AsyncThrottler<
|
|
|
164
157
|
}
|
|
165
158
|
|
|
166
159
|
private async executeFunction(...args: TArgs): Promise<void> {
|
|
167
|
-
if (!this.
|
|
168
|
-
this.
|
|
160
|
+
if (!this._options.enabled) return
|
|
161
|
+
this._executionCount++
|
|
169
162
|
await this.fn(...args)
|
|
163
|
+
this._options.onExecute(this)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Cancels any pending execution
|
|
168
|
+
*/
|
|
169
|
+
cancel(): void {
|
|
170
|
+
if (this._abortController) {
|
|
171
|
+
this._abortController.abort()
|
|
172
|
+
this._abortController = null
|
|
173
|
+
}
|
|
174
|
+
this._isPending = false
|
|
175
|
+
this._lastArgs = undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Returns the number of times the function has been executed
|
|
180
|
+
*/
|
|
181
|
+
getExecutionCount(): number {
|
|
182
|
+
return this._executionCount
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Returns the last execution time
|
|
187
|
+
*/
|
|
188
|
+
getLastExecutionTime(): number {
|
|
189
|
+
return this._lastExecutionTime
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns the next execution time
|
|
194
|
+
*/
|
|
195
|
+
getNextExecutionTime(): number {
|
|
196
|
+
return this._nextExecutionTime
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Returns the current pending state
|
|
201
|
+
*/
|
|
202
|
+
getIsPending(): boolean {
|
|
203
|
+
return this._isPending
|
|
170
204
|
}
|
|
171
205
|
}
|
|
172
206
|
|
|
@@ -187,8 +221,9 @@ export class AsyncThrottler<
|
|
|
187
221
|
* ```
|
|
188
222
|
*/
|
|
189
223
|
export function asyncThrottle<
|
|
190
|
-
TFn extends
|
|
191
|
-
|
|
224
|
+
TFn extends AnyAsyncFunction,
|
|
225
|
+
TArgs extends Parameters<TFn>,
|
|
226
|
+
>(fn: TFn, initialOptions: Omit<AsyncThrottlerOptions<TFn, TArgs>, 'enabled'>) {
|
|
192
227
|
const asyncThrottler = new AsyncThrottler(fn, initialOptions)
|
|
193
228
|
return asyncThrottler.maybeExecute.bind(asyncThrottler)
|
|
194
229
|
}
|
package/src/debouncer.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import type { AnyFunction } from './types'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Options for configuring a debounced function
|
|
3
5
|
*/
|
|
4
|
-
export interface DebouncerOptions
|
|
6
|
+
export interface DebouncerOptions<
|
|
7
|
+
TFn extends AnyFunction,
|
|
8
|
+
TArgs extends Parameters<TFn>,
|
|
9
|
+
> {
|
|
5
10
|
/**
|
|
6
11
|
* Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
7
12
|
* Defaults to true.
|
|
@@ -12,6 +17,10 @@ export interface DebouncerOptions {
|
|
|
12
17
|
* Defaults to false.
|
|
13
18
|
*/
|
|
14
19
|
leading?: boolean
|
|
20
|
+
/**
|
|
21
|
+
* Callback function that is called after the function is executed
|
|
22
|
+
*/
|
|
23
|
+
onExecute?: (debouncer: Debouncer<TFn, TArgs>) => void
|
|
15
24
|
/**
|
|
16
25
|
* Whether to execute on the trailing edge of the timeout.
|
|
17
26
|
* Defaults to true.
|
|
@@ -24,11 +33,12 @@ export interface DebouncerOptions {
|
|
|
24
33
|
wait: number
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
const defaultOptions: Required<DebouncerOptions
|
|
36
|
+
const defaultOptions: Required<DebouncerOptions<any, any>> = {
|
|
28
37
|
enabled: true,
|
|
29
38
|
leading: false,
|
|
30
39
|
trailing: true,
|
|
31
40
|
wait: 0,
|
|
41
|
+
onExecute: () => {},
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
/**
|
|
@@ -54,20 +64,18 @@ const defaultOptions: Required<DebouncerOptions> = {
|
|
|
54
64
|
* });
|
|
55
65
|
* ```
|
|
56
66
|
*/
|
|
57
|
-
export class Debouncer<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
private
|
|
63
|
-
private options: Required<DebouncerOptions>
|
|
64
|
-
private timeoutId: NodeJS.Timeout | undefined
|
|
67
|
+
export class Debouncer<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
|
|
68
|
+
private _canLeadingExecute = true
|
|
69
|
+
private _isPending = false
|
|
70
|
+
private _executionCount = 0
|
|
71
|
+
private _options: Required<DebouncerOptions<TFn, TArgs>>
|
|
72
|
+
private _timeoutId: NodeJS.Timeout | undefined
|
|
65
73
|
|
|
66
74
|
constructor(
|
|
67
75
|
private fn: TFn,
|
|
68
|
-
initialOptions: DebouncerOptions,
|
|
76
|
+
initialOptions: DebouncerOptions<TFn, TArgs>,
|
|
69
77
|
) {
|
|
70
|
-
this.
|
|
78
|
+
this._options = {
|
|
71
79
|
...defaultOptions,
|
|
72
80
|
...initialOptions,
|
|
73
81
|
}
|
|
@@ -78,20 +86,26 @@ export class Debouncer<
|
|
|
78
86
|
* Returns the new options state
|
|
79
87
|
*/
|
|
80
88
|
setOptions(
|
|
81
|
-
newOptions: Partial<DebouncerOptions
|
|
82
|
-
): Required<DebouncerOptions
|
|
83
|
-
this.
|
|
84
|
-
...this.
|
|
89
|
+
newOptions: Partial<DebouncerOptions<TFn, TArgs>>,
|
|
90
|
+
): Required<DebouncerOptions<TFn, TArgs>> {
|
|
91
|
+
this._options = {
|
|
92
|
+
...this._options,
|
|
85
93
|
...newOptions,
|
|
86
94
|
}
|
|
87
|
-
|
|
95
|
+
|
|
96
|
+
// End the pending state if the debouncer is disabled
|
|
97
|
+
if (!this._options.enabled) {
|
|
98
|
+
this._isPending = false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return this._options
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
/**
|
|
91
|
-
* Returns the
|
|
105
|
+
* Returns the current debouncer options
|
|
92
106
|
*/
|
|
93
|
-
|
|
94
|
-
return this.
|
|
107
|
+
getOptions(): Required<DebouncerOptions<TFn, TArgs>> {
|
|
108
|
+
return this._options
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
/**
|
|
@@ -100,39 +114,61 @@ export class Debouncer<
|
|
|
100
114
|
*/
|
|
101
115
|
maybeExecute(...args: TArgs): void {
|
|
102
116
|
// Handle leading execution
|
|
103
|
-
if (this.
|
|
117
|
+
if (this._options.leading && this._canLeadingExecute) {
|
|
104
118
|
this.executeFunction(...args)
|
|
105
|
-
this.
|
|
119
|
+
this._canLeadingExecute = false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Start pending state
|
|
123
|
+
if (this._options.leading || this._options.trailing) {
|
|
124
|
+
this._isPending = true
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
// Clear any existing timeout
|
|
109
|
-
if (this.
|
|
128
|
+
if (this._timeoutId) clearTimeout(this._timeoutId)
|
|
110
129
|
|
|
111
130
|
// Set new timeout that will reset canLeadingExecute
|
|
112
|
-
this.
|
|
113
|
-
this.
|
|
131
|
+
this._timeoutId = setTimeout(() => {
|
|
132
|
+
this._canLeadingExecute = true
|
|
133
|
+
this._isPending = false
|
|
114
134
|
// Execute trailing only if enabled
|
|
115
|
-
if (this.
|
|
135
|
+
if (this._options.trailing) {
|
|
116
136
|
this.executeFunction(...args)
|
|
117
137
|
}
|
|
118
|
-
}, this.
|
|
138
|
+
}, this._options.wait)
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
private executeFunction(...args: TArgs): void {
|
|
122
|
-
if (!this.
|
|
123
|
-
this.
|
|
124
|
-
this.
|
|
142
|
+
if (!this._options.enabled) return
|
|
143
|
+
this.fn(...args) // EXECUTE!
|
|
144
|
+
this._executionCount++
|
|
145
|
+
this._options.onExecute(this)
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
/**
|
|
128
149
|
* Cancels any pending execution
|
|
129
150
|
*/
|
|
130
151
|
cancel(): void {
|
|
131
|
-
if (this.
|
|
132
|
-
clearTimeout(this.
|
|
133
|
-
this.
|
|
152
|
+
if (this._timeoutId) {
|
|
153
|
+
clearTimeout(this._timeoutId)
|
|
154
|
+
this._canLeadingExecute = true
|
|
155
|
+
this._isPending = false
|
|
134
156
|
}
|
|
135
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns the number of times the function has been executed
|
|
161
|
+
*/
|
|
162
|
+
getExecutionCount(): number {
|
|
163
|
+
return this._executionCount
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Returns `true` if debouncing
|
|
168
|
+
*/
|
|
169
|
+
getIsPending(): boolean {
|
|
170
|
+
return this._options.enabled && this._isPending
|
|
171
|
+
}
|
|
136
172
|
}
|
|
137
173
|
|
|
138
174
|
/**
|
|
@@ -155,9 +191,9 @@ export class Debouncer<
|
|
|
155
191
|
* inputElement.addEventListener('input', debounced);
|
|
156
192
|
* ```
|
|
157
193
|
*/
|
|
158
|
-
export function debounce<TFn extends
|
|
194
|
+
export function debounce<TFn extends AnyFunction>(
|
|
159
195
|
fn: TFn,
|
|
160
|
-
initialOptions: Omit<DebouncerOptions, 'enabled'>,
|
|
196
|
+
initialOptions: Omit<DebouncerOptions<TFn, Parameters<TFn>>, 'enabled'>,
|
|
161
197
|
) {
|
|
162
198
|
const debouncer = new Debouncer(fn, initialOptions)
|
|
163
199
|
return debouncer.maybeExecute.bind(debouncer)
|