@tanstack/pacer 0.8.0 → 0.9.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 (91) hide show
  1. package/dist/cjs/async-batcher.cjs +163 -0
  2. package/dist/cjs/async-batcher.cjs.map +1 -0
  3. package/dist/cjs/async-batcher.d.cts +273 -0
  4. package/dist/cjs/async-debouncer.cjs +149 -162
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +76 -57
  7. package/dist/cjs/async-queuer.cjs +282 -343
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +121 -100
  10. package/dist/cjs/async-rate-limiter.cjs +128 -185
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +72 -61
  13. package/dist/cjs/async-throttler.cjs +168 -178
  14. package/dist/cjs/async-throttler.cjs.map +1 -1
  15. package/dist/cjs/async-throttler.d.cts +97 -69
  16. package/dist/cjs/batcher.cjs +110 -119
  17. package/dist/cjs/batcher.cjs.map +1 -1
  18. package/dist/cjs/batcher.d.cts +76 -51
  19. package/dist/cjs/debouncer.cjs +97 -85
  20. package/dist/cjs/debouncer.cjs.map +1 -1
  21. package/dist/cjs/debouncer.d.cts +54 -26
  22. package/dist/cjs/index.cjs +3 -6
  23. package/dist/cjs/index.cjs.map +1 -1
  24. package/dist/cjs/index.d.cts +1 -1
  25. package/dist/cjs/queuer.cjs +246 -294
  26. package/dist/cjs/queuer.cjs.map +1 -1
  27. package/dist/cjs/queuer.d.cts +102 -81
  28. package/dist/cjs/rate-limiter.cjs +97 -130
  29. package/dist/cjs/rate-limiter.cjs.map +1 -1
  30. package/dist/cjs/rate-limiter.d.cts +50 -37
  31. package/dist/cjs/throttler.cjs +107 -123
  32. package/dist/cjs/throttler.cjs.map +1 -1
  33. package/dist/cjs/throttler.d.cts +59 -35
  34. package/dist/cjs/utils.cjs +0 -13
  35. package/dist/cjs/utils.cjs.map +1 -1
  36. package/dist/cjs/utils.d.cts +0 -1
  37. package/dist/esm/async-batcher.d.ts +273 -0
  38. package/dist/esm/async-batcher.js +163 -0
  39. package/dist/esm/async-batcher.js.map +1 -0
  40. package/dist/esm/async-debouncer.d.ts +76 -57
  41. package/dist/esm/async-debouncer.js +149 -162
  42. package/dist/esm/async-debouncer.js.map +1 -1
  43. package/dist/esm/async-queuer.d.ts +121 -100
  44. package/dist/esm/async-queuer.js +282 -343
  45. package/dist/esm/async-queuer.js.map +1 -1
  46. package/dist/esm/async-rate-limiter.d.ts +72 -61
  47. package/dist/esm/async-rate-limiter.js +128 -185
  48. package/dist/esm/async-rate-limiter.js.map +1 -1
  49. package/dist/esm/async-throttler.d.ts +97 -69
  50. package/dist/esm/async-throttler.js +168 -178
  51. package/dist/esm/async-throttler.js.map +1 -1
  52. package/dist/esm/batcher.d.ts +76 -51
  53. package/dist/esm/batcher.js +110 -119
  54. package/dist/esm/batcher.js.map +1 -1
  55. package/dist/esm/debouncer.d.ts +54 -26
  56. package/dist/esm/debouncer.js +97 -85
  57. package/dist/esm/debouncer.js.map +1 -1
  58. package/dist/esm/index.d.ts +1 -1
  59. package/dist/esm/index.js +4 -7
  60. package/dist/esm/queuer.d.ts +102 -81
  61. package/dist/esm/queuer.js +246 -294
  62. package/dist/esm/queuer.js.map +1 -1
  63. package/dist/esm/rate-limiter.d.ts +50 -37
  64. package/dist/esm/rate-limiter.js +97 -130
  65. package/dist/esm/rate-limiter.js.map +1 -1
  66. package/dist/esm/throttler.d.ts +59 -35
  67. package/dist/esm/throttler.js +107 -123
  68. package/dist/esm/throttler.js.map +1 -1
  69. package/dist/esm/utils.d.ts +0 -1
  70. package/dist/esm/utils.js +0 -13
  71. package/dist/esm/utils.js.map +1 -1
  72. package/package.json +14 -11
  73. package/src/async-batcher.ts +475 -0
  74. package/src/async-debouncer.ts +201 -121
  75. package/src/async-queuer.ts +337 -216
  76. package/src/async-rate-limiter.ts +176 -136
  77. package/src/async-throttler.ts +233 -139
  78. package/src/batcher.ts +158 -92
  79. package/src/debouncer.ts +135 -52
  80. package/src/index.ts +1 -1
  81. package/src/queuer.ts +348 -226
  82. package/src/rate-limiter.ts +125 -80
  83. package/src/throttler.ts +152 -78
  84. package/src/utils.ts +0 -15
  85. package/dist/cjs/compare.cjs +0 -72
  86. package/dist/cjs/compare.cjs.map +0 -1
  87. package/dist/cjs/compare.d.cts +0 -12
  88. package/dist/esm/compare.d.ts +0 -12
  89. package/dist/esm/compare.js +0 -72
  90. package/dist/esm/compare.js.map +0 -1
  91. package/src/compare.ts +0 -105
