@tanstack/pacer 0.1.0 → 0.3.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 +112 -63
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +66 -25
- package/dist/cjs/async-queuer.cjs +198 -124
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +91 -49
- package/dist/cjs/async-rate-limiter.cjs +83 -55
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +55 -28
- package/dist/cjs/async-throttler.cjs +121 -70
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +75 -25
- package/dist/cjs/debouncer.cjs +45 -23
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +27 -12
- 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 +161 -101
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +80 -38
- package/dist/cjs/rate-limiter.cjs +52 -44
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +38 -46
- package/dist/cjs/throttler.cjs +57 -44
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +35 -23
- package/dist/cjs/types.d.cts +8 -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 +66 -25
- package/dist/esm/async-debouncer.js +112 -63
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +91 -49
- package/dist/esm/async-queuer.js +198 -124
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +55 -28
- package/dist/esm/async-rate-limiter.js +83 -55
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +75 -25
- package/dist/esm/async-throttler.js +121 -70
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/debouncer.d.ts +27 -12
- package/dist/esm/debouncer.js +45 -23
- 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 +80 -38
- package/dist/esm/queuer.js +161 -101
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +38 -46
- package/dist/esm/rate-limiter.js +52 -44
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +35 -23
- package/dist/esm/throttler.js +57 -44
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/types.d.ts +8 -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 +157 -88
- package/src/async-queuer.ts +266 -148
- package/src/async-rate-limiter.ts +123 -83
- package/src/async-throttler.ts +173 -89
- package/src/debouncer.ts +71 -42
- package/src/index.ts +2 -0
- package/src/queuer.ts +219 -114
- package/src/rate-limiter.ts +74 -88
- package/src/throttler.ts +83 -65
- package/src/types.ts +9 -0
- package/src/utils.ts +13 -0
|
@@ -1,9 +1,9 @@
|
|
|
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<TFn extends AnyAsyncFunction> {
|
|
7
7
|
/**
|
|
8
8
|
* Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
9
9
|
* Defaults to true.
|
|
@@ -14,17 +14,38 @@ export interface AsyncRateLimiterOptions {
|
|
|
14
14
|
*/
|
|
15
15
|
limit: number
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Optional error handler for when the rate-limited function throws
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
onError?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void
|
|
20
|
+
/**
|
|
21
|
+
* Optional function to call when the rate-limited function is executed
|
|
22
|
+
*/
|
|
23
|
+
onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void
|
|
24
|
+
/**
|
|
25
|
+
* Optional function to call when the rate-limited function is executed
|
|
26
|
+
*/
|
|
27
|
+
onSuccess?: (
|
|
28
|
+
result: ReturnType<TFn>,
|
|
29
|
+
rateLimiter: AsyncRateLimiter<TFn>,
|
|
30
|
+
) => void
|
|
20
31
|
/**
|
|
21
32
|
* Optional callback function that is called when an execution is rejected due to rate limiting
|
|
22
33
|
*/
|
|
23
|
-
onReject?: (
|
|
34
|
+
onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void
|
|
24
35
|
/**
|
|
25
|
-
*
|
|
36
|
+
* Time window in milliseconds within which the limit applies
|
|
26
37
|
*/
|
|
27
|
-
|
|
38
|
+
window: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const defaultOptions: Required<
|
|
42
|
+
Omit<AsyncRateLimiterOptions<any>, 'limit' | 'window'>
|
|
43
|
+
> = {
|
|
44
|
+
enabled: true,
|
|
45
|
+
onError: () => {},
|
|
46
|
+
onReject: () => {},
|
|
47
|
+
onSettled: () => {},
|
|
48
|
+
onSuccess: () => {},
|
|
28
49
|
}
|
|
29
50
|
|
|
30
51
|
/**
|
|
@@ -52,21 +73,21 @@ export interface AsyncRateLimiterOptions {
|
|
|
52
73
|
* await rateLimiter.maybeExecute('123');
|
|
53
74
|
* ```
|
|
54
75
|
*/
|
|
55
|
-
export class AsyncRateLimiter<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
>
|
|
59
|
-
private
|
|
60
|
-
private
|
|
61
|
-
private
|
|
62
|
-
private
|
|
76
|
+
export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
77
|
+
private _options: AsyncRateLimiterOptions<TFn>
|
|
78
|
+
private _errorCount = 0
|
|
79
|
+
private _executionTimes: Array<number> = []
|
|
80
|
+
private _lastResult: ReturnType<TFn> | undefined
|
|
81
|
+
private _rejectionCount = 0
|
|
82
|
+
private _settleCount = 0
|
|
83
|
+
private _successCount = 0
|
|
63
84
|
|
|
64
85
|
constructor(
|
|
65
86
|
private fn: TFn,
|
|
66
|
-
initialOptions: AsyncRateLimiterOptions
|
|
87
|
+
initialOptions: AsyncRateLimiterOptions<TFn>,
|
|
67
88
|
) {
|
|
68
|
-
this.
|
|
69
|
-
|
|
89
|
+
this._options = {
|
|
90
|
+
...defaultOptions,
|
|
70
91
|
...initialOptions,
|
|
71
92
|
}
|
|
72
93
|
}
|
|
@@ -75,36 +96,15 @@ export class AsyncRateLimiter<
|
|
|
75
96
|
* Updates the rate limiter options
|
|
76
97
|
* Returns the new options state
|
|
77
98
|
*/
|
|
78
|
-
setOptions(
|
|
79
|
-
newOptions
|
|
80
|
-
): AsyncRateLimiterOptions {
|
|
81
|
-
this.options = {
|
|
82
|
-
...this.options,
|
|
83
|
-
...newOptions,
|
|
84
|
-
}
|
|
85
|
-
return this.options
|
|
99
|
+
setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void {
|
|
100
|
+
this._options = { ...this._options, ...newOptions }
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
/**
|
|
89
|
-
* Returns the
|
|
104
|
+
* Returns the current rate limiter options
|
|
90
105
|
*/
|
|
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)
|
|
106
|
+
getOptions(): Required<AsyncRateLimiterOptions<TFn>> {
|
|
107
|
+
return this._options as Required<AsyncRateLimiterOptions<TFn>>
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
@@ -123,70 +123,109 @@ export class AsyncRateLimiter<
|
|
|
123
123
|
* await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected
|
|
124
124
|
* ```
|
|
125
125
|
*/
|
|
126
|
-
async maybeExecute(
|
|
126
|
+
async maybeExecute(
|
|
127
|
+
...args: Parameters<TFn>
|
|
128
|
+
): Promise<ReturnType<TFn> | undefined> {
|
|
127
129
|
this.cleanupOldExecutions()
|
|
128
130
|
|
|
129
|
-
if (this.
|
|
131
|
+
if (this._executionTimes.length < this._options.limit) {
|
|
130
132
|
await this.executeFunction(...args)
|
|
131
|
-
return
|
|
133
|
+
return this._lastResult
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
this.rejectFunction()
|
|
135
|
-
return
|
|
137
|
+
return undefined
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
private async executeFunction(
|
|
139
|
-
|
|
140
|
+
private async executeFunction(
|
|
141
|
+
...args: Parameters<TFn>
|
|
142
|
+
): Promise<ReturnType<TFn> | undefined> {
|
|
143
|
+
if (!this._options.enabled) return
|
|
140
144
|
const now = Date.now()
|
|
141
|
-
this.
|
|
142
|
-
this.executionTimes.push(now)
|
|
145
|
+
this._executionTimes.push(now)
|
|
143
146
|
|
|
144
147
|
try {
|
|
145
|
-
await this.fn(...args)
|
|
148
|
+
this._lastResult = await this.fn(...args)
|
|
149
|
+
this._successCount++
|
|
150
|
+
this._options.onSuccess?.(this._lastResult!, this)
|
|
146
151
|
} catch (error) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.options.onError(error)
|
|
150
|
-
} catch {
|
|
151
|
-
// Ignore errors from error handler
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
throw error
|
|
152
|
+
this._errorCount++
|
|
153
|
+
this._options.onError?.(error, this)
|
|
155
154
|
} finally {
|
|
155
|
+
this._settleCount++
|
|
156
|
+
this._options.onSettled?.(this)
|
|
156
157
|
}
|
|
158
|
+
|
|
159
|
+
return this._lastResult
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
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
|
-
})
|
|
163
|
+
this._rejectionCount++
|
|
164
|
+
if (this._options.onReject) {
|
|
165
|
+
this._options.onReject(this)
|
|
172
166
|
}
|
|
173
167
|
}
|
|
174
168
|
|
|
175
169
|
private cleanupOldExecutions(): void {
|
|
176
170
|
const now = Date.now()
|
|
177
|
-
const windowStart = now - this.
|
|
178
|
-
this.
|
|
171
|
+
const windowStart = now - this._options.window
|
|
172
|
+
this._executionTimes = this._executionTimes.filter(
|
|
179
173
|
(time) => time > windowStart,
|
|
180
174
|
)
|
|
181
175
|
}
|
|
182
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Returns the number of remaining executions allowed in the current window
|
|
179
|
+
*/
|
|
180
|
+
getRemainingInWindow(): number {
|
|
181
|
+
this.cleanupOldExecutions()
|
|
182
|
+
return Math.max(0, this._options.limit - this._executionTimes.length)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Returns the number of milliseconds until the next execution will be possible
|
|
187
|
+
*/
|
|
188
|
+
getMsUntilNextWindow(): number {
|
|
189
|
+
return this.getRemainingInWindow() * this._options.window
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns the number of times the function has been executed
|
|
194
|
+
*/
|
|
195
|
+
getSuccessCount(): number {
|
|
196
|
+
return this._successCount
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Returns the number of times the function has been settled
|
|
201
|
+
*/
|
|
202
|
+
getSettleCount(): number {
|
|
203
|
+
return this._settleCount
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Returns the number of times the function has errored
|
|
208
|
+
*/
|
|
209
|
+
getErrorCount(): number {
|
|
210
|
+
return this._errorCount
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Returns the number of times the function has been rejected
|
|
215
|
+
*/
|
|
216
|
+
getRejectionCount(): number {
|
|
217
|
+
return this._rejectionCount
|
|
218
|
+
}
|
|
219
|
+
|
|
183
220
|
/**
|
|
184
221
|
* Resets the rate limiter state
|
|
185
222
|
*/
|
|
186
223
|
reset(): void {
|
|
187
|
-
this.
|
|
188
|
-
this.
|
|
189
|
-
this.
|
|
224
|
+
this._executionTimes = []
|
|
225
|
+
this._successCount = 0
|
|
226
|
+
this._errorCount = 0
|
|
227
|
+
this._rejectionCount = 0
|
|
228
|
+
this._settleCount = 0
|
|
190
229
|
}
|
|
191
230
|
}
|
|
192
231
|
|
|
@@ -207,8 +246,8 @@ export class AsyncRateLimiter<
|
|
|
207
246
|
* const rateLimited = asyncRateLimit(makeApiCall, {
|
|
208
247
|
* limit: 5,
|
|
209
248
|
* window: 60000,
|
|
210
|
-
* onReject: (
|
|
211
|
-
* console.log(`Rate limit exceeded. Try again in ${
|
|
249
|
+
* onReject: (rateLimiter) => {
|
|
250
|
+
* console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
|
|
212
251
|
* }
|
|
213
252
|
* });
|
|
214
253
|
*
|
|
@@ -220,9 +259,10 @@ export class AsyncRateLimiter<
|
|
|
220
259
|
* const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds
|
|
221
260
|
* ```
|
|
222
261
|
*/
|
|
223
|
-
export function asyncRateLimit<
|
|
224
|
-
|
|
225
|
-
|
|
262
|
+
export function asyncRateLimit<TFn extends AnyAsyncFunction>(
|
|
263
|
+
fn: TFn,
|
|
264
|
+
initialOptions: Omit<AsyncRateLimiterOptions<TFn>, 'enabled'>,
|
|
265
|
+
) {
|
|
226
266
|
const rateLimiter = new AsyncRateLimiter(fn, initialOptions)
|
|
227
267
|
return rateLimiter.maybeExecute.bind(rateLimiter)
|
|
228
268
|
}
|
package/src/async-throttler.ts
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
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<TFn extends AnyAsyncFunction> {
|
|
5
7
|
/**
|
|
6
8
|
* Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
7
9
|
* Defaults to true.
|
|
8
10
|
*/
|
|
9
11
|
enabled?: boolean
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
* Defaults to
|
|
13
|
+
* Whether to execute the function immediately when called
|
|
14
|
+
* Defaults to true
|
|
13
15
|
*/
|
|
14
|
-
|
|
16
|
+
leading?: boolean
|
|
15
17
|
/**
|
|
16
18
|
* Optional error handler for when the throttled function throws
|
|
17
19
|
*/
|
|
18
|
-
onError?: (error: unknown) => void
|
|
20
|
+
onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void
|
|
21
|
+
/**
|
|
22
|
+
* Optional function to call when the throttled function is executed
|
|
23
|
+
*/
|
|
24
|
+
onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void
|
|
25
|
+
/**
|
|
26
|
+
* Optional function to call when the throttled function is executed
|
|
27
|
+
*/
|
|
28
|
+
onSuccess?: (
|
|
29
|
+
result: ReturnType<TFn>,
|
|
30
|
+
asyncThrottler: AsyncThrottler<TFn>,
|
|
31
|
+
) => void
|
|
32
|
+
/**
|
|
33
|
+
* Whether to execute the function on the trailing edge of the wait period
|
|
34
|
+
* Defaults to true
|
|
35
|
+
*/
|
|
36
|
+
trailing?: boolean
|
|
37
|
+
/**
|
|
38
|
+
* Time window in milliseconds during which the function can only be executed once
|
|
39
|
+
* Defaults to 0ms
|
|
40
|
+
*/
|
|
41
|
+
wait: number
|
|
19
42
|
}
|
|
20
43
|
|
|
21
|
-
const defaultOptions: Required<AsyncThrottlerOptions
|
|
44
|
+
const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
|
|
22
45
|
enabled: true,
|
|
46
|
+
leading: true,
|
|
23
47
|
onError: () => {},
|
|
48
|
+
onSettled: () => {},
|
|
49
|
+
onSuccess: () => {},
|
|
50
|
+
trailing: true,
|
|
24
51
|
wait: 0,
|
|
25
52
|
}
|
|
26
53
|
|
|
@@ -46,23 +73,24 @@ const defaultOptions: Required<AsyncThrottlerOptions> = {
|
|
|
46
73
|
* });
|
|
47
74
|
* ```
|
|
48
75
|
*/
|
|
49
|
-
export class AsyncThrottler<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
private
|
|
55
|
-
private
|
|
56
|
-
private
|
|
57
|
-
private
|
|
58
|
-
private
|
|
59
|
-
private
|
|
76
|
+
export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
77
|
+
private _options: Required<AsyncThrottlerOptions<TFn>>
|
|
78
|
+
private _abortController: AbortController | null = null
|
|
79
|
+
private _errorCount = 0
|
|
80
|
+
private _isExecuting = false
|
|
81
|
+
private _lastArgs: Parameters<TFn> | undefined
|
|
82
|
+
private _lastExecutionTime = 0
|
|
83
|
+
private _lastResult: ReturnType<TFn> | undefined
|
|
84
|
+
private _nextExecutionTime = 0
|
|
85
|
+
private _settleCount = 0
|
|
86
|
+
private _successCount = 0
|
|
87
|
+
private _timeoutId: NodeJS.Timeout | null = null
|
|
60
88
|
|
|
61
89
|
constructor(
|
|
62
90
|
private fn: TFn,
|
|
63
|
-
initialOptions: AsyncThrottlerOptions
|
|
91
|
+
initialOptions: AsyncThrottlerOptions<TFn>,
|
|
64
92
|
) {
|
|
65
|
-
this.
|
|
93
|
+
this._options = {
|
|
66
94
|
...defaultOptions,
|
|
67
95
|
...initialOptions,
|
|
68
96
|
}
|
|
@@ -72,101 +100,156 @@ export class AsyncThrottler<
|
|
|
72
100
|
* Updates the throttler options
|
|
73
101
|
* Returns the new options state
|
|
74
102
|
*/
|
|
75
|
-
setOptions(
|
|
76
|
-
newOptions
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {
|
|
104
|
+
this._options = { ...this._options, ...newOptions }
|
|
105
|
+
|
|
106
|
+
// End the pending state if the debouncer is disabled
|
|
107
|
+
if (!this._options.enabled) {
|
|
108
|
+
this.cancel()
|
|
81
109
|
}
|
|
82
|
-
return this.options
|
|
83
110
|
}
|
|
84
111
|
|
|
85
112
|
/**
|
|
86
|
-
* Returns the
|
|
113
|
+
* Returns the current options
|
|
87
114
|
*/
|
|
88
|
-
|
|
89
|
-
return this.
|
|
115
|
+
getOptions(): Required<AsyncThrottlerOptions<TFn>> {
|
|
116
|
+
return this._options
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
/**
|
|
93
|
-
*
|
|
120
|
+
* Attempts to execute the throttled function
|
|
121
|
+
* If a call is already in progress, it may be blocked or queued depending on the `wait` option
|
|
94
122
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
123
|
+
async maybeExecute(
|
|
124
|
+
...args: Parameters<TFn>
|
|
125
|
+
): Promise<ReturnType<TFn> | undefined> {
|
|
126
|
+
const now = Date.now()
|
|
127
|
+
const timeSinceLastExecution = now - this._lastExecutionTime
|
|
128
|
+
|
|
129
|
+
// Handle leading execution
|
|
130
|
+
if (this._options.leading && timeSinceLastExecution >= this._options.wait) {
|
|
131
|
+
await this.executeFunction(...args)
|
|
132
|
+
return this._lastResult
|
|
133
|
+
} else {
|
|
134
|
+
// Store the most recent arguments for potential trailing execution
|
|
135
|
+
this._lastArgs = args
|
|
136
|
+
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
// Clear any existing timeout to ensure we use the latest arguments
|
|
139
|
+
if (this._timeoutId) {
|
|
140
|
+
clearTimeout(this._timeoutId)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Set up trailing execution if enabled
|
|
144
|
+
if (this._options.trailing) {
|
|
145
|
+
const _timeSinceLastExecution = this._lastExecutionTime
|
|
146
|
+
? now - this._lastExecutionTime
|
|
147
|
+
: 0
|
|
148
|
+
const timeoutDuration = this._options.wait - _timeSinceLastExecution
|
|
149
|
+
this._timeoutId = setTimeout(async () => {
|
|
150
|
+
if (this._lastArgs !== undefined) {
|
|
151
|
+
await this.executeFunction(...this._lastArgs)
|
|
152
|
+
}
|
|
153
|
+
resolve(this._lastResult)
|
|
154
|
+
}, timeoutDuration)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async executeFunction(
|
|
161
|
+
...args: Parameters<TFn>
|
|
162
|
+
): Promise<ReturnType<TFn> | undefined> {
|
|
163
|
+
if (!this._options.enabled || this._isExecuting) return undefined
|
|
164
|
+
this._abortController = new AbortController()
|
|
165
|
+
try {
|
|
166
|
+
this._isExecuting = true
|
|
167
|
+
this._lastResult = await this.fn(...args) // EXECUTE!
|
|
168
|
+
this._successCount++
|
|
169
|
+
this._options.onSuccess(this._lastResult!, this)
|
|
170
|
+
} catch (error) {
|
|
171
|
+
this._errorCount++
|
|
172
|
+
this._options.onError(error, this)
|
|
173
|
+
} finally {
|
|
174
|
+
this._isExecuting = false
|
|
175
|
+
this._settleCount++
|
|
176
|
+
this._abortController = null
|
|
177
|
+
this._lastExecutionTime = Date.now()
|
|
178
|
+
this._nextExecutionTime = this._lastExecutionTime + this._options.wait
|
|
179
|
+
this._options.onSettled(this)
|
|
180
|
+
}
|
|
181
|
+
return this._lastResult
|
|
97
182
|
}
|
|
98
183
|
|
|
99
184
|
/**
|
|
100
|
-
* Cancels any pending execution
|
|
185
|
+
* Cancels any pending execution or aborts any execution in progress
|
|
101
186
|
*/
|
|
102
187
|
cancel(): void {
|
|
103
|
-
if (this.
|
|
104
|
-
this.
|
|
105
|
-
this.
|
|
188
|
+
if (this._timeoutId) {
|
|
189
|
+
clearTimeout(this._timeoutId)
|
|
190
|
+
this._timeoutId = null
|
|
191
|
+
}
|
|
192
|
+
if (this._abortController) {
|
|
193
|
+
this._abortController.abort()
|
|
194
|
+
this._abortController = null
|
|
106
195
|
}
|
|
107
|
-
this.
|
|
108
|
-
this.lastArgs = undefined
|
|
196
|
+
this._lastArgs = undefined
|
|
109
197
|
}
|
|
110
198
|
|
|
111
199
|
/**
|
|
112
|
-
*
|
|
113
|
-
* If a call is already in progress, it may be blocked or queued depending on the `wait` option
|
|
200
|
+
* Returns the last execution time
|
|
114
201
|
*/
|
|
115
|
-
|
|
116
|
-
this.
|
|
117
|
-
|
|
118
|
-
this.isScheduled = true
|
|
202
|
+
getLastExecutionTime(): number {
|
|
203
|
+
return this._lastExecutionTime
|
|
204
|
+
}
|
|
119
205
|
|
|
120
|
-
|
|
121
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Returns the next execution time
|
|
208
|
+
*/
|
|
209
|
+
getNextExecutionTime(): number {
|
|
210
|
+
return this._nextExecutionTime
|
|
211
|
+
}
|
|
122
212
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Returns the last result of the debounced function
|
|
215
|
+
*/
|
|
216
|
+
getLastResult(): ReturnType<TFn> | undefined {
|
|
217
|
+
return this._lastResult
|
|
218
|
+
}
|
|
127
219
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Returns the number of times the function has been executed successfully
|
|
222
|
+
*/
|
|
223
|
+
getSuccessCount(): number {
|
|
224
|
+
return this._successCount
|
|
225
|
+
}
|
|
131
226
|
|
|
132
|
-
|
|
133
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Returns the number of times the function has settled (completed or errored)
|
|
229
|
+
*/
|
|
230
|
+
getSettleCount(): number {
|
|
231
|
+
return this._settleCount
|
|
232
|
+
}
|
|
134
233
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
this.options.onError(error)
|
|
142
|
-
} catch {
|
|
143
|
-
// Ignore errors from error handler
|
|
144
|
-
}
|
|
145
|
-
} finally {
|
|
146
|
-
this.nextExecutionTime = Date.now() + this.options.wait
|
|
147
|
-
this.isExecuting = false
|
|
148
|
-
this.abortController = null
|
|
149
|
-
}
|
|
234
|
+
/**
|
|
235
|
+
* Returns the number of times the function has errored
|
|
236
|
+
*/
|
|
237
|
+
getErrorCount(): number {
|
|
238
|
+
return this._errorCount
|
|
150
239
|
}
|
|
151
240
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
() => {
|
|
158
|
-
clearTimeout(timeout)
|
|
159
|
-
reject(new Error('AbortError'))
|
|
160
|
-
},
|
|
161
|
-
{ once: true },
|
|
162
|
-
)
|
|
163
|
-
})
|
|
241
|
+
/**
|
|
242
|
+
* Returns the current pending state
|
|
243
|
+
*/
|
|
244
|
+
getIsPending(): boolean {
|
|
245
|
+
return this._options.enabled && !!this._timeoutId
|
|
164
246
|
}
|
|
165
247
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Returns the current executing state
|
|
250
|
+
*/
|
|
251
|
+
getIsExecuting(): boolean {
|
|
252
|
+
return this._isExecuting
|
|
170
253
|
}
|
|
171
254
|
}
|
|
172
255
|
|
|
@@ -186,9 +269,10 @@ export class AsyncThrottler<
|
|
|
186
269
|
* await throttled(); // Waits 1 second before executing
|
|
187
270
|
* ```
|
|
188
271
|
*/
|
|
189
|
-
export function asyncThrottle<
|
|
190
|
-
|
|
191
|
-
|
|
272
|
+
export function asyncThrottle<TFn extends AnyAsyncFunction>(
|
|
273
|
+
fn: TFn,
|
|
274
|
+
initialOptions: Omit<AsyncThrottlerOptions<TFn>, 'enabled'>,
|
|
275
|
+
) {
|
|
192
276
|
const asyncThrottler = new AsyncThrottler(fn, initialOptions)
|
|
193
277
|
return asyncThrottler.maybeExecute.bind(asyncThrottler)
|
|
194
278
|
}
|