@tanstack/pacer 0.2.0 → 0.4.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 +60 -24
  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 +75 -38
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +76 -24
  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 +66 -25
  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 +19 -9
  20. package/dist/cjs/rate-limiter.cjs.map +1 -1
  21. package/dist/cjs/rate-limiter.d.cts +31 -11
  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 +60 -24
  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 +76 -24
  35. package/dist/esm/async-rate-limiter.js +75 -38
  36. package/dist/esm/async-rate-limiter.js.map +1 -1
  37. package/dist/esm/async-throttler.d.ts +66 -25
  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 +31 -11
  47. package/dist/esm/rate-limiter.js +19 -9
  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 +130 -81
  57. package/src/async-queuer.ts +93 -8
  58. package/src/async-rate-limiter.ts +141 -67
  59. package/src/async-throttler.ts +152 -94
  60. package/src/debouncer.ts +26 -33
  61. package/src/queuer.ts +92 -4
  62. package/src/rate-limiter.ts +56 -32
  63. package/src/throttler.ts +45 -53
  64. package/src/types.ts +2 -10
  65. package/src/utils.ts +1 -1
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,23 +16,31 @@ 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
28
+ /**
29
+ * Type of window to use for rate limiting
30
+ * - 'fixed': Uses a fixed window that resets after the window period
31
+ * - 'sliding': Uses a sliding window that allows executions as old ones expire
32
+ * Defaults to 'fixed'
33
+ */
34
+ windowType?: 'fixed' | 'sliding'
31
35
  }
32
36
 
