@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
@@ -2,20 +2,34 @@ import { AnyAsyncFunction } from './types.cjs';
2
2
  /**
3
3
  * Options for configuring an async throttled function
4
4
  */
5
- export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction, TArgs extends Parameters<TFn>> {
5
+ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
6
6
  /**
7
7
  * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
8
8
  * Defaults to true.
9
9
  */
10
10
  enabled?: boolean;
11
+ /**
12
+ * Whether to execute the function immediately when called
13
+ * Defaults to true
14
+ */
15
+ leading?: boolean;
11
16
  /**
12
17
  * Optional error handler for when the throttled function throws
13
18
  */
14
- onError?: (error: unknown) => void;
19
+ onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void;
20
+ /**
21
+ * Optional function to call when the throttled function is executed
22
+ */
23
+ onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void;
15
24
  /**
16
25
  * Optional function to call when the throttled function is executed
17
26
  */
18
- onExecute?: (throttler: AsyncThrottler<TFn, TArgs>) => void;
27
+ onSuccess?: (result: ReturnType<TFn>, asyncThrottler: AsyncThrottler<TFn>) => void;
28
+ /**
29
+ * Whether to execute the function on the trailing edge of the wait period
30
+ * Defaults to true
31
+ */
32
+ trailing?: boolean;
19
33
  /**
20
34
  * Time window in milliseconds during which the function can only be executed once
21
35
  * Defaults to 0ms
@@ -44,41 +58,39 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction, TArgs exten
44
58
  * });
45
59
  * ```
46
60
  */
47
- export declare class AsyncThrottler<TFn extends AnyAsyncFunction, TArgs extends Parameters<TFn>> {
61
+ export declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
48
62
  private fn;
49
63
  private _options;
50
64
  private _abortController;
51
- private _executionCount;
65
+ private _errorCount;
52
66
  private _isExecuting;
53
- private _isPending;
54
67
  private _lastArgs;
55
68
  private _lastExecutionTime;
69
+ private _lastResult;
56
70
  private _nextExecutionTime;
57
- constructor(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn, TArgs>);
71
+ private _settleCount;
72
+ private _successCount;
73
+ private _timeoutId;
74
+ constructor(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>);
58
75
  /**
59
76
  * Updates the throttler options
60
77
  * Returns the new options state
61
78
  */
62
- setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn, TArgs>>): Required<AsyncThrottlerOptions<TFn, TArgs>>;
79
+ setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void;
63
80
  /**
64
81
  * Returns the current options
65
82
  */
66
- getOptions(): Required<AsyncThrottlerOptions<TFn, TArgs>>;
83
+ getOptions(): Required<AsyncThrottlerOptions<TFn>>;
67
84
  /**
68
85
  * Attempts to execute the throttled function
69
86
  * If a call is already in progress, it may be blocked or queued depending on the `wait` option
70
87
  */
71
- maybeExecute(...args: TArgs): Promise<void>;
72
- private delay;
88
+ maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>;
73
89
  private executeFunction;
74
90
  /**
75
- * Cancels any pending execution
91
+ * Cancels any pending execution or aborts any execution in progress
76
92
  */
77
93
  cancel(): void;
78
- /**
79
- * Returns the number of times the function has been executed
80
- */
81
- getExecutionCount(): number;
82
94
  /**
83
95
  * Returns the last execution time
84
96
  */
@@ -87,10 +99,30 @@ export declare class AsyncThrottler<TFn extends AnyAsyncFunction, TArgs extends
87
99
  * Returns the next execution time
88
100
  */
89
101
  getNextExecutionTime(): number;
102
+ /**
103
+ * Returns the last result of the debounced function
104
+ */
105
+ getLastResult(): ReturnType<TFn> | undefined;
106
+ /**
107
+ * Returns the number of times the function has been executed successfully
108
+ */
109
+ getSuccessCount(): number;
110
+ /**
111
+ * Returns the number of times the function has settled (completed or errored)
112
+ */
113
+ getSettleCount(): number;
114
+ /**
115
+ * Returns the number of times the function has errored
116
+ */
117
+ getErrorCount(): number;
90
118
  /**
91
119
  * Returns the current pending state
92
120
  */
93
121
  getIsPending(): boolean;
122
+ /**
123
+ * Returns the current executing state
124
+ */
125
+ getIsExecuting(): boolean;
94
126
  }
95
127
  /**
96
128
  * Creates an async throttled function that limits how often the function can execute.
@@ -108,4 +140,4 @@ export declare class AsyncThrottler<TFn extends AnyAsyncFunction, TArgs extends
108
140
  * await throttled(); // Waits 1 second before executing
109
141
  * ```
110
142
  */
111
- export declare function asyncThrottle<TFn extends AnyAsyncFunction, TArgs extends Parameters<TFn>>(fn: TFn, initialOptions: Omit<AsyncThrottlerOptions<TFn, TArgs>, 'enabled'>): (...args: TArgs) => Promise<void>;
143
+ export declare function asyncThrottle<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions: Omit<AsyncThrottlerOptions<TFn>, 'enabled'>): (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
@@ -3,17 +3,17 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const defaultOptions = {
4
4
  enabled: true,
5
5
  leading: false,
6
- trailing: true,
7
- wait: 0,
8
6
  onExecute: () => {
9
- }
7
+ },
8
+ trailing: true,
9
+ wait: 0
10
10
  };
