@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.
Files changed (74) hide show
  1. package/dist/cjs/async-debouncer.cjs +60 -44
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +37 -24
  4. package/dist/cjs/async-queuer.cjs +149 -125
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +65 -48
  7. package/dist/cjs/async-rate-limiter.cjs +63 -46
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +39 -27
  10. package/dist/cjs/async-throttler.cjs +70 -47
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +43 -25
  13. package/dist/cjs/debouncer.cjs +46 -22
  14. package/dist/cjs/debouncer.cjs.map +1 -1
  15. package/dist/cjs/debouncer.d.cts +25 -11
  16. package/dist/cjs/index.cjs +2 -0
  17. package/dist/cjs/index.cjs.map +1 -1
  18. package/dist/cjs/index.d.cts +2 -0
  19. package/dist/cjs/queuer.cjs +114 -104
  20. package/dist/cjs/queuer.cjs.map +1 -1
  21. package/dist/cjs/queuer.d.cts +53 -40
  22. package/dist/cjs/rate-limiter.cjs +54 -42
  23. package/dist/cjs/rate-limiter.cjs.map +1 -1
  24. package/dist/cjs/rate-limiter.d.cts +37 -45
  25. package/dist/cjs/throttler.cjs +61 -41
  26. package/dist/cjs/throttler.cjs.map +1 -1
  27. package/dist/cjs/throttler.d.cts +35 -22
  28. package/dist/cjs/types.d.cts +12 -0
  29. package/dist/cjs/utils.cjs +13 -0
  30. package/dist/cjs/utils.cjs.map +1 -0
  31. package/dist/cjs/utils.d.cts +1 -0
  32. package/dist/esm/async-debouncer.d.ts +37 -24
  33. package/dist/esm/async-debouncer.js +60 -44
  34. package/dist/esm/async-debouncer.js.map +1 -1
  35. package/dist/esm/async-queuer.d.ts +65 -48
  36. package/dist/esm/async-queuer.js +149 -125
  37. package/dist/esm/async-queuer.js.map +1 -1
  38. package/dist/esm/async-rate-limiter.d.ts +39 -27
  39. package/dist/esm/async-rate-limiter.js +63 -46
  40. package/dist/esm/async-rate-limiter.js.map +1 -1
  41. package/dist/esm/async-throttler.d.ts +43 -25
  42. package/dist/esm/async-throttler.js +70 -47
  43. package/dist/esm/async-throttler.js.map +1 -1
  44. package/dist/esm/debouncer.d.ts +25 -11
  45. package/dist/esm/debouncer.js +46 -22
  46. package/dist/esm/debouncer.js.map +1 -1
  47. package/dist/esm/index.d.ts +2 -0
  48. package/dist/esm/index.js +2 -0
  49. package/dist/esm/index.js.map +1 -1
  50. package/dist/esm/queuer.d.ts +53 -40
  51. package/dist/esm/queuer.js +114 -104
  52. package/dist/esm/queuer.js.map +1 -1
  53. package/dist/esm/rate-limiter.d.ts +37 -45
  54. package/dist/esm/rate-limiter.js +54 -42
  55. package/dist/esm/rate-limiter.js.map +1 -1
  56. package/dist/esm/throttler.d.ts +35 -22
  57. package/dist/esm/throttler.js +61 -41
  58. package/dist/esm/throttler.js.map +1 -1
  59. package/dist/esm/types.d.ts +12 -0
  60. package/dist/esm/utils.d.ts +1 -0
  61. package/dist/esm/utils.js +13 -0
  62. package/dist/esm/utils.js.map +1 -0
  63. package/package.json +8 -1
  64. package/src/async-debouncer.ts +90 -62
  65. package/src/async-queuer.ts +178 -145
  66. package/src/async-rate-limiter.ts +93 -67
  67. package/src/async-throttler.ts +98 -63
  68. package/src/debouncer.ts +71 -35
  69. package/src/index.ts +2 -0
  70. package/src/queuer.ts +135 -118
  71. package/src/rate-limiter.ts +79 -81
  72. package/src/throttler.ts +87 -61
  73. package/src/types.ts +17 -0
  74. package/src/utils.ts +13 -0
@@ -1,9 +1,12 @@
1
- import type { RateLimitRejectionInfo } from './rate-limiter'
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
- * Time window in milliseconds within which the limit applies
20
+ * Optional error handler for when the rate-limited function throws
18
21
  */
19
- window: number
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?: (info: RateLimitRejectionInfo) => void
30
+ onReject?: (rateLimiter: AsyncRateLimiter<TFn, TArgs>) => void
24
31
  /**
25
- * Optional error handler for when the rate-limited function throws
32
+ * Time window in milliseconds within which the limit applies
26
33
  */
27
- onError?: (error: unknown) => void
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 (...args: Array<any>) => Promise<any>,
72
+ TFn extends AnyAsyncFunction,
57
73
  TArgs extends Parameters<TFn>,