33
- const defaultOptions: Required<RateLimiterOptions<any, any>> = {
37
+ const defaultOptions: Required<RateLimiterOptions<any>> = {
34
38
  enabled: true,
35
39
  limit: 1,
36
40
  onExecute: () => {},
37
41
  onReject: () => {},
38
42
  window: 0,
43
+ windowType: 'fixed',
39
44
  }
40
45
 
41
46
  /**
@@ -45,6 +50,12 @@ const defaultOptions: Required<RateLimiterOptions<any, any>> = {
45
50
  * then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where
46
51
  * all executions happen immediately, followed by a complete block.
47
52
  *
53
+ * The rate limiter supports two types of windows:
54
+ * - 'fixed': A strict window that resets after the window period. All executions within the window count
55
+ * towards the limit, and the window resets completely after the period.
56
+ * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
57
+ * consistent rate of execution over time.
58
+ *
48
59
  * For smoother execution patterns, consider using:
49
60
  * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)
50
61
  * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)
@@ -56,25 +67,22 @@ const defaultOptions: Required<RateLimiterOptions<any, any>> = {
56
67
  * ```ts
57
68
  * const rateLimiter = new RateLimiter(
58
69
  * (id: string) => api.getData(id),
59
- * { limit: 5, window: 1000 } // 5 calls per second
70
+ * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window
60
71
  * );
61
72
  *
62
73
  * // Will execute immediately until limit reached, then block
63
74
  * rateLimiter.maybeExecute('123');
64
75
  * ```
65
76
  */
66
- export class RateLimiter<
67
- TFn extends AnyFunction,
68
- TArgs extends Parameters<TFn>,
69
- > {
77
+ export class RateLimiter<TFn extends AnyFunction> {
70
78
  private _executionCount = 0
71
79
  private _rejectionCount = 0
72
80
  private _executionTimes: Array<number> = []
73
- private _options: RateLimiterOptions<TFn, TArgs>
81
+ private _options: RateLimiterOptions<TFn>
74
82
 
75
83
  constructor(
76
84
  private fn: TFn,
77
- initialOptions: RateLimiterOptions<TFn, TArgs>,
85
+ initialOptions: RateLimiterOptions<TFn>,
78
86
  ) {
79
87
  this._options = {
80
88
  ...defaultOptions,
@@ -86,21 +94,15 @@ export class RateLimiter<
86
94
  * Updates the rate limiter options
87
95
  * Returns the new options state
88
96
  */
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
97
+ setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {
98
+ this._options = { ...this._options, ...newOptions }
97
99
  }
98
100
 
99
101
  /**
100
102
  * Returns the current rate limiter options
101
103
  */
102
- getOptions(): Required<RateLimiterOptions<TFn, TArgs>> {
103
- return this._options as Required<RateLimiterOptions<TFn, TArgs>>
104
+ getOptions(): Required<RateLimiterOptions<TFn>> {
105
+ return this._options as Required<RateLimiterOptions<TFn>>
104
106
  }
105
107
 
106
108
  /**
@@ -118,20 +120,32 @@ export class RateLimiter<
118
120
  * rateLimiter.maybeExecute('arg1', 'arg2'); // false
119
121
  * ```
120
122
  */
121
- maybeExecute(...args: TArgs): boolean {
123
+ maybeExecute(...args: Parameters<TFn>): boolean {
122
124
  this.cleanupOldExecutions()
123
125
 
124
- if (this._executionTimes.length < this._options.limit) {
125
- this.executeFunction(...args)
126
- return true
126
+ if (this._options.windowType === 'sliding') {
127
+ // For sliding window, we can execute if we have capacity in the current window
128
+ if (this._executionTimes.length < this._options.limit) {
129
+ this.executeFunction(...args)
130
+ return true
131
+ }
132
+ } else {
133
+ // For fixed window, we need to check if we're in a new window
134
+ const now = Date.now()
135
+ const oldestExecution = Math.min(...this._executionTimes)
136
+ const isNewWindow = oldestExecution + this._options.window <= now
137
+
138
+ if (isNewWindow || this._executionTimes.length < this._options.limit) {
139
+ this.executeFunction(...args)
140
+ return true
141
+ }
127
142
  }
128
143
 
129
144
  this.rejectFunction()
130
-
131
145
  return false
132
146
  }
133
147
 
134
- private executeFunction(...args: TArgs): void {
148
+ private executeFunction(...args: Parameters<TFn>): void {
135
149
  if (!this._options.enabled) return
136
150
  const now = Date.now()
137
151
  this._executionCount++
@@ -181,6 +195,9 @@ export class RateLimiter<
181
195
  * Returns the number of milliseconds until the next execution will be possible
182
196
  */
183
197
  getMsUntilNextWindow(): number {
198
+ if (this.getRemainingInWindow() > 0) {
199
+ return 0
200
+ }
184
201
  const oldestExecution = Math.min(...this._executionTimes)
185
202
  return oldestExecution + this._options.window - Date.now()
186
203
  }
@@ -203,15 +220,22 @@ export class RateLimiter<
203
220
  * - A throttler ensures even spacing between executions, which can be better for consistent performance
204
221
  * - A debouncer collapses multiple calls into one, which is better for handling bursts of events
205
222
  *
223
+ * The rate limiter supports two types of windows:
224
+ * - 'fixed': A strict window that resets after the window period. All executions within the window count
225
+ * towards the limit, and the window resets completely after the period.
226
+ * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
227
+ * consistent rate of execution over time.
228
+ *
206
229
  * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
207
230
  * need to enforce a hard limit on the number of executions within a time period.
208
231
  *
209
232
  * @example
210
233
  * ```ts
211
- * // Rate limit to 5 calls per minute
234
+ * // Rate limit to 5 calls per minute with a sliding window
212
235
  * const rateLimited = rateLimit(makeApiCall, {
213
236
  * limit: 5,
214
237
  * window: 60000,
238
+ * windowType: 'sliding',
215
239
  * onReject: (rateLimiter) => {
216
240
  * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
217
241
  * }
@@ -227,7 +251,7 @@ export class RateLimiter<
227
251
  */
228
252
  export function rateLimit<TFn extends AnyFunction>(
229
253
  fn: TFn,
230
- initialOptions: Omit<RateLimiterOptions<TFn, Parameters<TFn>>, 'enabled'>,
254
+ initialOptions: Omit<RateLimiterOptions<TFn>, 'enabled'>,
231
255
  ) {
232
256
  const rateLimiter = new RateLimiter(fn, initialOptions)
233
257
  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) => {