11
11
  class Debouncer {
12
12
  constructor(fn, initialOptions) {
13
13
  this.fn = fn;
14
14
  this._canLeadingExecute = true;
15
- this._isPending = false;
16
15
  this._executionCount = 0;
16
+ this._isPending = false;
17
17
  this._options = {
18
18
  ...defaultOptions,
19
19
  ...initialOptions
@@ -24,14 +24,10 @@ class Debouncer {
24
24
  * Returns the new options state
25
25
  */
26
26
  setOptions(newOptions) {
27
- this._options = {
28
- ...this._options,
29
- ...newOptions
30
- };
27
+ this._options = { ...this._options, ...newOptions };
31
28
  if (!this._options.enabled) {
32
29
  this._isPending = false;
33
30
  }
34
- return this._options;
35
31
  }
36
32
  /**
37
33
  * Returns the current debouncer options
@@ -44,25 +40,27 @@ class Debouncer {
44
40
  * If a call is already in progress, it will be queued
45
41
  */
46
42
  maybeExecute(...args) {
43
+ let _didLeadingExecute = false;
47
44
  if (this._options.leading && this._canLeadingExecute) {
48
- this.executeFunction(...args);
49
45
  this._canLeadingExecute = false;
46
+ _didLeadingExecute = true;
47
+ this.executeFunction(...args);
50
48
  }
51
- if (this._options.leading || this._options.trailing) {
49
+ if (this._options.trailing) {
52
50
  this._isPending = true;
53
51
  }
54
52
  if (this._timeoutId) clearTimeout(this._timeoutId);
55
53
  this._timeoutId = setTimeout(() => {
56
54
  this._canLeadingExecute = true;
57
- this._isPending = false;
58
- if (this._options.trailing) {
55
+ if (this._options.trailing && !_didLeadingExecute) {
59
56
  this.executeFunction(...args);
60
57
  }
61
58
  }, this._options.wait);
62
59
  }
63
60
  executeFunction(...args) {
64
- if (!this._options.enabled) return;
61
+ if (!this._options.enabled) return void 0;
65
62
  this.fn(...args);
63
+ this._isPending = false;
66
64
  this._executionCount++;
67
65
  this._options.onExecute(this);
68
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"debouncer.cjs","sources":["../../src/debouncer.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\n/**\n * Options for configuring a debounced function\n */\nexport interface DebouncerOptions<\n TFn extends AnyFunction,\n TArgs extends Parameters<TFn>,\n> {\n /**\n * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (debouncer: Debouncer<TFn, TArgs>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds before executing the function\n * Defaults to 0ms\n */\n wait: number\n}\n\nconst defaultOptions: Required<DebouncerOptions<any, any>> = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n onExecute: () => {},\n}\n\n/**\n * A class that creates a debounced function.\n *\n * Debouncing ensures that a function is only executed after a certain amount of time has passed\n * since its last invocation. This is useful for handling frequent events like window resizing,\n * scroll events, or input changes where you want to limit the rate of execution.\n *\n * The debounced function can be configured to execute either at the start of the delay period\n * (leading edge) or at the end (trailing edge, default). Each new call during the wait period\n * will reset the timer.\n *\n * @example\n * ```ts\n * const debouncer = new Debouncer((value: string) => {\n * saveToDatabase(value);\n * }, { wait: 500 });\n *\n * // Will only save after 500ms of no new input\n * inputElement.addEventListener('input', () => {\n * debouncer.maybeExecute(inputElement.value);\n * });\n * ```\n */\nexport class Debouncer<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {\n private _canLeadingExecute = true\n private _isPending = false\n private _executionCount = 0\n private _options: Required<DebouncerOptions<TFn, TArgs>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: DebouncerOptions<TFn, TArgs>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the debouncer options\n * Returns the new options state\n */\n setOptions(\n newOptions: Partial<DebouncerOptions<TFn, TArgs>>,\n ): Required<DebouncerOptions<TFn, TArgs>> {\n this._options = {\n ...this._options,\n ...newOptions,\n }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n\n return this._options\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): Required<DebouncerOptions<TFn, TArgs>> {\n return this._options\n }\n\n /**\n * Attempts to execute the debounced function\n * If a call is already in progress, it will be queued\n */\n maybeExecute(...args: TArgs): void {\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this.executeFunction(...args)\n this._canLeadingExecute = false\n }\n\n // Start pending state\n if (this._options.leading || this._options.trailing) {\n this._isPending = true\n }\n\n // Clear any existing timeout\n if (this._timeoutId) clearTimeout(this._timeoutId)\n\n // Set new timeout that will reset canLeadingExecute\n this._timeoutId = setTimeout(() => {\n this._canLeadingExecute = true\n this._isPending = false\n // Execute trailing only if enabled\n if (this._options.trailing) {\n this.executeFunction(...args)\n }\n }, this._options.wait)\n }\n\n private executeFunction(...args: TArgs): void {\n if (!this._options.enabled) return\n this.fn(...args) // EXECUTE!\n this._executionCount++\n this._options.onExecute(this)\n }\n\n /**\n * Cancels any pending execution\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._canLeadingExecute = true\n this._isPending = false\n }\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if debouncing\n */\n getIsPending(): boolean {\n return this._options.enabled && this._isPending\n }\n}\n\n/**\n * Creates a debounced function that delays invoking the provided function until after a specified wait time.\n * Multiple calls during the wait period will cancel previous pending invocations and reset the timer.\n *\n * This the the simple function wrapper implementation pulled from the Debouncer class. If you need\n * more control over the debouncing behavior, use the Debouncer class directly.\n *\n * If leading option is true, the function will execute immediately on the first call, then wait the delay\n * before allowing another execution.\n *\n * @example\n * ```ts\n * const debounced = debounce(() => {\n * saveChanges();\n * }, { wait: 1000 });\n *\n * // Called repeatedly but executes at most once per second\n * inputElement.addEventListener('input', debounced);\n * ```\n */\nexport function debounce<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: Omit<DebouncerOptions<TFn, Parameters<TFn>>, 'enabled'>,\n) {\n const debouncer = new Debouncer(fn, initialOptions)\n return debouncer.maybeExecute.bind(debouncer)\n}\n"],"names":[],"mappings":";;AAmCA,MAAM,iBAAuD;AAAA,EAC3D,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AAAA,EACN,WAAW,MAAM;AAAA,EAAA;AACnB;AAyBO,MAAM,UAAkE;AAAA,EAO7E,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,qBAAqB;AAC7B,SAAQ,aAAa;AACrB,SAAQ,kBAAkB;AAQxB,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,WACE,YACwC;AACxC,SAAK,WAAW;AAAA,MACd,GAAG,KAAK;AAAA,MACR,GAAG;AAAA,IACL;AAGI,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,aAAa;AAAA,IAAA;AAGpB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAqD;AACnD,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd,gBAAgB,MAAmB;AAEjC,QAAI,KAAK,SAAS,WAAW,KAAK,oBAAoB;AAC/C,WAAA,gBAAgB,GAAG,IAAI;AAC5B,WAAK,qBAAqB;AAAA,IAAA;AAI5B,QAAI,KAAK,SAAS,WAAW,KAAK,SAAS,UAAU;AACnD,WAAK,aAAa;AAAA,IAAA;AAIpB,QAAI,KAAK,WAAyB,cAAA,KAAK,UAAU;AAG5C,SAAA,aAAa,WAAW,MAAM;AACjC,WAAK,qBAAqB;AAC1B,WAAK,aAAa;AAEd,UAAA,KAAK,SAAS,UAAU;AACrB,aAAA,gBAAgB,GAAG,IAAI;AAAA,MAAA;AAAA,IAC9B,GACC,KAAK,SAAS,IAAI;AAAA,EAAA;AAAA,EAGf,mBAAmB,MAAmB;AACxC,QAAA,CAAC,KAAK,SAAS,QAAS;AACvB,SAAA,GAAG,GAAG,IAAI;AACV,SAAA;AACA,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,qBAAqB;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACf,WAAA,KAAK,SAAS,WAAW,KAAK;AAAA,EAAA;AAEzC;AAsBgB,SAAA,SACd,IACA,gBACA;AACA,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;;;"}
1
+ {"version":3,"file":"debouncer.cjs","sources":["../../src/debouncer.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\n/**\n * Options for configuring a debounced function\n */\nexport interface DebouncerOptions<TFn extends AnyFunction> {\n /**\n * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean\n /**\n * Whether to execute on the leading edge of the timeout.\n * The first call will execute immediately and the rest will wait the delay.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (debouncer: Debouncer<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds before executing the function\n * Defaults to 0ms\n */\n wait: number\n}\n\nconst defaultOptions: Required<DebouncerOptions<any>> = {\n enabled: true,\n leading: false,\n onExecute: () => {},\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a debounced function.\n *\n * Debouncing ensures that a function is only executed after a certain amount of time has passed\n * since its last invocation. This is useful for handling frequent events like window resizing,\n * scroll events, or input changes where you want to limit the rate of execution.\n *\n * The debounced function can be configured to execute either at the start of the delay period\n * (leading edge) or at the end (trailing edge, default). Each new call during the wait period\n * will reset the timer.\n *\n * @example\n * ```ts\n * const debouncer = new Debouncer((value: string) => {\n * saveToDatabase(value);\n * }, { wait: 500 });\n *\n * // Will only save after 500ms of no new input\n * inputElement.addEventListener('input', () => {\n * debouncer.maybeExecute(inputElement.value);\n * });\n * ```\n */\nexport class Debouncer<TFn extends AnyFunction> {\n private _canLeadingExecute = true\n private _executionCount = 0\n private _isPending = false\n private _options: Required<DebouncerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the debouncer options\n * Returns the new options state\n */\n setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): Required<DebouncerOptions<TFn>> {\n return this._options\n }\n\n /**\n * Attempts to execute the debounced function\n * If a call is already in progress, it will be queued\n */\n maybeExecute(...args: Parameters<TFn>): void {\n let _didLeadingExecute = false\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._canLeadingExecute = false\n _didLeadingExecute = true\n this.executeFunction(...args)\n }\n\n // Start pending state to indicate that the debouncer is waiting for the trailing edge\n if (this._options.trailing) {\n this._isPending = true\n }\n\n // Clear any existing timeout\n if (this._timeoutId) clearTimeout(this._timeoutId)\n\n // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading\n this._timeoutId = setTimeout(() => {\n this._canLeadingExecute = true\n if (this._options.trailing && !_didLeadingExecute) {\n this.executeFunction(...args)\n }\n }, this._options.wait)\n }\n\n private executeFunction(...args: Parameters<TFn>): void {\n if (!this._options.enabled) return undefined\n this.fn(...args) // EXECUTE!\n this._isPending = false\n this._executionCount++\n this._options.onExecute(this)\n }\n\n /**\n * Cancels any pending execution\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._canLeadingExecute = true\n this._isPending = false\n }\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if debouncing\n */\n getIsPending(): boolean {\n return this._options.enabled && this._isPending\n }\n}\n\n/**\n * Creates a debounced function that delays invoking the provided function until after a specified wait time.\n * Multiple calls during the wait period will cancel previous pending invocations and reset the timer.\n *\n * This the the simple function wrapper implementation pulled from the Debouncer class. If you need\n * more control over the debouncing behavior, use the Debouncer class directly.\n *\n * If leading option is true, the function will execute immediately on the first call, then wait the delay\n * before allowing another execution.\n *\n * @example\n * ```ts\n * const debounced = debounce(() => {\n * saveChanges();\n * }, { wait: 1000 });\n *\n * // Called repeatedly but executes at most once per second\n * inputElement.addEventListener('input', debounced);\n * ```\n */\nexport function debounce<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: Omit<DebouncerOptions<TFn>, 'enabled'>,\n): (...args: Parameters<TFn>) => void {\n const debouncer = new Debouncer(fn, initialOptions)\n return debouncer.maybeExecute.bind(debouncer)\n}\n"],"names":[],"mappings":";;AAiCA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AAyBO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,qBAAqB;AAC7B,SAAQ,kBAAkB;AAC1B,SAAQ,aAAa;AAQnB,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd,gBAAgB,MAA6B;AAC3C,QAAI,qBAAqB;AAGzB,QAAI,KAAK,SAAS,WAAW,KAAK,oBAAoB;AACpD,WAAK,qBAAqB;AACL,2BAAA;AAChB,WAAA,gBAAgB,GAAG,IAAI;AAAA,IAAA;AAI1B,QAAA,KAAK,SAAS,UAAU;AAC1B,WAAK,aAAa;AAAA,IAAA;AAIpB,QAAI,KAAK,WAAyB,cAAA,KAAK,UAAU;AAG5C,SAAA,aAAa,WAAW,MAAM;AACjC,WAAK,qBAAqB;AAC1B,UAAI,KAAK,SAAS,YAAY,CAAC,oBAAoB;AAC5C,aAAA,gBAAgB,GAAG,IAAI;AAAA,MAAA;AAAA,IAC9B,GACC,KAAK,SAAS,IAAI;AAAA,EAAA;AAAA,EAGf,mBAAmB,MAA6B;AACtD,QAAI,CAAC,KAAK,SAAS,QAAgB,QAAA;AAC9B,SAAA,GAAG,GAAG,IAAI;AACf,SAAK,aAAa;AACb,SAAA;AACA,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,qBAAqB;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACf,WAAA,KAAK,SAAS,WAAW,KAAK;AAAA,EAAA;AAEzC;AAsBgB,SAAA,SACd,IACA,gBACoC;AACpC,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;;;"}
@@ -2,7 +2,7 @@ import { AnyFunction } from './types.cjs';
2
2
  /**
3
3
  * Options for configuring a debounced function
4
4
  */
5
- export interface DebouncerOptions<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
5
+ export interface DebouncerOptions<TFn extends AnyFunction> {
6
6
  /**
7
7
  * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
8
8
  * Defaults to true.
@@ -10,13 +10,14 @@ export interface DebouncerOptions<TFn extends AnyFunction, TArgs extends Paramet
10
10
  enabled?: boolean;
11
11
  /**
12
12
  * Whether to execute on the leading edge of the timeout.
13
+ * The first call will execute immediately and the rest will wait the delay.
13
14
  * Defaults to false.
14
15
  */
15
16
  leading?: boolean;
16
17
  /**
17
18
  * Callback function that is called after the function is executed
18
19
  */
19
- onExecute?: (debouncer: Debouncer<TFn, TArgs>) => void;
20
+ onExecute?: (debouncer: Debouncer<TFn>) => void;
20
21
  /**
21
22
  * Whether to execute on the trailing edge of the timeout.
22
23
  * Defaults to true.
@@ -51,28 +52,28 @@ export interface DebouncerOptions<TFn extends AnyFunction, TArgs extends Paramet
51
52
  * });
52
53
  * ```
53
54
  */
54
- export declare class Debouncer<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
55
+ export declare class Debouncer<TFn extends AnyFunction> {
55
56
  private fn;
56
57
  private _canLeadingExecute;
57
- private _isPending;
58
58
  private _executionCount;
59
+ private _isPending;
59
60
  private _options;
60
61
  private _timeoutId;
61
- constructor(fn: TFn, initialOptions: DebouncerOptions<TFn, TArgs>);
62
+ constructor(fn: TFn, initialOptions: DebouncerOptions<TFn>);
62
63
  /**
63
64
  * Updates the debouncer options
64
65
  * Returns the new options state
65
66
  */
66
- setOptions(newOptions: Partial<DebouncerOptions<TFn, TArgs>>): Required<DebouncerOptions<TFn, TArgs>>;
67
+ setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void;
67
68
  /**
68
69
  * Returns the current debouncer options
69
70
  */
70
- getOptions(): Required<DebouncerOptions<TFn, TArgs>>;
71
+ getOptions(): Required<DebouncerOptions<TFn>>;
71
72
  /**
72
73
  * Attempts to execute the debounced function
73
74
  * If a call is already in progress, it will be queued
74
75
  */
75
- maybeExecute(...args: TArgs): void;
76
+ maybeExecute(...args: Parameters<TFn>): void;
76
77
  private executeFunction;
77
78
  /**
78
79
  * Cancels any pending execution
@@ -107,4 +108,4 @@ export declare class Debouncer<TFn extends AnyFunction, TArgs extends Parameters
107
108
  * inputElement.addEventListener('input', debounced);
108
109
  * ```
109
110
  */
110
- export declare function debounce<TFn extends AnyFunction>(fn: TFn, initialOptions: Omit<DebouncerOptions<TFn, Parameters<TFn>>, 'enabled'>): (...args: Parameters<TFn>) => void;
111
+ export declare function debounce<TFn extends AnyFunction>(fn: TFn, initialOptions: Omit<DebouncerOptions<TFn>, 'enabled'>): (...args: Parameters<TFn>) => void;
@@ -4,6 +4,8 @@ const defaultOptions = {
4
4
  addItemsTo: "back",
5
5
  getItemsFrom: "front",
6
6
  getPriority: (item) => (item == null ? void 0 : item.priority) ?? 0,
7
+ getIsExpired: () => false,
8
+ expirationDuration: Infinity,
7
9
  initialItems: [],
8
10
  maxSize: Infinity,
9
11
  onGetNextItem: () => {
@@ -14,14 +16,18 @@ const defaultOptions = {
14
16
  },
15
17
  onReject: () => {
16
18
  },
19
+ onExpire: () => {
20
+ },
17
21
  started: false,
18
22
  wait: 0
19
23
  };
20
24
  class Queuer {
21
25
  constructor(initialOptions = defaultOptions) {
22
26
  this._items = [];
27
+ this._itemTimestamps = [];
23
28
  this._executionCount = 0;
24
29
  this._rejectionCount = 0;
30
+ this._expirationCount = 0;
25
31
  this._onItemsChanges = [];
26
32
  this._pendingTick = false;
27
33
  this._options = { ...defaultOptions, ...initialOptions };
@@ -38,7 +44,6 @@ class Queuer {
38
44
  */
39
45
  setOptions(newOptions) {
40
46
  this._options = { ...this._options, ...newOptions };
41
- return this._options;
42
47
  }
43
48
  /**
44
49
  * Returns the current queuer options
@@ -54,6 +59,7 @@ class Queuer {
54
59
  this._pendingTick = false;
55
60
  return;
56
61
  }
62
+ this.checkExpiredItems();
57
63
  while (!this.getIsEmpty()) {
58
64
  const nextItem = this.getNextItem(this._options.getItemsFrom);
59
65
  if (nextItem === void 0) {
@@ -68,6 +74,38 @@ class Queuer {
68
74
  }
69
75
  this._pendingTick = false;
70
76
  }
77
+ /**
78
+ * Checks for and removes expired items from the queuer
79
+ */
80
+ checkExpiredItems() {
81
+ if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired)
82
+ return;
83
+ const now = Date.now();
84
+ const expiredIndices = [];
85
+ for (let i = 0; i < this._items.length; i++) {
86
+ const timestamp = this._itemTimestamps[i];
87
+ if (timestamp === void 0) continue;
88
+ const item = this._items[i];
89
+ if (item === void 0) continue;
90
+ const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration;
91
+ if (isExpired) {
92
+ expiredIndices.push(i);
93
+ }
94
+ }
95
+ for (let i = expiredIndices.length - 1; i >= 0; i--) {
96
+ const index = expiredIndices[i];
97
+ if (index === void 0) continue;
98
+ const expiredItem = this._items[index];
99
+ if (expiredItem === void 0) continue;
100
+ this._items.splice(index, 1);
101
+ this._itemTimestamps.splice(index, 1);
102
+ this._expirationCount++;
103
+ this._options.onExpire(expiredItem, this);
104
+ }
105
+ if (expiredIndices.length > 0) {
106
+ this._options.onItemsChange(this);
107
+ }
108
+ }
71
109
  /**
72
110
  * Stops the queuer from processing items
73
111
  */
@@ -122,14 +160,18 @@ class Queuer {
122
160
  );
123
161
  if (insertIndex === -1) {
124
162
  this._items.push(item);
163
+ this._itemTimestamps.push(Date.now());
125
164
  } else {
126
165
  this._items.splice(insertIndex, 0, item);
166
+ this._itemTimestamps.splice(insertIndex, 0, Date.now());
127
167
  }
128
168
  } else {
129
169
  if (position === "front") {
130
170
  this._items.unshift(item);
171
+ this._itemTimestamps.unshift(Date.now());
131
172
  } else {
132
173
  this._items.push(item);
174
+ this._itemTimestamps.push(Date.now());
133
175
  }
134
176
  }
135
177
  if (this._running && !this._pendingTick) {
@@ -156,8 +198,10 @@ class Queuer {
156
198
  let item;
157
199
  if (position === "front") {
158
200
  item = this._items.shift();
201
+ this._itemTimestamps.shift();
159
202
  } else {
160
203
  item = this._items.pop();
204
+ this._itemTimestamps.pop();
161
205
  }
162
206
  if (item !== void 0) {
163
207
  this._executionCount++;
@@ -219,6 +263,12 @@ class Queuer {
219
263
  getRejectionCount() {
220
264
  return this._rejectionCount;
221
265
  }
266
+ /**
267
+ * Returns the number of items that have expired from the queuer
268
+ */
269
+ getExpirationCount() {
270
+ return this._expirationCount;
271
+ }
222
272
  /**
223
273
  * Returns true if the queuer is running
224
274
  */
@@ -1 +1 @@
1
- {"version":3,"file":"queuer.cjs","sources":["../../src/queuer.ts"],"sourcesContent":["/**\n * Options for configuring a Queuer instance\n */\nexport interface QueuerOptions<TValue> {\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Callback fired whenever an item is removed from the queuer\n */\n onGetNextItem?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever the queuer's running state changes\n */\n onIsRunningChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately\n */\n started?: boolean\n /**\n * Time in milliseconds to wait between processing items\n */\n wait?: number\n}\n\nconst defaultOptions: Required<QueuerOptions<any>> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n initialItems: [],\n maxSize: Infinity,\n onGetNextItem: () => {},\n onIsRunningChange: () => {},\n onItemsChange: () => {},\n onReject: () => {},\n started: false,\n wait: 0,\n}\n\n/**\n * Position type for addItem and getNextItem operations\n */\nexport type QueuePosition = 'front' | 'back'\n\n/**\n * A flexible queue data structure that defaults to FIFO (First In First Out) behavior\n * with optional position overrides for stack-like or double-ended operations.\n *\n * The queuer can automatically process items as they are added, with configurable\n * wait times between processing each item. Processing can be started/stopped\n * and the queuer will maintain its state.\n *\n * Supports priority-based ordering when a getPriority function is provided.\n * Items with higher priority values will be processed first.\n *\n * Default queue behavior:\n * - addItem(item): adds to back of queuer\n * - getNextItem(): removes and returns from front of queuer\n *\n * Stack (LIFO) behavior:\n * - addItem(item, 'back'): adds to back\n * - getNextItem('back'): removes and returns from back\n *\n * Double-ended queuer behavior:\n * - addItem(item, position): adds to specified position ('front' or 'back')\n * - getNextItem(position): removes and returns from specified position\n *\n * Processing behavior:\n * - start(): begins processing items in the queuer\n * - stop(): pauses processing\n * - wait: configurable delay between processing items\n * - onItemsChange/onGetNextItem: callbacks for monitoring queuer state\n *\n * @example\n * ```ts\n * // FIFO queuer\n * const queuer = new Queuer<number>();\n * queuer.addItem(1); // [1]\n * queuer.addItem(2); // [1, 2]\n * queuer.getNextItem(); // returns 1, queuer is [2]\n *\n * // Priority queuer with processing\n * const priorityQueue = new Queuer<number>({\n * getPriority: (n) => n, // Higher numbers have priority\n * started: true, // Begin processing immediately\n * wait: 1000, // Wait 1s between items\n * onGetNextItem: (item, queuer) => console.log(item)\n * });\n * priorityQueue.addItem(1); // [1]\n * priorityQueue.addItem(3); // [3, 1] - 3 processed first\n * priorityQueue.addItem(2); // [3, 2, 1]\n * ```\n */\nexport class Queuer<TValue> {\n private _options: Required<QueuerOptions<TValue>>\n private _items: Array<TValue> = []\n private _executionCount = 0\n private _rejectionCount = 0\n private _onItemsChanges: Array<(item: TValue) => void> = []\n private _running: boolean\n private _pendingTick = false\n\n constructor(initialOptions: QueuerOptions<TValue> = defaultOptions) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\n\n for (let i = 0; i < this._options.initialItems.length; i++) {\n const item = this._options.initialItems[i]!\n const isLast = i === this._options.initialItems.length - 1\n this.addItem(item, this._options.addItemsTo, isLast)\n }\n }\n\n /**\n * Updates the queuer options\n * Returns the new options state\n */\n setOptions(\n newOptions: Partial<QueuerOptions<TValue>>,\n ): QueuerOptions<TValue> {\n this._options = { ...this._options, ...newOptions }\n return this._options\n }\n\n /**\n * Returns the current queuer options\n */\n getOptions(): Required<QueuerOptions<TValue>> {\n return this._options\n }\n\n /**\n * Processes items in the queuer\n */\n private tick() {\n if (!this._running) {\n this._pendingTick = false\n return\n }\n while (!this.getIsEmpty()) {\n const nextItem = this.getNextItem(this._options.getItemsFrom)\n if (nextItem === undefined) {\n break\n }\n this._onItemsChanges.forEach((cb) => cb(nextItem))\n\n if (this._options.wait > 0) {\n // Use setTimeout to wait before processing next item\n setTimeout(() => this.tick(), this._options.wait)\n return\n }\n\n this.tick()\n }\n this._pendingTick = false\n }\n\n /**\n * Stops the queuer from processing items\n */\n stop() {\n this._running = false\n this._pendingTick = false\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Starts the queuer and processes items\n */\n start() {\n this._running = true\n if (!this._pendingTick && !this.getIsEmpty()) {\n this._pendingTick = true\n this.tick()\n }\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Removes all items from the queuer\n */\n clear(): void {\n this._items = []\n this._options.onItemsChange(this)\n }\n\n /**\n * Resets the queuer to its initial state\n */\n reset(withInitialItems?: boolean): void {\n this.clear()\n this._executionCount = 0\n if (withInitialItems) {\n this._items = [...this._options.initialItems]\n }\n this._running = this._options.started\n }\n\n /**\n * Adds an item to the queuer and starts processing if not already running\n * @returns true if item was added, false if queuer is full\n */\n addItem(\n item: TValue,\n position: QueuePosition = this._options.addItemsTo,\n runOnUpdate: boolean = true,\n ): boolean {\n if (this.getIsFull()) {\n this._rejectionCount++\n this._options.onReject(item, this)\n return false\n }\n\n if (this._options.getPriority !== defaultOptions.getPriority) {\n // If custom priority function is provided, insert based on priority\n const priority = this._options.getPriority(item)\n const insertIndex = this._items.findIndex(\n (existing) => this._options.getPriority(existing) > priority,\n )\n\n if (insertIndex === -1) {\n this._items.push(item)\n } else {\n this._items.splice(insertIndex, 0, item)\n }\n } else {\n // Default FIFO/LIFO behavior\n if (position === 'front') {\n this._items.unshift(item)\n } else {\n this._items.push(item)\n }\n }\n\n if (this._running && !this._pendingTick) {\n this._pendingTick = true\n this.tick()\n }\n if (runOnUpdate) {\n this._options.onItemsChange(this)\n }\n return true\n }\n\n /**\n * Removes and returns an item from the queuer using shift (default) or pop\n *\n * @example\n * ```ts\n * // Standard FIFO queuer\n * queuer.getNextItem()\n * // Stack-like behavior (LIFO)\n * queuer.getNextItem('back')\n * ```\n */\n getNextItem(\n position: QueuePosition = this._options.getItemsFrom,\n ): TValue | undefined {\n let item: TValue | undefined\n\n if (position === 'front') {\n item = this._items.shift()\n } else {\n item = this._items.pop()\n }\n\n if (item !== undefined) {\n this._executionCount++\n this._options.onItemsChange(this)\n this._options.onGetNextItem(item, this)\n }\n return item\n }\n\n /**\n * Returns an item without removing it\n *\n * @example\n * ```ts\n * // Look at next item to getNextItem\n * queuer.getPeek()\n * // Look at last item (like stack top)\n * queuer.getPeek('back')\n * ```\n */\n getPeek(\n position: QueuePosition = this._options.getItemsFrom,\n ): TValue | undefined {\n if (position === 'front') {\n return this._items[0]\n }\n return this._items[this._items.length - 1]\n }\n\n /**\n * Returns true if the queuer is empty\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the queuer is full\n */\n getIsFull(): boolean {\n return this._items.length >= this._options.maxSize\n }\n\n /**\n * Returns the current size of the queuer\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns a copy of all items in the queuer\n */\n getAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of items that have been removed from the queuer\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of items that have been rejected from the queuer\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns true if the queuer is running\n */\n getIsRunning() {\n return this._running\n }\n\n /**\n * Returns true if the queuer is running but has no items to process\n */\n getIsIdle() {\n return this._running && this.getIsEmpty()\n }\n}\n\n/**\n * Creates a queue that processes items in a queuer immediately upon addition.\n * Items are processed sequentially in FIFO order by default.\n *\n * This is a simplified wrapper around the Queuer class that only exposes the\n * `addItem` method. This queue is always running and will process items as they are added.\n * For more control over queuer processing, use the Queuer class\n * directly which provides methods like `start`, `stop`, `reset`, and more.\n *\n * @example\n * ```ts\n * // Basic sequential processing\n * const processItems = queuer<number>({\n * wait: 1000,\n * onItemsChange: (queuer) => console.log(queuer.getAllItems())\n * })\n * processItems(1) // Logs: 1\n * processItems(2) // Logs: 2 after 1 completes\n *\n * // Priority queuer\n * const processPriority = queuer<number>({\n * process: async (n) => console.log(n),\n * getPriority: n => n // Higher numbers processed first\n * })\n * processPriority(1)\n * processPriority(3) // Processed before 1\n * ```\n */\nexport function queue<TValue>(options: QueuerOptions<TValue> = {}) {\n const queuer = new Queuer<TValue>({ ...options, started: true })\n return queuer.addItem.bind(queuer)\n}\n"],"names":[],"mappings":";;AAqDA,MAAM,iBAA+C;AAAA,EACnD,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,aAAa,CAAC,UAAS,6BAAM,aAAY;AAAA,EACzC,cAAc,CAAC;AAAA,EACf,SAAS;AAAA,EACT,eAAe,MAAM;AAAA,EAAC;AAAA,EACtB,mBAAmB,MAAM;AAAA,EAAC;AAAA,EAC1B,eAAe,MAAM;AAAA,EAAC;AAAA,EACtB,UAAU,MAAM;AAAA,EAAC;AAAA,EACjB,SAAS;AAAA,EACT,MAAM;AACR;AAwDO,MAAM,OAAe;AAAA,EAS1B,YAAY,iBAAwC,gBAAgB;AAPpE,SAAQ,SAAwB,CAAC;AACjC,SAAQ,kBAAkB;AAC1B,SAAQ,kBAAkB;AAC1B,SAAQ,kBAAiD,CAAC;AAE1D,SAAQ,eAAe;AAGrB,SAAK,WAAW,EAAE,GAAG,gBAAgB,GAAG,eAAe;AAClD,SAAA,WAAW,KAAK,SAAS;AAE9B,aAAS,IAAI,GAAG,IAAI,KAAK,SAAS,aAAa,QAAQ,KAAK;AAC1D,YAAM,OAAO,KAAK,SAAS,aAAa,CAAC;AACzC,YAAM,SAAS,MAAM,KAAK,SAAS,aAAa,SAAS;AACzD,WAAK,QAAQ,MAAM,KAAK,SAAS,YAAY,MAAM;AAAA,IAAA;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,WACE,YACuB;AACvB,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAClD,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMN,OAAO;AACT,QAAA,CAAC,KAAK,UAAU;AAClB,WAAK,eAAe;AACpB;AAAA,IAAA;AAEK,WAAA,CAAC,KAAK,cAAc;AACzB,YAAM,WAAW,KAAK,YAAY,KAAK,SAAS,YAAY;AAC5D,UAAI,aAAa,QAAW;AAC1B;AAAA,MAAA;AAEF,WAAK,gBAAgB,QAAQ,CAAC,OAAO,GAAG,QAAQ,CAAC;AAE7C,UAAA,KAAK,SAAS,OAAO,GAAG;AAE1B,mBAAW,MAAM,KAAK,KAAQ,GAAA,KAAK,SAAS,IAAI;AAChD;AAAA,MAAA;AAGF,WAAK,KAAK;AAAA,IAAA;AAEZ,SAAK,eAAe;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMtB,OAAO;AACL,SAAK,WAAW;AAChB,SAAK,eAAe;AACf,SAAA,SAAS,kBAAkB,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMtC,QAAQ;AACN,SAAK,WAAW;AAChB,QAAI,CAAC,KAAK,gBAAgB,CAAC,KAAK,cAAc;AAC5C,WAAK,eAAe;AACpB,WAAK,KAAK;AAAA,IAAA;AAEP,SAAA,SAAS,kBAAkB,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMtC,QAAc;AACZ,SAAK,SAAS,CAAC;AACV,SAAA,SAAS,cAAc,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMlC,MAAM,kBAAkC;AACtC,SAAK,MAAM;AACX,SAAK,kBAAkB;AACvB,QAAI,kBAAkB;AACpB,WAAK,SAAS,CAAC,GAAG,KAAK,SAAS,YAAY;AAAA,IAAA;AAEzC,SAAA,WAAW,KAAK,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOhC,QACE,MACA,WAA0B,KAAK,SAAS,YACxC,cAAuB,MACd;AACL,QAAA,KAAK,aAAa;AACf,WAAA;AACA,WAAA,SAAS,SAAS,MAAM,IAAI;AAC1B,aAAA;AAAA,IAAA;AAGT,QAAI,KAAK,SAAS,gBAAgB,eAAe,aAAa;AAE5D,YAAM,WAAW,KAAK,SAAS,YAAY,IAAI;AACzC,YAAA,cAAc,KAAK,OAAO;AAAA,QAC9B,CAAC,aAAa,KAAK,SAAS,YAAY,QAAQ,IAAI;AAAA,MACtD;AAEA,UAAI,gBAAgB,IAAI;AACjB,aAAA,OAAO,KAAK,IAAI;AAAA,MAAA,OAChB;AACL,aAAK,OAAO,OAAO,aAAa,GAAG,IAAI;AAAA,MAAA;AAAA,IACzC,OACK;AAEL,UAAI,aAAa,SAAS;AACnB,aAAA,OAAO,QAAQ,IAAI;AAAA,MAAA,OACnB;AACA,aAAA,OAAO,KAAK,IAAI;AAAA,MAAA;AAAA,IACvB;AAGF,QAAI,KAAK,YAAY,CAAC,KAAK,cAAc;AACvC,WAAK,eAAe;AACpB,WAAK,KAAK;AAAA,IAAA;AAEZ,QAAI,aAAa;AACV,WAAA,SAAS,cAAc,IAAI;AAAA,IAAA;AAE3B,WAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcT,YACE,WAA0B,KAAK,SAAS,cACpB;AAChB,QAAA;AAEJ,QAAI,aAAa,SAAS;AACjB,aAAA,KAAK,OAAO,MAAM;AAAA,IAAA,OACpB;AACE,aAAA,KAAK,OAAO,IAAI;AAAA,IAAA;AAGzB,QAAI,SAAS,QAAW;AACjB,WAAA;AACA,WAAA,SAAS,cAAc,IAAI;AAC3B,WAAA,SAAS,cAAc,MAAM,IAAI;AAAA,IAAA;AAEjC,WAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcT,QACE,WAA0B,KAAK,SAAS,cACpB;AACpB,QAAI,aAAa,SAAS;AACjB,aAAA,KAAK,OAAO,CAAC;AAAA,IAAA;AAEtB,WAAO,KAAK,OAAO,KAAK,OAAO,SAAS,CAAC;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM3C,aAAsB;AACb,WAAA,KAAK,OAAO,WAAW;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhC,YAAqB;AACnB,WAAO,KAAK,OAAO,UAAU,KAAK,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM7C,UAAkB;AAChB,WAAO,KAAK,OAAO;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMrB,cAA6B;AACpB,WAAA,CAAC,GAAG,KAAK,MAAM;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMxB,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAe;AACb,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,YAAY;AACH,WAAA,KAAK,YAAY,KAAK,WAAW;AAAA,EAAA;AAE5C;AA8BgB,SAAA,MAAc,UAAiC,IAAI;AAC3D,QAAA,SAAS,IAAI,OAAe,EAAE,GAAG,SAAS,SAAS,MAAM;AACxD,SAAA,OAAO,QAAQ,KAAK,MAAM;AACnC;;;"}
1
+ {"version":3,"file":"queuer.cjs","sources":["../../src/queuer.ts"],"sourcesContent":["/**\n * Options for configuring a Queuer instance\n */\nexport interface QueuerOptions<TValue> {\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is removed from the queuer\n */\n onGetNextItem?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever the queuer's running state changes\n */\n onIsRunningChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately\n */\n started?: boolean\n /**\n * Time in milliseconds to wait between processing items\n */\n wait?: number\n}\n\nconst defaultOptions: Required<QueuerOptions<any>> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n onGetNextItem: () => {},\n onIsRunningChange: () => {},\n onItemsChange: () => {},\n onReject: () => {},\n onExpire: () => {},\n started: false,\n wait: 0,\n}\n\n/**\n * Position type for addItem and getNextItem operations\n */\nexport type QueuePosition = 'front' | 'back'\n\n/**\n * A flexible queue data structure that defaults to FIFO (First In First Out) behavior\n * with optional position overrides for stack-like or double-ended operations.\n *\n * The queuer can automatically process items as they are added, with configurable\n * wait times between processing each item. Processing can be started/stopped\n * and the queuer will maintain its state.\n *\n * Supports priority-based ordering when a getPriority function is provided.\n * Items with higher priority values will be processed first.\n *\n * Default queue behavior:\n * - addItem(item): adds to back of queuer\n * - getNextItem(): removes and returns from front of queuer\n *\n * Stack (LIFO) behavior:\n * - addItem(item, 'back'): adds to back\n * - getNextItem('back'): removes and returns from back\n *\n * Double-ended queuer behavior:\n * - addItem(item, position): adds to specified position ('front' or 'back')\n * - getNextItem(position): removes and returns from specified position\n *\n * Processing behavior:\n * - start(): begins processing items in the queuer\n * - stop(): pauses processing\n * - wait: configurable delay between processing items\n * - onItemsChange/onGetNextItem: callbacks for monitoring queuer state\n *\n * Supports item expiration to clear stale items from the queuer\n * - expirationDuration: maximum time in milliseconds that an item can stay in the queue\n * - getIsExpired: function to override default expiration behavior\n * - onExpire: callback for when an item expires\n *\n * @example\n * ```ts\n * // FIFO queuer\n * const queuer = new Queuer<number>();\n * queuer.addItem(1); // [1]\n * queuer.addItem(2); // [1, 2]\n * queuer.getNextItem(); // returns 1, queuer is [2]\n *\n * // Priority queuer with processing\n * const priorityQueue = new Queuer<number>({\n * getPriority: (n) => n, // Higher numbers have priority\n * started: true, // Begin processing immediately\n * wait: 1000, // Wait 1s between items\n * onGetNextItem: (item, queuer) => console.log(item)\n * });\n * priorityQueue.addItem(1); // [1]\n * priorityQueue.addItem(3); // [3, 1] - 3 processed first\n * priorityQueue.addItem(2); // [3, 2, 1]\n * ```\n */\nexport class Queuer<TValue> {\n private _options: Required<QueuerOptions<TValue>>\n private _items: Array<TValue> = []\n private _itemTimestamps: Array<number> = []\n private _executionCount = 0\n private _rejectionCount = 0\n private _expirationCount = 0\n private _onItemsChanges: Array<(item: TValue) => void> = []\n private _running: boolean\n private _pendingTick = false\n\n constructor(initialOptions: QueuerOptions<TValue> = defaultOptions) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\n\n for (let i = 0; i < this._options.initialItems.length; i++) {\n const item = this._options.initialItems[i]!\n const isLast = i === this._options.initialItems.length - 1\n this.addItem(item, this._options.addItemsTo, isLast)\n }\n }\n\n /**\n * Updates the queuer options\n * Returns the new options state\n */\n setOptions(newOptions: Partial<QueuerOptions<TValue>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current queuer options\n */\n getOptions(): Required<QueuerOptions<TValue>> {\n return this._options\n }\n\n /**\n * Processes items in the queuer\n */\n private tick() {\n if (!this._running) {\n this._pendingTick = false\n return\n }\n\n // Check for expired items\n this.checkExpiredItems()\n\n while (!this.getIsEmpty()) {\n const nextItem = this.getNextItem(this._options.getItemsFrom)\n if (nextItem === undefined) {\n break\n }\n this._onItemsChanges.forEach((cb) => cb(nextItem))\n\n if (this._options.wait > 0) {\n // Use setTimeout to wait before processing next item\n setTimeout(() => this.tick(), this._options.wait)\n return\n }\n\n this.tick()\n }\n this._pendingTick = false\n }\n\n /**\n * Checks for and removes expired items from the queuer\n */\n private checkExpiredItems() {\n if (\n this._options.expirationDuration === Infinity &&\n this._options.getIsExpired === defaultOptions.getIsExpired\n )\n return\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this._items.length; i++) {\n const timestamp = this._itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this._items[i]\n if (item === undefined) continue\n\n const isExpired =\n this._options.getIsExpired !== defaultOptions.getIsExpired\n ? this._options.getIsExpired(item, timestamp)\n : now - timestamp > this._options.expirationDuration\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this._items[index]\n if (expiredItem === undefined) continue\n\n this._items.splice(index, 1)\n this._itemTimestamps.splice(index, 1)\n this._expirationCount++\n this._options.onExpire(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this._options.onItemsChange(this)\n }\n }\n\n /**\n * Stops the queuer from processing items\n */\n stop() {\n this._running = false\n this._pendingTick = false\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Starts the queuer and processes items\n */\n start() {\n this._running = true\n if (!this._pendingTick && !this.getIsEmpty()) {\n this._pendingTick = true\n this.tick()\n }\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Removes all items from the queuer\n */\n clear(): void {\n this._items = []\n this._options.onItemsChange(this)\n }\n\n /**\n * Resets the queuer to its initial state\n */\n reset(withInitialItems?: boolean): void {\n this.clear()\n this._executionCount = 0\n if (withInitialItems) {\n this._items = [...this._options.initialItems]\n }\n this._running = this._options.started\n }\n\n /**\n * Adds an item to the queuer and starts processing if not already running\n * @returns true if item was added, false if queuer is full\n */\n addItem(\n item: TValue,\n position: QueuePosition = this._options.addItemsTo,\n runOnUpdate: boolean = true,\n ): boolean {\n if (this.getIsFull()) {\n this._rejectionCount++\n this._options.onReject(item, this)\n return false\n }\n\n if (this._options.getPriority !== defaultOptions.getPriority) {\n // If custom priority function is provided, insert based on priority\n const priority = this._options.getPriority(item)\n const insertIndex = this._items.findIndex(\n (existing) => this._options.getPriority(existing) > priority,\n )\n\n if (insertIndex === -1) {\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n } else {\n this._items.splice(insertIndex, 0, item)\n this._itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n // Default FIFO/LIFO behavior\n if (position === 'front') {\n this._items.unshift(item)\n this._itemTimestamps.unshift(Date.now())\n } else {\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n }\n }\n\n if (this._running && !this._pendingTick) {\n this._pendingTick = true\n this.tick()\n }\n if (runOnUpdate) {\n this._options.onItemsChange(this)\n }\n return true\n }\n\n /**\n * Removes and returns an item from the queuer using shift (default) or pop\n *\n * @example\n * ```ts\n * // Standard FIFO queuer\n * queuer.getNextItem()\n * // Stack-like behavior (LIFO)\n * queuer.getNextItem('back')\n * ```\n */\n getNextItem(\n position: QueuePosition = this._options.getItemsFrom,\n ): TValue | undefined {\n let item: TValue | undefined\n\n if (position === 'front') {\n item = this._items.shift()\n this._itemTimestamps.shift()\n } else {\n item = this._items.pop()\n this._itemTimestamps.pop()\n }\n\n if (item !== undefined) {\n this._executionCount++\n this._options.onItemsChange(this)\n this._options.onGetNextItem(item, this)\n }\n return item\n }\n\n /**\n * Returns an item without removing it\n *\n * @example\n * ```ts\n * // Look at next item to getNextItem\n * queuer.getPeek()\n * // Look at last item (like stack top)\n * queuer.getPeek('back')\n * ```\n */\n getPeek(\n position: QueuePosition = this._options.getItemsFrom,\n ): TValue | undefined {\n if (position === 'front') {\n return this._items[0]\n }\n return this._items[this._items.length - 1]\n }\n\n /**\n * Returns true if the queuer is empty\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the queuer is full\n */\n getIsFull(): boolean {\n return this._items.length >= this._options.maxSize\n }\n\n /**\n * Returns the current size of the queuer\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns a copy of all items in the queuer\n */\n getAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of items that have been removed from the queuer\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of items that have been rejected from the queuer\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of items that have expired from the queuer\n */\n getExpirationCount(): number {\n return this._expirationCount\n }\n\n /**\n * Returns true if the queuer is running\n */\n getIsRunning() {\n return this._running\n }\n\n /**\n * Returns true if the queuer is running but has no items to process\n */\n getIsIdle() {\n return this._running && this.getIsEmpty()\n }\n}\n\n/**\n * Creates a queue that processes items in a queuer immediately upon addition.\n * Items are processed sequentially in FIFO order by default.\n *\n * This is a simplified wrapper around the Queuer class that only exposes the\n * `addItem` method. This queue is always running and will process items as they are added.\n * For more control over queuer processing, use the Queuer class\n * directly which provides methods like `start`, `stop`, `reset`, and more.\n *\n * @example\n * ```ts\n * // Basic sequential processing\n * const processItems = queuer<number>({\n * wait: 1000,\n * onItemsChange: (queuer) => console.log(queuer.getAllItems())\n * })\n * processItems(1) // Logs: 1\n * processItems(2) // Logs: 2 after 1 completes\n *\n * // Priority queuer\n * const processPriority = queuer<number>({\n * process: async (n) => console.log(n),\n * getPriority: n => n // Higher numbers processed first\n * })\n * processPriority(1)\n * processPriority(3) // Processed before 1\n * ```\n */\nexport function queue<TValue>(options: QueuerOptions<TValue> = {}) {\n const queuer = new Queuer<TValue>({ ...options, started: true })\n return queuer.addItem.bind(queuer)\n}\n"],"names":[],"mappings":"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"}
@@ -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
  */
@@ -83,6 +97,11 @@ export type QueuePosition = 'front' | 'back';
83
97
  * - wait: configurable delay between processing items
84
98
  * - onItemsChange/onGetNextItem: callbacks for monitoring queuer state
85
99
  *
100
+ * Supports item expiration to clear stale items from the queuer
101
+ * - expirationDuration: maximum time in milliseconds that an item can stay in the queue
102
+ * - getIsExpired: function to override default expiration behavior
103
+ * - onExpire: callback for when an item expires
104
+ *
86
105
  * @example
87
106
  * ```ts
88
107
  * // FIFO queuer
@@ -106,8 +125,10 @@ export type QueuePosition = 'front' | 'back';
106
125
  export declare class Queuer<TValue> {
107
126
  private _options;
108
127
  private _items;
128
+ private _itemTimestamps;
109
129
  private _executionCount;
110
130
  private _rejectionCount;
131
+ private _expirationCount;
111
132
  private _onItemsChanges;
112
133
  private _running;
113
134
  private _pendingTick;
@@ -116,7 +137,7 @@ export declare class Queuer<TValue> {
116
137
  * Updates the queuer options
117
138
  * Returns the new options state
118
139
  */
119
- setOptions(newOptions: Partial<QueuerOptions<TValue>>): QueuerOptions<TValue>;
140
+ setOptions(newOptions: Partial<QueuerOptions<TValue>>): void;
120
141
  /**
121
142
  * Returns the current queuer options
122
143
  */
@@ -125,6 +146,10 @@ export declare class Queuer<TValue> {
125
146
  * Processes items in the queuer
126
147
  */
127
148
  private tick;
149
+ /**
150
+ * Checks for and removes expired items from the queuer
151
+ */
152
+ private checkExpiredItems;
128
153
  /**
129
154
  * Stops the queuer from processing items
130
155
  */
@@ -194,6 +219,10 @@ export declare class Queuer<TValue> {
194
219
  * Returns the number of items that have been rejected from the queuer
195
220
  */
196
221
  getRejectionCount(): number;
222
+ /**
223
+ * Returns the number of items that have expired from the queuer
224
+ */
225
+ getExpirationCount(): number;
197
226
  /**
198
227
  * Returns true if the queuer is running
199
228
  */
@@ -25,11 +25,7 @@ class RateLimiter {
25
25
  * Returns the new options state
26
26
  */
27
27
  setOptions(newOptions) {
28
- this._options = {
29
- ...this._options,
30
- ...newOptions
31
- };
32
- return this._options;
28
+ this._options = { ...this._options, ...newOptions };
33
29
  }
34
30
  /**
35
31
  * Returns the current rate limiter options
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limiter.cjs","sources":["../../src/rate-limiter.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\n/**\n * Options for configuring a rate-limited function\n */\nexport interface RateLimiterOptions<\n TFn extends AnyFunction,\n TArgs extends Parameters<TFn>,\n> {\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean\n /**\n * Maximum number of executions allowed within the time window\n */\n limit: number\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (rateLimiter: RateLimiter<TFn, TArgs>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: RateLimiter<TFn, TArgs>) => void\n /**\n * Time window in milliseconds within which the limit applies\n */\n window: number\n}\n\nconst defaultOptions: Required<RateLimiterOptions<any, any>> = {\n enabled: true,\n limit: 1,\n onExecute: () => {},\n onReject: () => {},\n window: 0,\n}\n\n/**\n * A class that creates a rate-limited function.\n *\n * Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * For smoother execution patterns, consider using:\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * { limit: 5, window: 1000 } // 5 calls per second\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<\n TFn extends AnyFunction,\n TArgs extends Parameters<TFn>,\n> {\n private _executionCount = 0\n private _rejectionCount = 0\n private _executionTimes: Array<number> = []\n private _options: RateLimiterOptions<TFn, TArgs>\n\n constructor(\n private fn: TFn,\n initialOptions: RateLimiterOptions<TFn, TArgs>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the rate limiter options\n * Returns the new options state\n */\n setOptions(\n newOptions: Partial<RateLimiterOptions<TFn, TArgs>>,\n ): RateLimiterOptions<TFn, TArgs> {\n this._options = {\n ...this._options,\n ...newOptions,\n }\n return this._options\n }\n\n /**\n * Returns the current rate limiter options\n */\n getOptions(): Required<RateLimiterOptions<TFn, TArgs>> {\n return this._options as Required<RateLimiterOptions<TFn, TArgs>>\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return true\n * rateLimiter.maybeExecute('arg1', 'arg2'); // true\n *\n * // Additional calls within the window will return false\n * rateLimiter.maybeExecute('arg1', 'arg2'); // false\n * ```\n */\n maybeExecute(...args: TArgs): boolean {\n this.cleanupOldExecutions()\n\n if (this._executionTimes.length < this._options.limit) {\n this.executeFunction(...args)\n return true\n }\n\n this.rejectFunction()\n\n return false\n }\n\n private executeFunction(...args: TArgs): void {\n if (!this._options.enabled) return\n const now = Date.now()\n this._executionCount++\n this._executionTimes.push(now)\n this.fn(...args) // execute the function\n this._options.onExecute?.(this)\n }\n\n private rejectFunction(): void {\n this._rejectionCount++\n if (this._options.onReject) {\n this._options.onReject(this)\n }\n }\n\n private cleanupOldExecutions(): void {\n const now = Date.now()\n const windowStart = now - this._options.window\n this._executionTimes = this._executionTimes.filter(\n (time) => time > windowStart,\n )\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of times the function has been rejected\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow(): number {\n this.cleanupOldExecutions()\n return Math.max(0, this._options.limit - this._executionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n */\n getMsUntilNextWindow(): number {\n const oldestExecution = Math.min(...this._executionTimes)\n return oldestExecution + this._options.window - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset(): void {\n this._executionTimes = []\n this._executionCount = 0\n this._rejectionCount = 0\n }\n}\n\n/**\n * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute\n * const rateLimited = rateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function rateLimit<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: Omit<RateLimiterOptions<TFn, Parameters<TFn>>, 'enabled'>,\n) {\n const rateLimiter = new RateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute.bind(rateLimiter)\n}\n"],"names":[],"mappings":";;AAgCA,MAAM,iBAAyD;AAAA,EAC7D,SAAS;AAAA,EACT,OAAO;AAAA,EACP,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU,MAAM;AAAA,EAAC;AAAA,EACjB,QAAQ;AACV;AA2BO,MAAM,YAGX;AAAA,EAMA,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AANV,SAAQ,kBAAkB;AAC1B,SAAQ,kBAAkB;AAC1B,SAAQ,kBAAiC,CAAC;AAOxC,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,WACE,YACgC;AAChC,SAAK,WAAW;AAAA,MACd,GAAG,KAAK;AAAA,MACR,GAAG;AAAA,IACL;AACA,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAuD;AACrD,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBd,gBAAgB,MAAsB;AACpC,SAAK,qBAAqB;AAE1B,QAAI,KAAK,gBAAgB,SAAS,KAAK,SAAS,OAAO;AAChD,WAAA,gBAAgB,GAAG,IAAI;AACrB,aAAA;AAAA,IAAA;AAGT,SAAK,eAAe;AAEb,WAAA;AAAA,EAAA;AAAA,EAGD,mBAAmB,MAAmB;;AACxC,QAAA,CAAC,KAAK,SAAS,QAAS;AACtB,UAAA,MAAM,KAAK,IAAI;AAChB,SAAA;AACA,SAAA,gBAAgB,KAAK,GAAG;AACxB,SAAA,GAAG,GAAG,IAAI;AACV,qBAAA,UAAS,cAAT,4BAAqB;AAAA,EAAI;AAAA,EAGxB,iBAAuB;AACxB,SAAA;AACD,QAAA,KAAK,SAAS,UAAU;AACrB,WAAA,SAAS,SAAS,IAAI;AAAA,IAAA;AAAA,EAC7B;AAAA,EAGM,uBAA6B;AAC7B,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,cAAc,MAAM,KAAK,SAAS;AACnC,SAAA,kBAAkB,KAAK,gBAAgB;AAAA,MAC1C,CAAC,SAAS,OAAO;AAAA,IACnB;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AAC7B,SAAK,qBAAqB;AACnB,WAAA,KAAK,IAAI,GAAG,KAAK,SAAS,QAAQ,KAAK,gBAAgB,MAAM;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMtE,uBAA+B;AAC7B,UAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,eAAe;AACxD,WAAO,kBAAkB,KAAK,SAAS,SAAS,KAAK,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM3D,QAAc;AACZ,SAAK,kBAAkB,CAAC;AACxB,SAAK,kBAAkB;AACvB,SAAK,kBAAkB;AAAA,EAAA;AAE3B;AAgCgB,SAAA,UACd,IACA,gBACA;AACA,QAAM,cAAc,IAAI,YAAY,IAAI,cAAc;AAC/C,SAAA,YAAY,aAAa,KAAK,WAAW;AAClD;;;"}
1
+ {"version":3,"file":"rate-limiter.cjs","sources":["../../src/rate-limiter.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\n/**\n * Options for configuring a rate-limited function\n */\nexport interface RateLimiterOptions<TFn extends AnyFunction> {\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean\n /**\n * Maximum number of executions allowed within the time window\n */\n limit: number\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Time window in milliseconds within which the limit applies\n */\n window: number\n}\n\nconst defaultOptions: Required<RateLimiterOptions<any>> = {\n enabled: true,\n limit: 1,\n onExecute: () => {},\n onReject: () => {},\n window: 0,\n}\n\n/**\n * A class that creates a rate-limited function.\n *\n * Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * For smoother execution patterns, consider using:\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * { limit: 5, window: 1000 } // 5 calls per second\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n private _executionCount = 0\n private _rejectionCount = 0\n private _executionTimes: Array<number> = []\n private _options: RateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the rate limiter options\n * Returns the new options state\n */\n setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current rate limiter options\n */\n getOptions(): Required<RateLimiterOptions<TFn>> {\n return this._options as Required<RateLimiterOptions<TFn>>\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return true\n * rateLimiter.maybeExecute('arg1', 'arg2'); // true\n *\n * // Additional calls within the window will return false\n * rateLimiter.maybeExecute('arg1', 'arg2'); // false\n * ```\n */\n maybeExecute(...args: Parameters<TFn>): boolean {\n this.cleanupOldExecutions()\n\n if (this._executionTimes.length < this._options.limit) {\n this.executeFunction(...args)\n return true\n }\n\n this.rejectFunction()\n\n return false\n }\n\n private executeFunction(...args: Parameters<TFn>): void {\n if (!this._options.enabled) return\n const now = Date.now()\n this._executionCount++\n this._executionTimes.push(now)\n this.fn(...args) // execute the function\n this._options.onExecute?.(this)\n }\n\n private rejectFunction(): void {\n this._rejectionCount++\n if (this._options.onReject) {\n this._options.onReject(this)\n }\n }\n\n private cleanupOldExecutions(): void {\n const now = Date.now()\n const windowStart = now - this._options.window\n this._executionTimes = this._executionTimes.filter(\n (time) => time > windowStart,\n )\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of times the function has been rejected\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow(): number {\n this.cleanupOldExecutions()\n return Math.max(0, this._options.limit - this._executionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n */\n getMsUntilNextWindow(): number {\n const oldestExecution = Math.min(...this._executionTimes)\n return oldestExecution + this._options.window - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset(): void {\n this._executionTimes = []\n this._executionCount = 0\n this._rejectionCount = 0\n }\n}\n\n/**\n * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute\n * const rateLimited = rateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function rateLimit<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: Omit<RateLimiterOptions<TFn>, 'enabled'>,\n) {\n const rateLimiter = new RateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute.bind(rateLimiter)\n}\n"],"names":[],"mappings":";;AA6BA,MAAM,iBAAoD;AAAA,EACxD,SAAS;AAAA,EACT,OAAO;AAAA,EACP,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU,MAAM;AAAA,EAAC;AAAA,EACjB,QAAQ;AACV;AA2BO,MAAM,YAAqC;AAAA,EAMhD,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AANV,SAAQ,kBAAkB;AAC1B,SAAQ,kBAAkB;AAC1B,SAAQ,kBAAiC,CAAC;AAOxC,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,WAAW,YAAoD;AAC7D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMpD,aAAgD;AAC9C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBd,gBAAgB,MAAgC;AAC9C,SAAK,qBAAqB;AAE1B,QAAI,KAAK,gBAAgB,SAAS,KAAK,SAAS,OAAO;AAChD,WAAA,gBAAgB,GAAG,IAAI;AACrB,aAAA;AAAA,IAAA;AAGT,SAAK,eAAe;AAEb,WAAA;AAAA,EAAA;AAAA,EAGD,mBAAmB,MAA6B;;AAClD,QAAA,CAAC,KAAK,SAAS,QAAS;AACtB,UAAA,MAAM,KAAK,IAAI;AAChB,SAAA;AACA,SAAA,gBAAgB,KAAK,GAAG;AACxB,SAAA,GAAG,GAAG,IAAI;AACV,qBAAA,UAAS,cAAT,4BAAqB;AAAA,EAAI;AAAA,EAGxB,iBAAuB;AACxB,SAAA;AACD,QAAA,KAAK,SAAS,UAAU;AACrB,WAAA,SAAS,SAAS,IAAI;AAAA,IAAA;AAAA,EAC7B;AAAA,EAGM,uBAA6B;AAC7B,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,cAAc,MAAM,KAAK,SAAS;AACnC,SAAA,kBAAkB,KAAK,gBAAgB;AAAA,MAC1C,CAAC,SAAS,OAAO;AAAA,IACnB;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AAC7B,SAAK,qBAAqB;AACnB,WAAA,KAAK,IAAI,GAAG,KAAK,SAAS,QAAQ,KAAK,gBAAgB,MAAM;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMtE,uBAA+B;AAC7B,UAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,eAAe;AACxD,WAAO,kBAAkB,KAAK,SAAS,SAAS,KAAK,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM3D,QAAc;AACZ,SAAK,kBAAkB,CAAC;AACxB,SAAK,kBAAkB;AACvB,SAAK,kBAAkB;AAAA,EAAA;AAE3B;AAgCgB,SAAA,UACd,IACA,gBACA;AACA,QAAM,cAAc,IAAI,YAAY,IAAI,cAAc;AAC/C,SAAA,YAAY,aAAa,KAAK,WAAW;AAClD;;;"}