@tanstack/pacer 0.2.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.
Files changed (65) hide show
  1. package/dist/cjs/async-debouncer.cjs +78 -45
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +44 -16
  4. package/dist/cjs/async-queuer.cjs +53 -3
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +28 -3
  7. package/dist/cjs/async-rate-limiter.cjs +46 -35
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +34 -19
  10. package/dist/cjs/async-throttler.cjs +85 -57
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +49 -17
  13. package/dist/cjs/debouncer.cjs +12 -14
  14. package/dist/cjs/debouncer.cjs.map +1 -1
  15. package/dist/cjs/debouncer.d.cts +10 -9
  16. package/dist/cjs/queuer.cjs +51 -1
  17. package/dist/cjs/queuer.cjs.map +1 -1
  18. package/dist/cjs/queuer.d.cts +30 -1
  19. package/dist/cjs/rate-limiter.cjs +1 -5
  20. package/dist/cjs/rate-limiter.cjs.map +1 -1
  21. package/dist/cjs/rate-limiter.d.cts +9 -9
  22. package/dist/cjs/throttler.cjs +29 -36
  23. package/dist/cjs/throttler.cjs.map +1 -1
  24. package/dist/cjs/throttler.d.cts +16 -17
  25. package/dist/cjs/types.d.cts +2 -6
  26. package/dist/cjs/utils.cjs.map +1 -1
  27. package/dist/cjs/utils.d.cts +1 -1
  28. package/dist/esm/async-debouncer.d.ts +44 -16
  29. package/dist/esm/async-debouncer.js +78 -45
  30. package/dist/esm/async-debouncer.js.map +1 -1
  31. package/dist/esm/async-queuer.d.ts +28 -3
  32. package/dist/esm/async-queuer.js +53 -3
  33. package/dist/esm/async-queuer.js.map +1 -1
  34. package/dist/esm/async-rate-limiter.d.ts +34 -19
  35. package/dist/esm/async-rate-limiter.js +46 -35
  36. package/dist/esm/async-rate-limiter.js.map +1 -1
  37. package/dist/esm/async-throttler.d.ts +49 -17
  38. package/dist/esm/async-throttler.js +85 -57
  39. package/dist/esm/async-throttler.js.map +1 -1
  40. package/dist/esm/debouncer.d.ts +10 -9
  41. package/dist/esm/debouncer.js +12 -14
  42. package/dist/esm/debouncer.js.map +1 -1
  43. package/dist/esm/queuer.d.ts +30 -1
  44. package/dist/esm/queuer.js +51 -1
  45. package/dist/esm/queuer.js.map +1 -1
  46. package/dist/esm/rate-limiter.d.ts +9 -9
  47. package/dist/esm/rate-limiter.js +1 -5
  48. package/dist/esm/rate-limiter.js.map +1 -1
  49. package/dist/esm/throttler.d.ts +16 -17
  50. package/dist/esm/throttler.js +29 -36
  51. package/dist/esm/throttler.js.map +1 -1
  52. package/dist/esm/types.d.ts +2 -6
  53. package/dist/esm/utils.d.ts +1 -1
  54. package/dist/esm/utils.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/async-debouncer.ts +114 -73
  57. package/src/async-queuer.ts +93 -8
  58. package/src/async-rate-limiter.ts +74 -60
  59. package/src/async-throttler.ts +135 -86
  60. package/src/debouncer.ts +26 -33
  61. package/src/queuer.ts +92 -4
  62. package/src/rate-limiter.ts +14 -26
  63. package/src/throttler.ts +45 -53
  64. package/src/types.ts +2 -10
  65. package/src/utils.ts +1 -1
package/src/debouncer.ts CHANGED
@@ -3,10 +3,7 @@ import type { AnyFunction } from './types'
3
3
  /**
4
4
  * Options for configuring a debounced function
5
5
  */