@@ -1,6 +1,30 @@
1
+ import { Store } from '@tanstack/store'
1
2
  import { parseFunctionOrValue } from './utils'
2
3
  import type { AnyFunction } from './types'
3
4
 
5
+ export interface RateLimiterState {
6
+ /**
7
+ * Number of function executions that have been completed
8
+ */
9
+ executionCount: number
10
+ /**
11
+ * Array of timestamps when executions occurred for rate limiting calculations
12
+ */
13
+ executionTimes: Array<number>
14
+ /**
15
+ * Number of function executions that have been rejected due to rate limiting
16
+ */
17
+ rejectionCount: number
18
+ }
19
+
20
+ function getDefaultRateLimiterState(): RateLimiterState {
21
+ return structuredClone({
22
+ executionCount: 0,
23
+ executionTimes: [],
24
+ rejectionCount: 0,
25
+ })
26
+ }
27
+
4
28
  /**
5
29
  * Options for configuring a rate-limited function
6
30
  */
@@ -10,6 +34,10 @@ export interface RateLimiterOptions<TFn extends AnyFunction> {
10
34
  * Defaults to true.
11
35
  */
12
36
  enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)
37
+ /**
38
+ * Initial state for the rate limiter
39
+ */
40
+ initialState?: Partial<RateLimiterState>
13
41
  /**
14
42
  * Maximum number of executions allowed within the time window.
15
43
  * Can be a number or a callback function that receives the rate limiter instance and returns a number.
@@ -37,11 +65,12 @@ export interface RateLimiterOptions<TFn extends AnyFunction> {
37
65
  windowType?: 'fixed' | 'sliding'
38
66
  }
39
67
 
40
- const defaultOptions: Required<RateLimiterOptions<any>> = {
68
+ const defaultOptions: Omit<
69
+ Required<RateLimiterOptions<any>>,
70
+ 'initialState' | 'onExecute' | 'onReject'
71
+ > = {
41
72
  enabled: true,
42
73
  limit: 1,
43
- onExecute: () => {},
44
- onReject: () => {},
45
74
  window: 0,
46
75
  windowType: 'fixed',
47
76
  }
@@ -66,11 +95,24 @@ const defaultOptions: Required<RateLimiterOptions<any>> = {
66
95
  * Rate limiting is best used for hard API limits or resource constraints. For UI updates or
67
96
  * smoothing out frequent events, throttling or debouncing usually provide better user experience.
68
97
  *
98
+ * State Management:
99
+ * - Uses TanStack Store for reactive state management
100
+ * - Use `initialState` to provide initial state values when creating the rate limiter
101
+ * - Use `onExecute` callback to react to function execution and implement custom logic
102
+ * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
103
+ * - The state includes execution count, execution times, and rejection count
104
+ * - State can be accessed via `rateLimiter.store.state` when using the class directly
105
+ * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`
106
+ *
69
107
  * @example
70
108
  * ```ts
71
109
  * const rateLimiter = new RateLimiter(
72
110
  * (id: string) => api.getData(id),
73
- * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window
111
+ * {
112
+ * limit: 5,
113
+ * window: 1000,
114
+ * windowType: 'sliding',
115
+ * }
74
116
  * );
75
117
  *
76
118
  * // Will execute immediately until limit reached, then block
@@ -78,54 +120,57 @@ const defaultOptions: Required<RateLimiterOptions<any>> = {
78
120
  * ```
79
121
  */