58
74
  > {
59
- private executionCount = 0
60
- private rejectionCount = 0
61
- private executionTimes: Array<number> = []
62
- private options: AsyncRateLimiterOptions
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.options = {
69
- enabled: true,
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.options = {
82
- ...this.options,
95
+ newOptions: Partial<AsyncRateLimiterOptions<TFn, TArgs>>,
96
+ ): AsyncRateLimiterOptions<TFn, TArgs> {
97
+ this._options = {
98
+ ...this._options,
83
99
  ...newOptions,
84
100
  }
85
- return this.options
101
+ return this._options
86
102
  }
87
103
 
88
104
  /**
89
- * Returns the number of times the function has been executed
105
+ * Returns the current rate limiter options
90
106
  */
91
- getExecutionCount(): number {
92
- return this.executionCount
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.executionTimes.length < this.options.limit) {
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.options.enabled) return
140
+ if (!this._options.enabled) return
140
141
  const now = Date.now()
141
- this.executionCount++
142
- this.executionTimes.push(now)
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.options.onError) {
148
+ if (this._options.onError) {
148
149
  try {
149
- this.options.onError(error)
150
- } catch {
151
- // Ignore errors from error handler
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.rejectionCount++
161
- if (this.options.onReject) {
162
- const oldestExecution = Math.min(...this.executionTimes)
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.options.window
178
- this.executionTimes = this.executionTimes.filter(
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.executionTimes = []
188
- this.executionCount = 0
189
- this.rejectionCount = 0
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: ({ msUntilNextWindow }) => {
211
- * console.log(`Rate limit exceeded. Try again in ${msUntilNextWindow}ms`);
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 (...args: Array<any>) => Promise<any>,
225
- >(fn: TFn, initialOptions: Omit<AsyncRateLimiterOptions, 'enabled'>) {
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
  }
@@ -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 (...args: Array<any>) => Promise<any>,
60
+ TFn extends AnyAsyncFunction,
51
61
  TArgs extends Parameters<TFn>,
52
62
  > {
53
- private abortController: AbortController | null = null
54
- private executionCount = 0
55
- private isExecuting = false
56
- private isScheduled = false
57
- private lastArgs: TArgs | undefined
58
- private nextExecutionTime = 0
59
- private options: Required<AsyncThrottlerOptions>
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.options = {
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.options = {
79
- ...this.options,
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.options
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
- * Cancels any pending execution
97
+ * Returns the current options
101
98
  */
102
- cancel(): void {
103
- if (this.abortController) {
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.lastArgs = args
117
- if (this.isScheduled) return
118
- this.isScheduled = true
108
+ this._lastArgs = args
109
+ if (this._isPending) return
110
+ this._isPending = true
119
111
 
120
- this.abortController = new AbortController()
121
- const signal = this.abortController.signal
112
+ this._abortController = new AbortController()
113
+ const signal = this._abortController.signal
122
114
 
123
115
  try {
124
- while (this.isExecuting) {
125
- await this.delay(this.options.wait, signal)
116
+ while (this._isExecuting) {
117
+ await this.delay(this._options.wait, signal)
126
118
  }
127
119
 
128
- while (Date.now() < this.nextExecutionTime) {
129
- await this.delay(this.nextExecutionTime - Date.now(), signal)
120
+ while (Date.now() < this._nextExecutionTime) {
121
+ await this.delay(this._nextExecutionTime - Date.now(), signal)
130
122
  }
131
123
 
132
- this.isScheduled = false
133
- this.isExecuting = true
124
+ this._isPending = false
125
+ this._isExecuting = true
134
126
 
135
- await this.executeFunction(...this.lastArgs)
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.options.onError(error)
133
+ this._options.onError(error)
142
134
  } catch {
143
135
  // Ignore errors from error handler
144
136
  }
145
137
  } finally {
146
- this.nextExecutionTime = Date.now() + this.options.wait
147
- this.isExecuting = false
148
- this.abortController = null
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.options.enabled) return
168
- this.executionCount++
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 (...args: Array<any>) => Promise<any>,
191
- >(fn: TFn, initialOptions: Omit<AsyncThrottlerOptions, 'enabled'>) {
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
- TFn extends (...args: Array<any>) => any,
59
- TArgs extends Parameters<TFn>,
60
- > {
61
- private canLeadingExecute = true
62
- private executionCount = 0
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.options = {
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.options = {
84
- ...this.options,
89
+ newOptions: Partial<DebouncerOptions<TFn, TArgs>>,
90
+ ): Required<DebouncerOptions<TFn, TArgs>> {
91
+ this._options = {
92
+ ...this._options,
85
93
  ...newOptions,
86
94
  }
87
- return this.options
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 number of times the function has been executed
105
+ * Returns the current debouncer options
92
106
  */
93
- getExecutionCount(): number {
94
- return this.executionCount
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.options.leading && this.canLeadingExecute) {
117
+ if (this._options.leading && this._canLeadingExecute) {
104
118
  this.executeFunction(...args)
105
- this.canLeadingExecute = false
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.timeoutId) clearTimeout(this.timeoutId)
128
+ if (this._timeoutId) clearTimeout(this._timeoutId)
110
129
 
111
130
  // Set new timeout that will reset canLeadingExecute
112
- this.timeoutId = setTimeout(() => {
113
- this.canLeadingExecute = true
131
+ this._timeoutId = setTimeout(() => {
132
+ this._canLeadingExecute = true
133
+ this._isPending = false
114
134
  // Execute trailing only if enabled
115
- if (this.options.trailing) {
135
+ if (this._options.trailing) {
116
136
  this.executeFunction(...args)
117
137
  }
118
- }, this.options.wait)
138
+ }, this._options.wait)
119
139
  }
120
140
 
121
141
  private executeFunction(...args: TArgs): void {
122
- if (!this.options.enabled) return
123
- this.executionCount++
124
- this.fn(...args)
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.timeoutId) {
132
- clearTimeout(this.timeoutId)
133
- this.canLeadingExecute = true
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 (...args: Array<any>) => any>(
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)