6
- export interface DebouncerOptions<
7
- TFn extends AnyFunction,
8
- TArgs extends Parameters<TFn>,
9
- > {
6
+ export interface DebouncerOptions<TFn extends AnyFunction> {
10
7
  /**
11
8
  * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
12
9
  * Defaults to true.
@@ -14,13 +11,14 @@ export interface DebouncerOptions<
14
11
  enabled?: boolean
15
12
  /**
16
13
  * Whether to execute on the leading edge of the timeout.
14
+ * The first call will execute immediately and the rest will wait the delay.
17
15
  * Defaults to false.
18
16
  */
19
17
  leading?: boolean
20
18
  /**
21
19
  * Callback function that is called after the function is executed
22
20
  */
23
- onExecute?: (debouncer: Debouncer<TFn, TArgs>) => void
21
+ onExecute?: (debouncer: Debouncer<TFn>) => void
24
22
  /**
25
23
  * Whether to execute on the trailing edge of the timeout.
26
24
  * Defaults to true.
@@ -33,12 +31,12 @@ export interface DebouncerOptions<
33
31
  wait: number
34
32
  }
35
33
 
36
- const defaultOptions: Required<DebouncerOptions<any, any>> = {
34
+ const defaultOptions: Required<DebouncerOptions<any>> = {
37
35
  enabled: true,
38
36
  leading: false,
37
+ onExecute: () => {},
39
38
  trailing: true,
40
39
  wait: 0,
41
- onExecute: () => {},
42
40
  }
43
41
 
44
42
  /**
@@ -64,16 +62,16 @@ const defaultOptions: Required<DebouncerOptions<any, any>> = {
64
62
  * });
65
63
  * ```
66
64
  */
67
- export class Debouncer<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
65
+ export class Debouncer<TFn extends AnyFunction> {
68
66
  private _canLeadingExecute = true
69
- private _isPending = false
70
67
  private _executionCount = 0
71
- private _options: Required<DebouncerOptions<TFn, TArgs>>
68
+ private _isPending = false
69
+ private _options: Required<DebouncerOptions<TFn>>
72
70
  private _timeoutId: NodeJS.Timeout | undefined
73
71
 
74
72
  constructor(
75
73
  private fn: TFn,
76
- initialOptions: DebouncerOptions<TFn, TArgs>,
74
+ initialOptions: DebouncerOptions<TFn>,
77
75
  ) {
78
76
  this._options = {
79
77
  ...defaultOptions,
@@ -85,26 +83,19 @@ export class Debouncer<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
85
83
  * Updates the debouncer options
86
84
  * Returns the new options state
87
85
  */
88
- setOptions(
89
- newOptions: Partial<DebouncerOptions<TFn, TArgs>>,
90
- ): Required<DebouncerOptions<TFn, TArgs>> {
91
- this._options = {
92
- ...this._options,
93
- ...newOptions,
94
- }
86
+ setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void {
87
+ this._options = { ...this._options, ...newOptions }
95
88
 
96
89
  // End the pending state if the debouncer is disabled
97
90
  if (!this._options.enabled) {
98
91
  this._isPending = false
99
92
  }
100
-
101
- return this._options
102
93
  }
103
94
 
104
95
  /**
105
96
  * Returns the current debouncer options
106
97
  */
107
- getOptions(): Required<DebouncerOptions<TFn, TArgs>> {
98
+ getOptions(): Required<DebouncerOptions<TFn>> {
108
99
  return this._options
109
100
  }
110
101
 
@@ -112,35 +103,37 @@ export class Debouncer<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
112
103
  * Attempts to execute the debounced function
113
104
  * If a call is already in progress, it will be queued
114
105
  */
115
- maybeExecute(...args: TArgs): void {
106
+ maybeExecute(...args: Parameters<TFn>): void {
107
+ let _didLeadingExecute = false
108
+
116
109
  // Handle leading execution
117
110
  if (this._options.leading && this._canLeadingExecute) {
118
- this.executeFunction(...args)
119
111
  this._canLeadingExecute = false
112
+ _didLeadingExecute = true
113
+ this.executeFunction(...args)
120
114
  }
121
115
 
122
- // Start pending state
123
- if (this._options.leading || this._options.trailing) {
116
+ // Start pending state to indicate that the debouncer is waiting for the trailing edge
117
+ if (this._options.trailing) {
124
118
  this._isPending = true
125
119
  }
126
120
 
127
121
  // Clear any existing timeout
128
122
  if (this._timeoutId) clearTimeout(this._timeoutId)
129
123
 
130
- // Set new timeout that will reset canLeadingExecute
124
+ // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading
131
125
  this._timeoutId = setTimeout(() => {
132
126
  this._canLeadingExecute = true
133
- this._isPending = false
134
- // Execute trailing only if enabled
135
- if (this._options.trailing) {
127
+ if (this._options.trailing && !_didLeadingExecute) {
136
128
  this.executeFunction(...args)
137
129
  }
138
130
  }, this._options.wait)
139
131
  }
140
132
 
141
- private executeFunction(...args: TArgs): void {
142
- if (!this._options.enabled) return
133
+ private executeFunction(...args: Parameters<TFn>): void {
134
+ if (!this._options.enabled) return undefined
143
135
  this.fn(...args) // EXECUTE!
136
+ this._isPending = false
144
137
  this._executionCount++
145
138
  this._options.onExecute(this)
146
139
  }
@@ -193,8 +186,8 @@ export class Debouncer<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
193
186
  */
194
187
  export function debounce<TFn extends AnyFunction>(
195
188
  fn: TFn,
196
- initialOptions: Omit<DebouncerOptions<TFn, Parameters<TFn>>, 'enabled'>,
197
- ) {
189
+ initialOptions: Omit<DebouncerOptions<TFn>, 'enabled'>,
190
+ ): (...args: Parameters<TFn>) => void {
198
191
  const debouncer = new Debouncer(fn, initialOptions)
199
192
  return debouncer.maybeExecute.bind(debouncer)
200
193
  }
package/src/queuer.ts CHANGED
@@ -7,6 +7,16 @@ export interface QueuerOptions<TValue> {
7
7
  * @default 'back'
8
8
  */
9
9
  addItemsTo?: QueuePosition
10
+ /**
11
+ * Maximum time in milliseconds that an item can stay in the queue
12
+ * If not provided, items will never expire
13
+ */
14
+ expirationDuration?: number
15
+ /**
16
+ * Function to determine if an item has expired
17
+ * If provided, this overrides the expirationDuration behavior
18
+ */
19
+ getIsExpired?: (item: TValue, addedAt: number) => boolean
10
20
  /**
11
21
  * Default position to get items from during processing
12
22
  * @default 'front'
@@ -25,6 +35,10 @@ export interface QueuerOptions<TValue> {
25
35
  * Maximum number of items allowed in the queuer
26
36
  */
27
37
  maxSize?: number
38
+ /**
39
+ * Callback fired whenever an item expires in the queuer
40
+ */
41
+ onExpire?: (item: TValue, queuer: Queuer<TValue>) => void
28
42
  /**
29
43
  * Callback fired whenever an item is removed from the queuer
30
44
  */
@@ -55,12 +69,15 @@ const defaultOptions: Required<QueuerOptions<any>> = {
55
69
  addItemsTo: 'back',
56
70
  getItemsFrom: 'front',
57
71
  getPriority: (item) => item?.priority ?? 0,
72
+ getIsExpired: () => false,
73
+ expirationDuration: Infinity,
58
74
  initialItems: [],
59
75
  maxSize: Infinity,
60
76
  onGetNextItem: () => {},
61
77
  onIsRunningChange: () => {},
62
78
  onItemsChange: () => {},
63
79
  onReject: () => {},
80
+ onExpire: () => {},
64
81
  started: false,
65
82
  wait: 0,
66
83
  }
@@ -99,6 +116,11 @@ export type QueuePosition = 'front' | 'back'
99
116
  * - wait: configurable delay between processing items
100
117
  * - onItemsChange/onGetNextItem: callbacks for monitoring queuer state
101
118
  *
119
+ * Supports item expiration to clear stale items from the queuer
120
+ * - expirationDuration: maximum time in milliseconds that an item can stay in the queue
121
+ * - getIsExpired: function to override default expiration behavior
122
+ * - onExpire: callback for when an item expires
123
+ *
102
124
  * @example
103
125
  * ```ts
104
126
  * // FIFO queuer
@@ -122,8 +144,10 @@ export type QueuePosition = 'front' | 'back'
122
144
  export class Queuer<TValue> {
123
145
  private _options: Required<QueuerOptions<TValue>>
124
146
  private _items: Array<TValue> = []
147
+ private _itemTimestamps: Array<number> = []
125
148
  private _executionCount = 0
126
149
  private _rejectionCount = 0
150
+ private _expirationCount = 0
127
151
  private _onItemsChanges: Array<(item: TValue) => void> = []
128
152
  private _running: boolean
129
153
  private _pendingTick = false
@@ -143,11 +167,8 @@ export class Queuer<TValue> {
143
167
  * Updates the queuer options
144
168
  * Returns the new options state
145
169
  */
146
- setOptions(
147
- newOptions: Partial<QueuerOptions<TValue>>,
148
- ): QueuerOptions<TValue> {
170
+ setOptions(newOptions: Partial<QueuerOptions<TValue>>): void {
149
171
  this._options = { ...this._options, ...newOptions }
150
- return this._options
151
172
  }
152
173
 
153
174
  /**
@@ -165,6 +186,10 @@ export class Queuer<TValue> {
165
186
  this._pendingTick = false
166
187
  return
167
188
  }
189
+
190
+ // Check for expired items
191
+ this.checkExpiredItems()
192
+
168
193
  while (!this.getIsEmpty()) {
169
194
  const nextItem = this.getNextItem(this._options.getItemsFrom)
170
195
  if (nextItem === undefined) {
@@ -183,6 +208,56 @@ export class Queuer<TValue> {
183
208
  this._pendingTick = false
184
209
  }
185
210
 
211
+ /**
212
+ * Checks for and removes expired items from the queuer
213
+ */
214
+ private checkExpiredItems() {
215
+ if (
216
+ this._options.expirationDuration === Infinity &&
217
+ this._options.getIsExpired === defaultOptions.getIsExpired
218
+ )
219
+ return
220
+
221
+ const now = Date.now()
222
+ const expiredIndices: Array<number> = []
223
+
224
+ // Find indices of expired items
225
+ for (let i = 0; i < this._items.length; i++) {
226
+ const timestamp = this._itemTimestamps[i]
227
+ if (timestamp === undefined) continue
228
+
229
+ const item = this._items[i]
230
+ if (item === undefined) continue
231
+
232
+ const isExpired =
233
+ this._options.getIsExpired !== defaultOptions.getIsExpired
234
+ ? this._options.getIsExpired(item, timestamp)
235
+ : now - timestamp > this._options.expirationDuration
236
+
237
+ if (isExpired) {
238
+ expiredIndices.push(i)
239
+ }
240
+ }
241
+
242
+ // Remove expired items from back to front to maintain indices
243
+ for (let i = expiredIndices.length - 1; i >= 0; i--) {
244
+ const index = expiredIndices[i]
245
+ if (index === undefined) continue
246
+
247
+ const expiredItem = this._items[index]
248
+ if (expiredItem === undefined) continue
249
+
250
+ this._items.splice(index, 1)
251
+ this._itemTimestamps.splice(index, 1)
252
+ this._expirationCount++
253
+ this._options.onExpire(expiredItem, this)
254
+ }
255
+
256
+ if (expiredIndices.length > 0) {
257
+ this._options.onItemsChange(this)
258
+ }
259
+ }
260
+
186
261
  /**
187
262
  * Stops the queuer from processing items
188
263
  */
@@ -248,15 +323,19 @@ export class Queuer<TValue> {
248
323
 
249
324
  if (insertIndex === -1) {
250
325
  this._items.push(item)
326
+ this._itemTimestamps.push(Date.now())
251
327
  } else {
252
328
  this._items.splice(insertIndex, 0, item)
329
+ this._itemTimestamps.splice(insertIndex, 0, Date.now())
253
330
  }
254
331
  } else {
255
332
  // Default FIFO/LIFO behavior
256
333
  if (position === 'front') {
257
334
  this._items.unshift(item)
335
+ this._itemTimestamps.unshift(Date.now())
258
336
  } else {
259
337
  this._items.push(item)
338
+ this._itemTimestamps.push(Date.now())
260
339
  }
261
340
  }
262
341
 
@@ -288,8 +367,10 @@ export class Queuer<TValue> {
288
367
 
289
368
  if (position === 'front') {
290
369
  item = this._items.shift()
370
+ this._itemTimestamps.shift()
291
371
  } else {
292
372
  item = this._items.pop()
373
+ this._itemTimestamps.pop()
293
374
  }
294
375
 
295
376
  if (item !== undefined) {
@@ -362,6 +443,13 @@ export class Queuer<TValue> {
362
443
  return this._rejectionCount
363
444
  }
364
445
 
446
+ /**
447
+ * Returns the number of items that have expired from the queuer
448
+ */
449
+ getExpirationCount(): number {
450
+ return this._expirationCount
451
+ }
452
+
365
453
  /**
366
454
  * Returns true if the queuer is running
367
455
  */
@@ -3,10 +3,7 @@ import type { AnyFunction } from './types'
3
3
  /**
4
4
  * Options for configuring a rate-limited function
5
5
  */
6
- export interface RateLimiterOptions<
7
- TFn extends AnyFunction,
8
- TArgs extends Parameters<TFn>,
9
- > {
6
+ export interface RateLimiterOptions<TFn extends AnyFunction> {
10
7
  /**
11
8
  * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.
12
9
  * Defaults to true.
@@ -19,18 +16,18 @@ export interface RateLimiterOptions<
19
16
  /**
20
17
  * Callback function that is called after the function is executed
21
18
  */
22
- onExecute?: (rateLimiter: RateLimiter<TFn, TArgs>) => void
19
+ onExecute?: (rateLimiter: RateLimiter<TFn>) => void
23
20
  /**
24
21
  * Optional callback function that is called when an execution is rejected due to rate limiting
25
22
  */
26
- onReject?: (rateLimiter: RateLimiter<TFn, TArgs>) => void
23
+ onReject?: (rateLimiter: RateLimiter<TFn>) => void
27
24
  /**
28
25
  * Time window in milliseconds within which the limit applies
29
26
  */
30
27
  window: number
31
28
  }
32
29
 
33
- const defaultOptions: Required<RateLimiterOptions<any, any>> = {
30
+ const defaultOptions: Required<RateLimiterOptions<any>> = {
34
31
  enabled: true,
35
32
  limit: 1,
36
33
  onExecute: () => {},
@@ -63,18 +60,15 @@ const defaultOptions: Required<RateLimiterOptions<any, any>> = {
63
60
  * rateLimiter.maybeExecute('123');
64
61
  * ```
65
62
  */
66
- export class RateLimiter<
67
- TFn extends AnyFunction,
68
- TArgs extends Parameters<TFn>,
69
- > {
63
+ export class RateLimiter<TFn extends AnyFunction> {
70
64
  private _executionCount = 0
71
65
  private _rejectionCount = 0
72
66
  private _executionTimes: Array<number> = []
73
- private _options: RateLimiterOptions<TFn, TArgs>
67
+ private _options: RateLimiterOptions<TFn>
74
68
 
75
69
  constructor(
76
70
  private fn: TFn,
77
- initialOptions: RateLimiterOptions<TFn, TArgs>,
71
+ initialOptions: RateLimiterOptions<TFn>,
78
72
  ) {
79
73
  this._options = {
80
74
  ...defaultOptions,
@@ -86,21 +80,15 @@ export class RateLimiter<
86
80
  * Updates the rate limiter options
87
81
  * Returns the new options state
88
82
  */
89
- setOptions(
90
- newOptions: Partial<RateLimiterOptions<TFn, TArgs>>,
91
- ): RateLimiterOptions<TFn, TArgs> {
92
- this._options = {
93
- ...this._options,
94
- ...newOptions,
95
- }
96
- return this._options
83
+ setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {
84
+ this._options = { ...this._options, ...newOptions }
97
85
  }
98
86
 
99
87
  /**
100
88
  * Returns the current rate limiter options
101
89
  */
102
- getOptions(): Required<RateLimiterOptions<TFn, TArgs>> {
103
- return this._options as Required<RateLimiterOptions<TFn, TArgs>>
90
+ getOptions(): Required<RateLimiterOptions<TFn>> {
91
+ return this._options as Required<RateLimiterOptions<TFn>>
104
92
  }
105
93
 
106
94
  /**
@@ -118,7 +106,7 @@ export class RateLimiter<
118
106
  * rateLimiter.maybeExecute('arg1', 'arg2'); // false
119
107
  * ```
120
108
  */
121
- maybeExecute(...args: TArgs): boolean {
109
+ maybeExecute(...args: Parameters<TFn>): boolean {
122
110
  this.cleanupOldExecutions()
123
111
 
124
112
  if (this._executionTimes.length < this._options.limit) {
@@ -131,7 +119,7 @@ export class RateLimiter<
131
119
  return false
132
120
  }
133
121
 
134
- private executeFunction(...args: TArgs): void {
122
+ private executeFunction(...args: Parameters<TFn>): void {
135
123
  if (!this._options.enabled) return
136
124
  const now = Date.now()
137
125
  this._executionCount++
@@ -227,7 +215,7 @@ export class RateLimiter<
227
215
  */
228
216
  export function rateLimit<TFn extends AnyFunction>(
229
217
  fn: TFn,
230
- initialOptions: Omit<RateLimiterOptions<TFn, Parameters<TFn>>, 'enabled'>,
218
+ initialOptions: Omit<RateLimiterOptions<TFn>, 'enabled'>,
231
219
  ) {
232
220
  const rateLimiter = new RateLimiter(fn, initialOptions)
233
221
  return rateLimiter.maybeExecute.bind(rateLimiter)
package/src/throttler.ts CHANGED
@@ -3,10 +3,7 @@ import type { AnyFunction } from './types'
3
3
  /**
4
4
  * Options for configuring a throttled function
5
5
  */
6
- export interface ThrottlerOptions<
7
- TFn extends AnyFunction,
8
- TArgs extends Parameters<TFn>,
9
- > {
6
+ export interface ThrottlerOptions<TFn extends AnyFunction> {
10
7
  /**
11
8
  * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
12
9
  * Defaults to true.
@@ -20,7 +17,7 @@ export interface ThrottlerOptions<
20
17
  /**
21
18
  * Callback function that is called after the function is executed
22
19
  */
23
- onExecute?: (throttler: Throttler<TFn, TArgs>) => void
20
+ onExecute?: (throttler: Throttler<TFn>) => void
24
21
  /**
25
22
  * Whether to execute on the trailing edge of the timeout.
26
23
  * Defaults to true.
@@ -32,12 +29,12 @@ export interface ThrottlerOptions<
32
29
  wait: number
33
30
  }
34
31
 
35
- const defaultOptions: Required<ThrottlerOptions<any, any>> = {
32
+ const defaultOptions: Required<ThrottlerOptions<any>> = {
36
33
  enabled: true,
37
34
  leading: true,
35
+ onExecute: () => {},
38
36
  trailing: true,
39
37
  wait: 0,
40
- onExecute: () => {},
41
38
  }
42
39
 
43
40
  /**
@@ -67,17 +64,16 @@ const defaultOptions: Required<ThrottlerOptions<any, any>> = {
67
64
  * throttler.maybeExecute('123'); // Throttled
68
65
  * ```
69
66
  */
70
- export class Throttler<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
67
+ export class Throttler<TFn extends AnyFunction> {
71
68
  private _executionCount = 0
72
- private _lastArgs: TArgs | undefined
69
+ private _lastArgs: Parameters<TFn> | undefined
73
70
  private _lastExecutionTime = 0
74
- private _options: Required<ThrottlerOptions<TFn, TArgs>>
71
+ private _options: Required<ThrottlerOptions<TFn>>
75
72
  private _timeoutId: NodeJS.Timeout | undefined
76
- private _isPending = false
77
73
 
78
74
  constructor(
79
75
  private fn: TFn,
80
- initialOptions: ThrottlerOptions<TFn, TArgs>,
76
+ initialOptions: ThrottlerOptions<TFn>,
81
77
  ) {
82
78
  this._options = {
83
79
  ...defaultOptions,
@@ -89,20 +85,19 @@ export class Throttler<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
89
85
  * Updates the throttler options
90
86
  * Returns the new options state
91
87
  */
92
- setOptions(
93
- newOptions: Partial<ThrottlerOptions<TFn, TArgs>>,
94
- ): Required<ThrottlerOptions<TFn, TArgs>> {
95
- this._options = {
96
- ...this._options,
97
- ...newOptions,
88
+ setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void {
89
+ this._options = { ...this._options, ...newOptions }
90
+
91
+ // End the pending state if the debouncer is disabled
92
+ if (!this._options.enabled) {
93
+ this.cancel()
98
94
  }
99
- return this._options
100
95
  }
101
96
 
102
97
  /**
103
98
  * Returns the current throttler options
104
99
  */
105
- getOptions(): Required<ThrottlerOptions<TFn, TArgs>> {
100
+ getOptions(): Required<ThrottlerOptions<TFn>> {
106
101
  return this._options
107
102
  }
108
103
 
@@ -128,42 +123,40 @@ export class Throttler<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
128
123
  * throttled.maybeExecute('c', 'd');
129
124
  * ```
130
125
  */
131
- maybeExecute(...args: TArgs): void {
126
+ maybeExecute(...args: Parameters<TFn>): void {
132
127
  const now = Date.now()
133
128
  const timeSinceLastExecution = now - this._lastExecutionTime
134
129
 
135
130
  // Handle leading execution
136
- if (timeSinceLastExecution >= this._options.wait) {
137
- if (this._options.leading) {
138
- this.executeFunction(...args)
139
- }
140
- this._lastExecutionTime = now
141
- this._isPending = false
131
+ if (this._options.leading && timeSinceLastExecution >= this._options.wait) {
132
+ this.executeFunction(...args)
142
133
  } else {
143
134
  // Store the most recent arguments for potential trailing execution
144
135
  this._lastArgs = args
145
136
 
146
137
  // Set up trailing execution if not already scheduled
147
138
  if (!this._timeoutId && this._options.trailing) {
148
- this._isPending = true
139
+ const _timeSinceLastExecution = this._lastExecutionTime
140
+ ? now - this._lastExecutionTime
141
+ : 0
142
+ const timeoutDuration = this._options.wait - _timeSinceLastExecution
149
143
  this._timeoutId = setTimeout(() => {
150
- if (this._lastArgs) {
144
+ if (this._lastArgs !== undefined) {
151
145
  this.executeFunction(...this._lastArgs)
152
- this._lastArgs = undefined
153
146
  }
154
- this._lastExecutionTime = Date.now()
155
- this._timeoutId = undefined
156
- this._isPending = false
157
- this._options.onExecute(this)
158
- }, this._options.wait - timeSinceLastExecution)
147
+ }, timeoutDuration)
159
148
  }
160
149
  }
161
150
  }
162
151
 
163
- private executeFunction(...args: TArgs): void {
152
+ private executeFunction(...args: Parameters<TFn>): void {
164
153
  if (!this._options.enabled) return
165
154
  this.fn(...args) // EXECUTE!
166
155
  this._executionCount++
156
+ this._lastExecutionTime = Date.now()
157
+ this._timeoutId = undefined
158
+ this._lastArgs = undefined
159
+ this._options.onExecute(this)
167
160
  }
168
161
 
169
162
  /**
@@ -180,36 +173,35 @@ export class Throttler<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
180
173
  clearTimeout(this._timeoutId)
181
174
  this._timeoutId = undefined
182
175
  this._lastArgs = undefined
183
- this._isPending = false
184
176
  }
185
177
  }
186
178
 
187
179
  /**
188
- * Returns the number of times the function has been executed
180
+ * Returns the last execution time
189
181
  */
190
- getExecutionCount(): number {
191
- return this._executionCount
182
+ getLastExecutionTime(): number {
183
+ return this._lastExecutionTime
192
184
  }
193
185
 
194
186
  /**
195
- * Returns `true` if there is a pending execution
187
+ * Returns the next execution time
196
188
  */
197
- getIsPending(): boolean {
198
- return this._options.enabled && this._isPending
189
+ getNextExecutionTime(): number {
190
+ return this._lastExecutionTime + this._options.wait
199
191
  }
200
192
 
201
193
  /**
202
- * Returns the last execution time
194
+ * Returns the number of times the function has been executed
203
195
  */
204
- getLastExecutionTime(): number {
205
- return this._lastExecutionTime
196
+ getExecutionCount(): number {
197
+ return this._executionCount
206
198
  }
207
199
 
208
200
  /**
209
- * Returns the next execution time
201
+ * Returns `true` if there is a pending execution
210
202
  */
211
- getNextExecutionTime(): number {
212
- return this._lastExecutionTime + this._options.wait
203
+ getIsPending(): boolean {
204
+ return this._options.enabled && !!this._timeoutId
213
205
  }
214
206
  }
215
207
 
@@ -239,10 +231,10 @@ export class Throttler<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
239
231
  * });
240
232
  * ```
241
233
  */
242
- export function throttle<
243
- TFn extends AnyFunction,
244
- TArgs extends Parameters<TFn>,
245
- >(fn: TFn, initialOptions: Omit<ThrottlerOptions<TFn, TArgs>, 'enabled'>) {
234
+ export function throttle<TFn extends AnyFunction>(
235
+ fn: TFn,
236
+ initialOptions: Omit<ThrottlerOptions<TFn>, 'enabled'>,
237
+ ) {
246
238
  const throttler = new Throttler(fn, initialOptions)
247
239
  return throttler.maybeExecute.bind(throttler)
248
240
  }
package/src/types.ts CHANGED
@@ -1,17 +1,9 @@
1
1
  /**
2
2
  * Represents a function that can be called with any arguments and returns any value.
3
- * @template TArgs - The type of the arguments the function can be called with.
4
- * @returns The return value of the function.
5
3
  */
6
- export type AnyFunction<TArgs extends Array<any> = Array<any>> = (
7
- ...args: TArgs
8
- ) => any
4
+ export type AnyFunction = (...args: Array<any>) => any
9
5
 
10
6
  /**
11
7
  * Represents an asynchronous function that can be called with any arguments and returns a promise.
12
- * @template TArgs - The type of the arguments the function can be called with.
13
- * @returns A promise that resolves to the return value of the function.
14
8
  */
15
- export type AnyAsyncFunction<TArgs extends Array<any> = Array<any>> = (
16
- ...args: TArgs
17
- ) => Promise<any>
9
+ export type AnyAsyncFunction = (...args: Array<any>) => Promise<any>
package/src/utils.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export function bindInstanceMethods<T extends Record<string, any>>(
2
2
  instance: T,
3
- ) {
3
+ ): T {
4
4
  return Object.getOwnPropertyNames(Object.getPrototypeOf(instance))
5
5
  .filter((key) => typeof instance[key as keyof T] === 'function')
6
6
  .reduce((acc: any, key) => {