80
122
  export class RateLimiter<TFn extends AnyFunction> {
81
- private _executionCount = 0
82
- private _rejectionCount = 0
83
- private _executionTimes: Array<number> = []
84
- private _options: RateLimiterOptions<TFn>
123
+ readonly store: Store<Readonly<RateLimiterState>> =
124
+ new Store<RateLimiterState>(getDefaultRateLimiterState())
125
+ options: RateLimiterOptions<TFn>
85
126
 
86
127
  constructor(
87
128
  private fn: TFn,
88
129
  initialOptions: RateLimiterOptions<TFn>,
89
130
  ) {
90
- this._options = {
131
+ this.options = {
91
132
  ...defaultOptions,
92
133
  ...initialOptions,
93
134
  }
135
+ this.#setState(this.options.initialState ?? {})
94
136
  }
95
137
 
96
138
  /**
97
139
  * Updates the rate limiter options
98
140
  */
99
- setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {
100
- this._options = { ...this._options, ...newOptions }
141
+ setOptions = (newOptions: Partial<RateLimiterOptions<TFn>>): void => {
142
+ this.options = { ...this.options, ...newOptions }
101
143
  }
102
144
 
103
- /**
104
- * Returns the current rate limiter options
105
- */
106
- getOptions(): Required<RateLimiterOptions<TFn>> {
107
- return this._options as Required<RateLimiterOptions<TFn>>
145
+ #setState = (newState: Partial<RateLimiterState>): void => {
146
+ this.store.setState((state) => {
147
+ const combinedState = {
148
+ ...state,
149
+ ...newState,
150
+ }
151
+ return combinedState
152
+ })
108
153
  }
109
154
 
110
155
  /**
111
156
  * Returns the current enabled state of the rate limiter
112
157
  */
113
- getEnabled(): boolean {
114
- return parseFunctionOrValue(this._options.enabled, this)!
158
+ #getEnabled = (): boolean => {
159
+ return !!parseFunctionOrValue(this.options.enabled, this)
115
160
  }
116
161
 
117
162
  /**
118
163
  * Returns the current limit of executions allowed within the time window
119
164
  */
120
- getLimit(): number {
121
- return parseFunctionOrValue(this._options.limit, this)
165
+ #getLimit = (): number => {
166
+ return parseFunctionOrValue(this.options.limit, this)
122
167
  }
123
168
 
124
169
  /**
125
170
  * Returns the current time window in milliseconds
126
171
  */
127
- getWindow(): number {
128
- return parseFunctionOrValue(this._options.window, this)
172
+ #getWindow = (): number => {
173
+ return parseFunctionOrValue(this.options.window, this)
129
174
  }
130
175
 
131
176
  /**
@@ -143,95 +188,86 @@ export class RateLimiter<TFn extends AnyFunction> {
143
188
  * rateLimiter.maybeExecute('arg1', 'arg2'); // false
144
189
  * ```
145
190
  */
146
- maybeExecute(...args: Parameters<TFn>): boolean {
147
- this.cleanupOldExecutions()
191
+ maybeExecute = (...args: Parameters<TFn>): boolean => {
192
+ this.#cleanupOldExecutions()
148
193
 
149
- if (this._options.windowType === 'sliding') {
150
- // For sliding window, we can execute if we have capacity in the current window
151
- if (this._executionTimes.length < this.getLimit()) {
152
- this.execute(...args)
153
- return true
154
- }
155
- } else {
156
- // For fixed window, we need to check if we're in a new window
157
- const now = Date.now()
158
- const oldestExecution = Math.min(...this._executionTimes)
159
- const isNewWindow = oldestExecution + this.getWindow() <= now
194
+ const relevantExecutionTimes = this.#getRelevantExecutionTimes()
160
195
 
161
- if (isNewWindow || this._executionTimes.length < this.getLimit()) {
162
- this.execute(...args)
163
- return true
164
- }
196
+ if (relevantExecutionTimes.length < this.#getLimit()) {
197
+ this.#execute(...args)
198
+ return true
165
199
  }
166
200
 
167
- this.rejectFunction()
201
+ this.#setState({
202
+ rejectionCount: this.store.state.rejectionCount + 1,
203
+ })
204
+ this.options.onReject?.(this)
168
205
  return false
169
206
  }
170
207
 
171
- private execute(...args: Parameters<TFn>): void {
172
- if (!this.getEnabled()) return
208
+ #execute = (...args: Parameters<TFn>): void => {
209
+ if (!this.#getEnabled()) return
173
210
  const now = Date.now()
174
- this._executionCount++
175
- this._executionTimes.push(now)
176
- this.fn(...args) // execute the function
177
- this._options.onExecute?.(this)
211
+ this.fn(...args) // EXECUTE!
212
+ this.store.state.executionTimes.push(now) // mutate state directly for performance
213
+ this.#setState({
214
+ executionCount: this.store.state.executionCount + 1,
215
+ })
216
+ this.options.onExecute?.(this)
178
217
  }
179
218
 
180
- private rejectFunction(): void {
181
- this._rejectionCount++
182
- if (this._options.onReject) {
183
- this._options.onReject(this)
219
+ #getRelevantExecutionTimes = (): Array<number> => {
220
+ if (this.options.windowType === 'sliding') {
221
+ // For sliding window, return all executions within the current window
222
+ return this.store.state.executionTimes.filter(
223
+ (time) => time > Date.now() - this.#getWindow(),
224
+ )
225
+ } else {
226
+ // For fixed window, return all executions in the current window
227
+ // The window starts from the oldest execution time
228
+ const oldestExecution = Math.min(...this.store.state.executionTimes)
229
+ const windowStart = oldestExecution
230
+ return this.store.state.executionTimes.filter(
231
+ (time) =>
232
+ time >= windowStart && time <= windowStart + this.#getWindow(),
233
+ )
184
234
  }
185
235
  }
186
236
 
187
- private cleanupOldExecutions(): void {
237
+ #cleanupOldExecutions = (): void => {
188
238
  const now = Date.now()
189
- const windowStart = now - this.getWindow()
190
- this._executionTimes = this._executionTimes.filter(
191
- (time) => time > windowStart,
192
- )
193
- }
194
-
195
- /**
196
- * Returns the number of times the function has been executed
197
- */
198
- getExecutionCount(): number {
199
- return this._executionCount
200
- }
201
-
202
- /**
203
- * Returns the number of times the function has been rejected
204
- */
205
- getRejectionCount(): number {
206
- return this._rejectionCount
239
+ const windowStart = now - this.#getWindow()
240
+ this.#setState({
241
+ executionTimes: this.store.state.executionTimes.filter(
242
+ (time) => time > windowStart,
243
+ ),
244
+ })
207
245
  }
208
246
 
209
247
  /**
210
248
  * Returns the number of remaining executions allowed in the current window
211
249
  */
212
- getRemainingInWindow(): number {
213
- this.cleanupOldExecutions()
214
- return Math.max(0, this.getLimit() - this._executionTimes.length)
250
+ getRemainingInWindow = (): number => {
251
+ const relevantExecutionTimes = this.#getRelevantExecutionTimes()
252
+ return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)
215
253
  }
216
254
 
217
255
  /**
218
256
  * Returns the number of milliseconds until the next execution will be possible
219
257
  */
220
- getMsUntilNextWindow(): number {
258
+ getMsUntilNextWindow = (): number => {
221
259
  if (this.getRemainingInWindow() > 0) {
222
260
  return 0
223
261
  }
224
- const oldestExecution = Math.min(...this._executionTimes)
225
- return oldestExecution + this.getWindow() - Date.now()
262
+ const oldestExecution = this.store.state.executionTimes[0] ?? Infinity
263
+ return oldestExecution + this.#getWindow() - Date.now()
226
264
  }
227
265
 
228
266
  /**
229
267
  * Resets the rate limiter state
230
268
  */
231
- reset(): void {
232
- this._executionTimes = []
233
- this._executionCount = 0
234
- this._rejectionCount = 0
269
+ reset = (): void => {
270
+ this.#setState(getDefaultRateLimiterState())
235
271
  }
236
272
  }
237
273
 
@@ -249,6 +285,15 @@ export class RateLimiter<TFn extends AnyFunction> {
249
285
  * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
250
286
  * consistent rate of execution over time.
251
287
  *
288
+ * State Management:
289
+ * - Uses TanStack Store for reactive state management
290
+ * - Use `initialState` to provide initial state values when creating the rate limiter
291
+ * - Use `onExecute` callback to react to function execution and implement custom logic
292
+ * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
293
+ * - The state includes execution count, execution times, and rejection count
294
+ * - State can be accessed via the underlying RateLimiter instance's `store.state` property
295
+ * - When using framework adapters (React/Solid), state is accessed from the hook's state property
296
+ *
252
297
  * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
253
298
  * need to enforce a hard limit on the number of executions within a time period.
254
299
  *
@@ -277,5 +322,5 @@ export function rateLimit<TFn extends AnyFunction>(
277
322
  initialOptions: RateLimiterOptions<TFn>,
278
323
  ) {
279
324
  const rateLimiter = new RateLimiter(fn, initialOptions)
280
- return rateLimiter.maybeExecute.bind(rateLimiter)
325
+ return rateLimiter.maybeExecute
281
326
  }
package/src/throttler.ts CHANGED
@@ -1,6 +1,47 @@
1
+ import { Store } from '@tanstack/store'
1
2
  import { parseFunctionOrValue } from './utils'
2
3
  import type { AnyFunction } from './types'
3
4
 
5
+ export interface ThrottlerState<TFn extends AnyFunction> {
6
+ /**
7
+ * Number of function executions that have been completed
8
+ */
9
+ executionCount: number
10
+ /**
11
+ * The arguments from the most recent call to maybeExecute
12
+ */
13
+ lastArgs: Parameters<TFn> | undefined
14
+ /**
15
+ * Timestamp of the last function execution in milliseconds
16
+ */
17
+ lastExecutionTime: number
18
+ /**
19
+ * Timestamp when the next execution can occur in milliseconds
20
+ */
21
+ nextExecutionTime: number
22
+ /**
23
+ * Whether the throttler is waiting for the timeout to trigger execution
24
+ */
25
+ isPending: boolean
26
+ /**
27
+ * Current execution status - 'idle' when not active, 'pending' when waiting for timeout
28
+ */
29
+ status: 'disabled' | 'idle' | 'pending'
30
+ }
31
+
32
+ function getDefaultThrottlerState<
33
+ TFn extends AnyFunction,
34
+ >(): ThrottlerState<TFn> {
35
+ return structuredClone({
36
+ executionCount: 0,
37
+ isPending: false,
38
+ lastArgs: undefined,
39
+ lastExecutionTime: 0,
40
+ nextExecutionTime: 0,
41
+ status: 'idle',
42
+ })
43
+ }
44
+
4
45
  /**
5
46
  * Options for configuring a throttled function
6
47
  */
@@ -11,6 +52,10 @@ export interface ThrottlerOptions<TFn extends AnyFunction> {
11
52
  * Defaults to true.
12
53
  */
13
54
  enabled?: boolean | ((throttler: Throttler<TFn>) => boolean)
55
+ /**
56
+ * Initial state for the throttler
57
+ */
58
+ initialState?: Partial<ThrottlerState<TFn>>
14
59
  /**
15
60
  * Whether to execute on the leading edge of the timeout.
16
61
  * Defaults to true.
@@ -33,10 +78,12 @@ export interface ThrottlerOptions<TFn extends AnyFunction> {
33
78
  wait: number | ((throttler: Throttler<TFn>) => number)
34
79
  }
35
80
 
36
- const defaultOptions: Required<ThrottlerOptions<any>> = {
81
+ const defaultOptions: Omit<
82
+ Required<ThrottlerOptions<any>>,
83
+ 'initialState' | 'onExecute'
84
+ > = {
37
85
  enabled: true,
38
86
  leading: true,
39
- onExecute: () => {},
40
87
  trailing: true,
41
88
  wait: 0,
42
89
  }
@@ -54,6 +101,14 @@ const defaultOptions: Required<ThrottlerOptions<any>> = {
54
101
  *
55
102
  * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.
56
103
  *
104
+ * State Management:
105
+ * - Uses TanStack Store for reactive state management
106
+ * - Use `initialState` to provide initial state values when creating the throttler
107
+ * - Use `onExecute` callback to react to function execution and implement custom logic
108
+ * - The state includes execution count, last execution time, pending status, and more
109
+ * - State can be accessed via `throttler.store.state` when using the class directly
110
+ * - When using framework adapters (React/Solid), state is accessed from `throttler.state`
111
+ *
57
112
  * @example
58
113
  * ```ts
59
114
  * const throttler = new Throttler(
@@ -69,53 +124,59 @@ const defaultOptions: Required<ThrottlerOptions<any>> = {
69
124
  * ```
70
125
  */
71
126
  export class Throttler<TFn extends AnyFunction> {
72
- private _executionCount = 0
73
- private _lastArgs: Parameters<TFn> | undefined
74
- private _lastExecutionTime = 0
75
- private _options: Required<ThrottlerOptions<TFn>>
76
- private _timeoutId: NodeJS.Timeout | undefined
127
+ readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(
128
+ getDefaultThrottlerState(),
129
+ )
130
+ options: ThrottlerOptions<TFn>
131
+ #timeoutId: NodeJS.Timeout | undefined
77
132
 
78
133
  constructor(
79
134
  private fn: TFn,
80
135
  initialOptions: ThrottlerOptions<TFn>,
81
136
  ) {
82
- this._options = {
137
+ this.options = {
83
138
  ...defaultOptions,
84
139
  ...initialOptions,
85
140
  }
141
+ this.#setState(this.options.initialState ?? {})
86
142
  }
87
143
 
88
144
  /**
89
145
  * Updates the throttler options
90
146
  */
91
- setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void {
92
- this._options = { ...this._options, ...newOptions }
147
+ setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {
148
+ this.options = { ...this.options, ...newOptions }
93
149
 
94
- // End the pending state if the debouncer is disabled
95
- if (!this._options.enabled) {
150
+ // Cancel pending execution if the throttler is disabled
151
+ if (!this.#getEnabled()) {
96
152
  this.cancel()
97
153
  }
98
154
  }
99
155
 
100
- /**
101
- * Returns the current throttler options
102
- */
103
- getOptions(): Required<ThrottlerOptions<TFn>> {
104
- return this._options
156
+ #setState = (newState: Partial<ThrottlerState<TFn>>): void => {
157
+ this.store.setState((state) => {
158
+ const combinedState = {
159
+ ...state,
160
+ ...newState,
161
+ }
162
+ const { isPending } = combinedState
163
+ return {
164
+ ...combinedState,
165
+ status: !this.#getEnabled()
166
+ ? 'disabled'
167
+ : isPending
168
+ ? 'pending'
169
+ : 'idle',
170
+ }
171
+ })
105
172
  }
106
173
 
107
- /**
108
- * Returns the current enabled state of the throttler
109
- */
110
- getEnabled(): boolean {
111
- return parseFunctionOrValue(this._options.enabled, this)
174
+ #getEnabled = (): boolean => {
175
+ return !!parseFunctionOrValue(this.options.enabled, this)
112
176
  }
113
177
 
114
- /**
115
- * Returns the current wait time in milliseconds
116
- */
117
- getWait(): number {
118
- return parseFunctionOrValue(this._options.wait, this)
178
+ #getWait = (): number => {
179
+ return parseFunctionOrValue(this.options.wait, this)
119
180
  }
120
181
 
121
182
  /**
@@ -140,86 +201,91 @@ export class Throttler<TFn extends AnyFunction> {
140
201
  * throttled.maybeExecute('c', 'd');
141
202
  * ```
142
203
  */
143
- maybeExecute(...args: Parameters<TFn>): void {
204
+ maybeExecute = (...args: Parameters<TFn>): void => {
144
205
  const now = Date.now()
145
- const timeSinceLastExecution = now - this._lastExecutionTime
146
- const wait = this.getWait()
206
+ const timeSinceLastExecution = now - this.store.state.lastExecutionTime
207
+ const wait = this.#getWait()
147
208
 
148
209
  // Handle leading execution
149
- if (this._options.leading && timeSinceLastExecution >= wait) {
150
- this.execute(...args)
210
+ if (this.options.leading && timeSinceLastExecution >= wait) {
211
+ this.#execute(...args)
151
212
  } else {
152
213
  // Store the most recent arguments for potential trailing execution
153
- this._lastArgs = args
154
-
214
+ this.#setState({
215
+ lastArgs: args,
216
+ })
155
217
  // Set up trailing execution if not already scheduled
156
- if (!this._timeoutId && this._options.trailing) {
157
- const _timeSinceLastExecution = this._lastExecutionTime
158
- ? now - this._lastExecutionTime
218
+ if (!this.#timeoutId && this.options.trailing) {
219
+ // prevent large number if lastExecutionTime is undefined
220
+ const _timeSinceLastExecution = this.store.state.lastExecutionTime
221
+ ? now - this.store.state.lastExecutionTime
159
222
  : 0
160
223
  const timeoutDuration = wait - _timeSinceLastExecution
161
- this._timeoutId = setTimeout(() => {
162
- if (this._lastArgs !== undefined) {
163
- this.execute(...this._lastArgs)
224
+ this.#setState({ isPending: true })
225
+ this.#timeoutId = setTimeout(() => {
226
+ const { lastArgs } = this.store.state
227
+ if (lastArgs !== undefined) {
228
+ this.#execute(...lastArgs)
164
229
  }
165
230
  }, timeoutDuration)
166
231
  }
167
232
  }
168
233
  }
169
234
 
170
- private execute(...args: Parameters<TFn>): void {
171
- if (!this.getEnabled()) return
235
+ #execute = (...args: Parameters<TFn>): void => {
236
+ if (!this.#getEnabled()) return
172
237
  this.fn(...args) // EXECUTE!
173
- this._executionCount++
174
- this._lastExecutionTime = Date.now()
175
- this._timeoutId = undefined
176
- this._lastArgs = undefined
177
- this._options.onExecute(this)
238
+ const lastExecutionTime = Date.now()
239
+ const nextExecutionTime = lastExecutionTime + this.#getWait()
240
+ this.#clearTimeout()
241
+ this.#setState({
242
+ executionCount: this.store.state.executionCount + 1,
243
+ lastExecutionTime,
244
+ nextExecutionTime,
245
+ isPending: false,
246
+ lastArgs: undefined,
247
+ })
248
+ this.options.onExecute?.(this)
178
249
  }
179
250
 
180
251
  /**
181
- * Cancels any pending trailing execution and clears internal state.
182
- *
183
- * If a trailing execution is scheduled (due to throttling with trailing=true),
184
- * this will prevent that execution from occurring. The internal timeout and
185
- * stored arguments will be cleared.
186
- *
187
- * Has no effect if there is no pending execution.
252
+ * Processes the current pending execution immediately
188
253
  */
189
- cancel(): void {
190
- if (this._timeoutId) {
191
- clearTimeout(this._timeoutId)
192
- this._timeoutId = undefined
193
- this._lastArgs = undefined
254
+ flush = (): void => {
255
+ if (this.store.state.isPending && this.store.state.lastArgs) {
256
+ this.#execute(...this.store.state.lastArgs)
194
257
  }
195
258
  }
196
259
 
197
- /**
198
- * Returns the last execution time
199
- */
200
- getLastExecutionTime(): number {
201
- return this._lastExecutionTime
202
- }
203
-
204
- /**
205
- * Returns the next execution time
206
- */
207
- getNextExecutionTime(): number {
208
- return this._lastExecutionTime + this.getWait()
260
+ #clearTimeout = (): void => {
261
+ if (this.#timeoutId) {
262
+ clearTimeout(this.#timeoutId)
263
+ this.#timeoutId = undefined
264
+ }
209
265
  }
210
266
 
211
267
  /**
212
- * Returns the number of times the function has been executed
268
+ * Cancels any pending trailing execution and clears internal state.
269
+ *
270
+ * If a trailing execution is scheduled (due to throttling with trailing=true),
271
+ * this will prevent that execution from occurring. The internal timeout and
272
+ * stored arguments will be cleared.
273
+ *
274
+ * Has no effect if there is no pending execution.
213
275
  */
214
- getExecutionCount(): number {
215
- return this._executionCount
276
+ cancel = (): void => {
277
+ this.#clearTimeout()
278
+ this.#setState({
279
+ lastArgs: undefined,
280
+ isPending: false,
281
+ })
216
282
  }
217
283
 
218
284
  /**
219
- * Returns `true` if there is a pending execution
285
+ * Resets the throttler state to its default values
220
286
  */
221
- getIsPending(): boolean {
222
- return this.getEnabled() && !!this._timeoutId
287
+ reset = (): void => {
288
+ this.#setState(getDefaultThrottlerState<TFn>())
223
289
  }
224
290
  }
225
291
 
@@ -236,6 +302,14 @@ export class Throttler<TFn extends AnyFunction> {
236
302
  * For handling bursts of events, consider using debounce() instead. For hard execution
237
303
  * limits, consider using rateLimit().
238
304
  *
305
+ * State Management:
306
+ * - Uses TanStack Store for reactive state management
307
+ * - Use `initialState` to provide initial state values when creating the throttler
308
+ * - Use `onExecute` callback to react to function execution and implement custom logic
309
+ * - The state includes execution count, last execution time, pending status, and more
310
+ * - State can be accessed via the underlying Throttler instance's `store.state` property
311
+ * - When using framework adapters (React/Solid), state is accessed from the hook's state property
312
+ *
239
313
  * @example
240
314
  * ```ts
241
315
  * // Basic throttling - max once per second
@@ -254,5 +328,5 @@ export function throttle<TFn extends AnyFunction>(
254
328
  initialOptions: ThrottlerOptions<TFn>,
255
329
  ) {
256
330
  const throttler = new Throttler(fn, initialOptions)
257
- return throttler.maybeExecute.bind(throttler)
331
+ return throttler.maybeExecute
258
332
  }
package/src/utils.ts CHANGED
@@ -10,18 +10,3 @@ export function parseFunctionOrValue<T, TArgs extends Array<any>>(
10
10
  ): T {
11
11
  return isFunction(value) ? value(...args) : value
12
12
  }
13
-
14
- export function bindInstanceMethods<T extends Record<string, any>>(
15
- instance: T,
16
- ): T {
17
- return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(
18
- (acc: any, key) => {
19
- const method = instance[key as keyof T]
20
- if (isFunction(method)) {
21
- acc[key] = method.bind(instance)
22
- }
23
- return acc
24
- },
25
- instance,
26
- )
27
- }