@alwatr/debounce 1.0.1 → 1.1.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.1.0](https://github.com/Alwatr/nanolib/compare/@alwatr/debounce@1.0.1...@alwatr/debounce@1.1.0) (2025-09-14)
7
+
8
+ ### ✨ Features
9
+
10
+ * add maxWait option to Debouncer for guaranteed execution ([a305a6e](https://github.com/Alwatr/nanolib/commit/a305a6e82f96feebe2e895cdd35676da0f8b79f6))
11
+
12
+ ### 🐛 Bug Fixes
13
+
14
+ * bind flush method in constructor for correct context ([671315c](https://github.com/Alwatr/nanolib/commit/671315cdadab448df660ae53c973eb71d4124699))
15
+
16
+ ### 🔨 Code Refactoring
17
+
18
+ * add missing type import for DebouncerConfig ([d0c5808](https://github.com/Alwatr/nanolib/commit/d0c5808f2b6eb6fe856f8a38f5a79d2c427cf928))
19
+ * improve trigger logic and clean up documentation comments ([cce9162](https://github.com/Alwatr/nanolib/commit/cce9162f2ee1c1fa2978ba04cd1be2eac5302b9e))
20
+ * simplify trailing call logic and ensure lastArgs are cleared after invocation ([e8f74da](https://github.com/Alwatr/nanolib/commit/e8f74da9660c0c44d2704eb863bb082bce9a5978))
21
+ * streamline invoke logic and restore createDebouncer function ([9bdc412](https://github.com/Alwatr/nanolib/commit/9bdc4124cdea3b267c040ec770a002ed64a814ac))
22
+
6
23
  ## [1.0.1](https://github.com/Alwatr/nanolib/compare/@alwatr/debounce@1.0.0...@alwatr/debounce@1.0.1) (2025-09-14)
7
24
 
8
25
  ### 🔨 Code Refactoring
@@ -32,6 +32,7 @@ import type { DebouncerConfig } from './type.ts';
32
32
  export declare class Debouncer<F extends AnyFunction> {
33
33
  private readonly config__;
34
34
  private timerId__?;
35
+ private maxWaitTimerId__?;
35
36
  private lastArgs__?;
36
37
  constructor(config__: DebouncerConfig<F>);
37
38
  /**
@@ -102,32 +103,4 @@ export declare class Debouncer<F extends AnyFunction> {
102
103
  */
103
104
  private invoke__;
104
105
  }
105
- /**
106
- * Factory function for creating a Debouncer instance for better type inference.
107
- * @param config Configuration for the debouncer.
108
- *
109
- * @example
110
- * ```typescript
111
- * const debouncer = createDebouncer({
112
- * func: (text: string) => console.log('Searching:', text),
113
- * delay: 300,
114
- * leading: false,
115
- * trailing: true,
116
- * });
117
- *
118
- * // Debounce search input
119
- * debouncer.trigger('hello');
120
- * debouncer.trigger('hello world'); // Only 'hello world' will log after 300ms
121
- *
122
- * // With custom thisContext
123
- * const obj = { log: (msg: string) => console.log('Obj:', msg) };
124
- * const debouncerWithContext = createDebouncer({
125
- * func: obj.log,
126
- * thisContext: obj,
127
- * delay: 200,
128
- * });
129
- * debouncerWithContext.trigger('test'); // Logs 'Obj: test'
130
- * ```
131
- */
132
- export declare function createDebouncer<F extends AnyFunction>(config: DebouncerConfig<F>): Debouncer<F>;
133
106
  //# sourceMappingURL=debounce.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"debounce.d.ts","sourceRoot":"","sources":["../src/debounce.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,WAAW,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,SAAS,CAAC,CAAC,SAAS,WAAW;IAIvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAH5C,OAAO,CAAC,SAAS,CAAC,CAA0B;IAC5C,OAAO,CAAC,UAAU,CAAC,CAAgB;gBAEC,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC;IAIhE;;;OAGG;IACH,IAAW,SAAS,IAAI,OAAO,CAE9B;IAED;;;;;;;;;;;;;;;;OAgBG;IACI,OAAO,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;IAoB5C;;;;;;;;;;;;;;;OAeG;IACI,MAAM,IAAI,IAAI;IAOrB;;OAEG;IACH,OAAO,CAAC,SAAS;IAKjB;;;;;;;;;;;;;;;;;OAiBG;IACI,KAAK,IAAI,IAAI;IAOpB;;OAEG;IACH,OAAO,CAAC,QAAQ;CAMjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAE/F"}
1
+ {"version":3,"file":"debounce.d.ts","sourceRoot":"","sources":["../src/debounce.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,WAAW,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,SAAS,CAAC,CAAC,SAAS,WAAW;IAKvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJ5C,OAAO,CAAC,SAAS,CAAC,CAA0B;IAC5C,OAAO,CAAC,gBAAgB,CAAC,CAA0B;IACnD,OAAO,CAAC,UAAU,CAAC,CAAgB;gBAEC,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC;IAKhE;;;OAGG;IACH,IAAW,SAAS,IAAI,OAAO,CAE9B;IAED;;;;;;;;;;;;;;;;OAgBG;IACI,OAAO,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;IAwB5C;;;;;;;;;;;;;;;OAeG;IACI,MAAM,IAAI,IAAI;IAUrB;;OAEG;IACH,OAAO,CAAC,SAAS;IAMjB;;;;;;;;;;;;;;;;;OAiBG;IACI,KAAK,IAAI,IAAI;IAOpB;;OAEG;IACH,OAAO,CAAC,QAAQ;CAMjB"}
package/dist/main.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /* @alwatr/debounce v1.0.1 */
1
+ /* @alwatr/debounce v1.1.0 */
2
2
  "use strict";
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -31,6 +31,7 @@ var Debouncer = class {
31
31
  constructor(config__) {
32
32
  this.config__ = config__;
33
33
  this.config__.trailing ??= true;
34
+ this.flush = this.flush.bind(this);
34
35
  }
35
36
  /**
36
37
  * Checks if there is a pending execution scheduled.
@@ -42,7 +43,7 @@ var Debouncer = class {
42
43
  /**
43
44
  * Triggers the debounced function with the stored `thisContext`.
44
45
  * @param args The arguments to pass to the `func`.
45
- *
46
+ *
46
47
  * @example
47
48
  * ```typescript
48
49
  * const debouncer = new Debouncer({
@@ -50,7 +51,7 @@ var Debouncer = class {
50
51
  * delay: 500,
51
52
  * });
52
53
  * debouncer.trigger(42); // Logs after 500ms if not triggered again
53
- *
54
+ *
54
55
  * // Edge case: Rapid triggers only execute the last one
55
56
  * debouncer.trigger(1);
56
57
  * debouncer.trigger(2); // Only 2 will execute after delay
@@ -58,15 +59,19 @@ var Debouncer = class {
58
59
  */
59
60
  trigger(...args) {
60
61
  this.lastArgs__ = args;
61
- const wasPending = this.isPending;
62
- if (wasPending) {
62
+ const firstTrigger = !this.isPending;
63
+ if (firstTrigger) {
64
+ if (this.config__.maxWait) {
65
+ this.maxWaitTimerId__ = setTimeout(this.flush, this.config__.maxWait);
66
+ }
67
+ if (this.config__.leading === true) {
68
+ this.invoke__();
69
+ }
70
+ } else {
63
71
  clearTimeout(this.timerId__);
64
72
  }
65
- if (this.config__.leading === true && !wasPending) {
66
- this.invoke__();
67
- }
68
73
  this.timerId__ = setTimeout(() => {
69
- if (this.config__.trailing === true && wasPending) {
74
+ if (this.config__.trailing === true) {
70
75
  this.invoke__();
71
76
  }
72
77
  this.cleanup__();
@@ -75,7 +80,7 @@ var Debouncer = class {
75
80
  /**
76
81
  * Cancels any pending debounced execution and cleans up internal state.
77
82
  * Useful for stopping execution when the operation is no longer needed (e.g., component unmount).
78
- *
83
+ *
79
84
  * @example
80
85
  * ```typescript
81
86
  * const debouncer = new Debouncer({
@@ -84,14 +89,17 @@ var Debouncer = class {
84
89
  * });
85
90
  * debouncer.trigger();
86
91
  * debouncer.cancel(); // Prevents execution
87
- *
92
+ *
88
93
  * // Note: After cancel, isPending becomes false
89
94
  * ```
90
95
  */
91
96
  cancel() {
92
- if (this.isPending) {
97
+ if (this.timerId__) {
93
98
  clearTimeout(this.timerId__);
94
99
  }
100
+ if (this.maxWaitTimerId__) {
101
+ clearTimeout(this.maxWaitTimerId__);
102
+ }
95
103
  this.cleanup__();
96
104
  }
97
105
  /**
@@ -99,12 +107,13 @@ var Debouncer = class {
99
107
  */
100
108
  cleanup__() {
101
109
  delete this.timerId__;
110
+ delete this.maxWaitTimerId__;
102
111
  delete this.lastArgs__;
103
112
  }
104
113
  /**
105
114
  * Immediately executes the pending function if one exists.
106
115
  * Bypasses the delay and cleans up state. If no pending call, does nothing.
107
- *
116
+ *
108
117
  * @example
109
118
  * ```typescript
110
119
  * const debouncer = new Debouncer({
@@ -113,7 +122,7 @@ var Debouncer = class {
113
122
  * });
114
123
  * debouncer.trigger();
115
124
  * setTimeout(() => debouncer.flush(), 500); // Executes immediately
116
- *
125
+ *
117
126
  * // Edge case: Flush after cancel does nothing
118
127
  * debouncer.cancel();
119
128
  * debouncer.flush(); // No execution
@@ -121,9 +130,9 @@ var Debouncer = class {
121
130
  */
122
131
  flush() {
123
132
  if (this.isPending) {
124
- this.cancel();
125
133
  this.invoke__();
126
134
  }
135
+ this.cancel();
127
136
  }
128
137
  /**
129
138
  * The core execution logic.
@@ -131,9 +140,12 @@ var Debouncer = class {
131
140
  invoke__() {
132
141
  if (this.lastArgs__) {
133
142
  this.config__.func.apply(this.config__.thisContext, this.lastArgs__);
143
+ this.lastArgs__ = void 0;
134
144
  }
135
145
  }
136
146
  };
147
+
148
+ // src/main.ts
137
149
  function createDebouncer(config) {
138
150
  return new Debouncer(config);
139
151
  }
package/dist/main.cjs.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/main.ts", "../src/debounce.ts"],
4
- "sourcesContent": ["export * from './debounce.js';\nexport type * from './type.js';\n", "import type {DebouncerConfig} from './type.ts';\n\n/**\n * A powerful and type-safe Debouncer class.\n * \n * It encapsulates the debouncing logic, state, and provides a rich control API.\n * Debouncing delays function execution until after a specified delay has passed since the last invocation.\n * Useful for optimizing performance in scenarios like search inputs, resize events, or API calls.\n * \n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: (text: string) => console.log('Searching:', text),\n * delay: 300,\n * leading: false,\n * trailing: true,\n * });\n * \n * // Debounce search input\n * debouncer.trigger('hello');\n * debouncer.trigger('hello world'); // Only 'hello world' will log after 300ms\n * \n * // Advanced: With leading edge\n * const leadingDebouncer = new Debouncer({\n * func: () => console.log('Immediate and delayed'),\n * delay: 500,\n * leading: true,\n * trailing: true,\n * });\n * leadingDebouncer.trigger(); // Logs immediately, then again after 500ms if not cancelled\n * ```\n */\nexport class Debouncer<F extends AnyFunction> {\n private timerId__?: number | NodeJS.Timeout;\n private lastArgs__?: Parameters<F>;\n\n public constructor(private readonly config__: DebouncerConfig<F>) {\n this.config__.trailing ??= true;\n }\n\n /**\n * Checks if there is a pending execution scheduled.\n * Returns true if a timer is active, indicating a debounced call is waiting.\n */\n public get isPending(): boolean {\n return this.timerId__ !== undefined;\n }\n\n /**\n * Triggers the debounced function with the stored `thisContext`.\n * @param args The arguments to pass to the `func`.\n * \n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: (value: number) => console.log('Value:', value),\n * delay: 500,\n * });\n * debouncer.trigger(42); // Logs after 500ms if not triggered again\n * \n * // Edge case: Rapid triggers only execute the last one\n * debouncer.trigger(1);\n * debouncer.trigger(2); // Only 2 will execute after delay\n * ```\n */\n public trigger(...args: Parameters<F>): void {\n this.lastArgs__ = args;\n const wasPending = this.isPending;\n\n if (wasPending) {\n clearTimeout(this.timerId__!);\n }\n\n if (this.config__.leading === true && !wasPending) {\n this.invoke__();\n }\n\n this.timerId__ = setTimeout(() => {\n if (this.config__.trailing === true && wasPending) {\n this.invoke__();\n }\n this.cleanup__();\n }, this.config__.delay);\n }\n\n /**\n * Cancels any pending debounced execution and cleans up internal state.\n * Useful for stopping execution when the operation is no longer needed (e.g., component unmount).\n * \n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: () => console.log('Executed'),\n * delay: 1000,\n * });\n * debouncer.trigger();\n * debouncer.cancel(); // Prevents execution\n * \n * // Note: After cancel, isPending becomes false\n * ```\n */\n public cancel(): void {\n if (this.isPending) {\n clearTimeout(this.timerId__!);\n }\n this.cleanup__();\n }\n\n /**\n * Cleans up internal state by deleting timer and arguments.\n */\n private cleanup__(): void {\n delete this.timerId__;\n delete this.lastArgs__;\n }\n\n /**\n * Immediately executes the pending function if one exists.\n * Bypasses the delay and cleans up state. If no pending call, does nothing.\n * \n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: () => console.log('Flushed'),\n * delay: 1000,\n * });\n * debouncer.trigger();\n * setTimeout(() => debouncer.flush(), 500); // Executes immediately\n * \n * // Edge case: Flush after cancel does nothing\n * debouncer.cancel();\n * debouncer.flush(); // No execution\n * ```\n */\n public flush(): void {\n if (this.isPending) {\n this.cancel();\n this.invoke__();\n }\n }\n\n /**\n * The core execution logic.\n */\n private invoke__(): void {\n if (this.lastArgs__) {\n // `thisContext` is now read directly from the stored config.\n this.config__.func.apply(this.config__.thisContext, this.lastArgs__);\n }\n }\n}\n\n/**\n * Factory function for creating a Debouncer instance for better type inference.\n * @param config Configuration for the debouncer.\n * \n * @example\n * ```typescript\n * const debouncer = createDebouncer({\n * func: (text: string) => console.log('Searching:', text),\n * delay: 300,\n * leading: false,\n * trailing: true,\n * });\n * \n * // Debounce search input\n * debouncer.trigger('hello');\n * debouncer.trigger('hello world'); // Only 'hello world' will log after 300ms\n * \n * // With custom thisContext\n * const obj = { log: (msg: string) => console.log('Obj:', msg) };\n * const debouncerWithContext = createDebouncer({\n * func: obj.log,\n * thisContext: obj,\n * delay: 200,\n * });\n * debouncerWithContext.trigger('test'); // Logs 'Obj: test'\n * ```\n */\nexport function createDebouncer<F extends AnyFunction>(config: DebouncerConfig<F>): Debouncer<F> {\n return new Debouncer(config);\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgCO,IAAM,YAAN,MAAuC;AAAA,EAIrC,YAA6B,UAA8B;AAA9B;AAClC,SAAK,SAAS,aAAa;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAW,YAAqB;AAC9B,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBO,WAAW,MAA2B;AAC3C,SAAK,aAAa;AAClB,UAAM,aAAa,KAAK;AAExB,QAAI,YAAY;AACd,mBAAa,KAAK,SAAU;AAAA,IAC9B;AAEA,QAAI,KAAK,SAAS,YAAY,QAAQ,CAAC,YAAY;AACjD,WAAK,SAAS;AAAA,IAChB;AAEA,SAAK,YAAY,WAAW,MAAM;AAChC,UAAI,KAAK,SAAS,aAAa,QAAQ,YAAY;AACjD,aAAK,SAAS;AAAA,MAChB;AACA,WAAK,UAAU;AAAA,IACjB,GAAG,KAAK,SAAS,KAAK;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBO,SAAe;AACpB,QAAI,KAAK,WAAW;AAClB,mBAAa,KAAK,SAAU;AAAA,IAC9B;AACA,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAkB;AACxB,WAAO,KAAK;AACZ,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBO,QAAc;AACnB,QAAI,KAAK,WAAW;AAClB,WAAK,OAAO;AACZ,WAAK,SAAS;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,WAAiB;AACvB,QAAI,KAAK,YAAY;AAEnB,WAAK,SAAS,KAAK,MAAM,KAAK,SAAS,aAAa,KAAK,UAAU;AAAA,IACrE;AAAA,EACF;AACF;AA6BO,SAAS,gBAAuC,QAA0C;AAC/F,SAAO,IAAI,UAAU,MAAM;AAC7B;",
4
+ "sourcesContent": ["import {Debouncer} from './debounce.js';\n\nimport type {DebouncerConfig} from './type.js';\n\nexport * from './debounce.js';\nexport type * from './type.js';\n\n/**\n * Factory function for creating a Debouncer instance for better type inference.\n * @param config Configuration for the debouncer.\n *\n * @example\n * ```typescript\n * const debouncer = createDebouncer({\n * func: (text: string) => console.log('Searching:', text),\n * delay: 300,\n * leading: false,\n * trailing: true,\n * });\n *\n * // Debounce search input\n * debouncer.trigger('hello');\n * debouncer.trigger('hello world'); // Only 'hello world' will log after 300ms\n *\n * // With custom thisContext\n * const obj = { log: (msg: string) => console.log('Obj:', msg) };\n * const debouncerWithContext = createDebouncer({\n * func: obj.log,\n * thisContext: obj,\n * delay: 200,\n * });\n * debouncerWithContext.trigger('test'); // Logs 'Obj: test'\n * ```\n */\nexport function createDebouncer<F extends AnyFunction>(config: DebouncerConfig<F>): Debouncer<F> {\n return new Debouncer(config);\n}\n", "import type {DebouncerConfig} from './type.ts';\n\n/**\n * A powerful and type-safe Debouncer class.\n *\n * It encapsulates the debouncing logic, state, and provides a rich control API.\n * Debouncing delays function execution until after a specified delay has passed since the last invocation.\n * Useful for optimizing performance in scenarios like search inputs, resize events, or API calls.\n *\n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: (text: string) => console.log('Searching:', text),\n * delay: 300,\n * leading: false,\n * trailing: true,\n * });\n *\n * // Debounce search input\n * debouncer.trigger('hello');\n * debouncer.trigger('hello world'); // Only 'hello world' will log after 300ms\n *\n * // Advanced: With leading edge\n * const leadingDebouncer = new Debouncer({\n * func: () => console.log('Immediate and delayed'),\n * delay: 500,\n * leading: true,\n * trailing: true,\n * });\n * leadingDebouncer.trigger(); // Logs immediately, then again after 500ms if not cancelled\n * ```\n */\nexport class Debouncer<F extends AnyFunction> {\n private timerId__?: number | NodeJS.Timeout;\n private maxWaitTimerId__?: number | NodeJS.Timeout;\n private lastArgs__?: Parameters<F>;\n\n public constructor(private readonly config__: DebouncerConfig<F>) {\n this.config__.trailing ??= true;\n this.flush = this.flush.bind(this);\n }\n\n /**\n * Checks if there is a pending execution scheduled.\n * Returns true if a timer is active, indicating a debounced call is waiting.\n */\n public get isPending(): boolean {\n return this.timerId__ !== undefined;\n }\n\n /**\n * Triggers the debounced function with the stored `thisContext`.\n * @param args The arguments to pass to the `func`.\n *\n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: (value: number) => console.log('Value:', value),\n * delay: 500,\n * });\n * debouncer.trigger(42); // Logs after 500ms if not triggered again\n *\n * // Edge case: Rapid triggers only execute the last one\n * debouncer.trigger(1);\n * debouncer.trigger(2); // Only 2 will execute after delay\n * ```\n */\n public trigger(...args: Parameters<F>): void {\n this.lastArgs__ = args; // its an array even if triggered without any args\n const firstTrigger = !this.isPending;\n\n if (firstTrigger) {\n if (this.config__.maxWait) {\n this.maxWaitTimerId__ = setTimeout(this.flush, this.config__.maxWait);\n }\n if (this.config__.leading === true) {\n this.invoke__();\n }\n }\n else {\n clearTimeout(this.timerId__!);\n }\n\n this.timerId__ = setTimeout(() => {\n if (this.config__.trailing === true) {\n this.invoke__();\n }\n this.cleanup__();\n }, this.config__.delay);\n }\n\n /**\n * Cancels any pending debounced execution and cleans up internal state.\n * Useful for stopping execution when the operation is no longer needed (e.g., component unmount).\n *\n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: () => console.log('Executed'),\n * delay: 1000,\n * });\n * debouncer.trigger();\n * debouncer.cancel(); // Prevents execution\n *\n * // Note: After cancel, isPending becomes false\n * ```\n */\n public cancel(): void {\n if (this.timerId__) {\n clearTimeout(this.timerId__);\n }\n if (this.maxWaitTimerId__) {\n clearTimeout(this.maxWaitTimerId__);\n }\n this.cleanup__();\n }\n\n /**\n * Cleans up internal state by deleting timer and arguments.\n */\n private cleanup__(): void {\n delete this.timerId__;\n delete this.maxWaitTimerId__;\n delete this.lastArgs__;\n }\n\n /**\n * Immediately executes the pending function if one exists.\n * Bypasses the delay and cleans up state. If no pending call, does nothing.\n *\n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: () => console.log('Flushed'),\n * delay: 1000,\n * });\n * debouncer.trigger();\n * setTimeout(() => debouncer.flush(), 500); // Executes immediately\n *\n * // Edge case: Flush after cancel does nothing\n * debouncer.cancel();\n * debouncer.flush(); // No execution\n * ```\n */\n public flush(): void {\n if (this.isPending) {\n this.invoke__();\n }\n this.cancel();\n }\n\n /**\n * The core execution logic.\n */\n private invoke__(): void {\n if (this.lastArgs__) { // only call if we have new args (skip trailing call if leading already called)\n this.config__.func.apply(this.config__.thisContext, this.lastArgs__);\n this.lastArgs__ = undefined;\n }\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgCO,IAAM,YAAN,MAAuC;AAAA,EAKrC,YAA6B,UAA8B;AAA9B;AAClC,SAAK,SAAS,aAAa;AAC3B,SAAK,QAAQ,KAAK,MAAM,KAAK,IAAI;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAW,YAAqB;AAC9B,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBO,WAAW,MAA2B;AAC3C,SAAK,aAAa;AAClB,UAAM,eAAe,CAAC,KAAK;AAE3B,QAAI,cAAc;AAChB,UAAI,KAAK,SAAS,SAAS;AACzB,aAAK,mBAAmB,WAAW,KAAK,OAAO,KAAK,SAAS,OAAO;AAAA,MACtE;AACA,UAAI,KAAK,SAAS,YAAY,MAAM;AAClC,aAAK,SAAS;AAAA,MAChB;AAAA,IACF,OACK;AACH,mBAAa,KAAK,SAAU;AAAA,IAC9B;AAEA,SAAK,YAAY,WAAW,MAAM;AAChC,UAAI,KAAK,SAAS,aAAa,MAAM;AACnC,aAAK,SAAS;AAAA,MAChB;AACA,WAAK,UAAU;AAAA,IACjB,GAAG,KAAK,SAAS,KAAK;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBO,SAAe;AACpB,QAAI,KAAK,WAAW;AAClB,mBAAa,KAAK,SAAS;AAAA,IAC7B;AACA,QAAI,KAAK,kBAAkB;AACzB,mBAAa,KAAK,gBAAgB;AAAA,IACpC;AACA,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAkB;AACxB,WAAO,KAAK;AACZ,WAAO,KAAK;AACZ,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBO,QAAc;AACnB,QAAI,KAAK,WAAW;AAClB,WAAK,SAAS;AAAA,IAChB;AACA,SAAK,OAAO;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKQ,WAAiB;AACvB,QAAI,KAAK,YAAY;AACnB,WAAK,SAAS,KAAK,MAAM,KAAK,SAAS,aAAa,KAAK,UAAU;AACnE,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AACF;;;AD9HO,SAAS,gBAAuC,QAA0C;AAC/F,SAAO,IAAI,UAAU,MAAM;AAC7B;",
6
6
  "names": []
7
7
  }
package/dist/main.d.ts CHANGED
@@ -1,3 +1,33 @@
1
+ import { Debouncer } from './debounce.js';
2
+ import type { DebouncerConfig } from './type.js';
1
3
  export * from './debounce.js';
2
4
  export type * from './type.js';
5
+ /**
6
+ * Factory function for creating a Debouncer instance for better type inference.
7
+ * @param config Configuration for the debouncer.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const debouncer = createDebouncer({
12
+ * func: (text: string) => console.log('Searching:', text),
13
+ * delay: 300,
14
+ * leading: false,
15
+ * trailing: true,
16
+ * });
17
+ *
18
+ * // Debounce search input
19
+ * debouncer.trigger('hello');
20
+ * debouncer.trigger('hello world'); // Only 'hello world' will log after 300ms
21
+ *
22
+ * // With custom thisContext
23
+ * const obj = { log: (msg: string) => console.log('Obj:', msg) };
24
+ * const debouncerWithContext = createDebouncer({
25
+ * func: obj.log,
26
+ * thisContext: obj,
27
+ * delay: 200,
28
+ * });
29
+ * debouncerWithContext.trigger('test'); // Logs 'Obj: test'
30
+ * ```
31
+ */
32
+ export declare function createDebouncer<F extends AnyFunction>(config: DebouncerConfig<F>): Debouncer<F>;
3
33
  //# sourceMappingURL=main.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,mBAAmB,WAAW,CAAC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,WAAW,CAAC;AAE/C,cAAc,eAAe,CAAC;AAC9B,mBAAmB,WAAW,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAE/F"}
package/dist/main.mjs CHANGED
@@ -1,10 +1,11 @@
1
- /* @alwatr/debounce v1.0.1 */
1
+ /* @alwatr/debounce v1.1.0 */
2
2
 
3
3
  // src/debounce.ts
4
4
  var Debouncer = class {
5
5
  constructor(config__) {
6
6
  this.config__ = config__;
7
7
  this.config__.trailing ??= true;
8
+ this.flush = this.flush.bind(this);
8
9
  }
9
10
  /**
10
11
  * Checks if there is a pending execution scheduled.
@@ -16,7 +17,7 @@ var Debouncer = class {
16
17
  /**
17
18
  * Triggers the debounced function with the stored `thisContext`.
18
19
  * @param args The arguments to pass to the `func`.
19
- *
20
+ *
20
21
  * @example
21
22
  * ```typescript
22
23
  * const debouncer = new Debouncer({
@@ -24,7 +25,7 @@ var Debouncer = class {
24
25
  * delay: 500,
25
26
  * });
26
27
  * debouncer.trigger(42); // Logs after 500ms if not triggered again
27
- *
28
+ *
28
29
  * // Edge case: Rapid triggers only execute the last one
29
30
  * debouncer.trigger(1);
30
31
  * debouncer.trigger(2); // Only 2 will execute after delay
@@ -32,15 +33,19 @@ var Debouncer = class {
32
33
  */
33
34
  trigger(...args) {
34
35
  this.lastArgs__ = args;
35
- const wasPending = this.isPending;
36
- if (wasPending) {
36
+ const firstTrigger = !this.isPending;
37
+ if (firstTrigger) {
38
+ if (this.config__.maxWait) {
39
+ this.maxWaitTimerId__ = setTimeout(this.flush, this.config__.maxWait);
40
+ }
41
+ if (this.config__.leading === true) {
42
+ this.invoke__();
43
+ }
44
+ } else {
37
45
  clearTimeout(this.timerId__);
38
46
  }
39
- if (this.config__.leading === true && !wasPending) {
40
- this.invoke__();
41
- }
42
47
  this.timerId__ = setTimeout(() => {
43
- if (this.config__.trailing === true && wasPending) {
48
+ if (this.config__.trailing === true) {
44
49
  this.invoke__();
45
50
  }
46
51
  this.cleanup__();
@@ -49,7 +54,7 @@ var Debouncer = class {
49
54
  /**
50
55
  * Cancels any pending debounced execution and cleans up internal state.
51
56
  * Useful for stopping execution when the operation is no longer needed (e.g., component unmount).
52
- *
57
+ *
53
58
  * @example
54
59
  * ```typescript
55
60
  * const debouncer = new Debouncer({
@@ -58,14 +63,17 @@ var Debouncer = class {
58
63
  * });
59
64
  * debouncer.trigger();
60
65
  * debouncer.cancel(); // Prevents execution
61
- *
66
+ *
62
67
  * // Note: After cancel, isPending becomes false
63
68
  * ```
64
69
  */
65
70
  cancel() {
66
- if (this.isPending) {
71
+ if (this.timerId__) {
67
72
  clearTimeout(this.timerId__);
68
73
  }
74
+ if (this.maxWaitTimerId__) {
75
+ clearTimeout(this.maxWaitTimerId__);
76
+ }
69
77
  this.cleanup__();
70
78
  }
71
79
  /**
@@ -73,12 +81,13 @@ var Debouncer = class {
73
81
  */
74
82
  cleanup__() {
75
83
  delete this.timerId__;
84
+ delete this.maxWaitTimerId__;
76
85
  delete this.lastArgs__;
77
86
  }
78
87
  /**
79
88
  * Immediately executes the pending function if one exists.
80
89
  * Bypasses the delay and cleans up state. If no pending call, does nothing.
81
- *
90
+ *
82
91
  * @example
83
92
  * ```typescript
84
93
  * const debouncer = new Debouncer({
@@ -87,7 +96,7 @@ var Debouncer = class {
87
96
  * });
88
97
  * debouncer.trigger();
89
98
  * setTimeout(() => debouncer.flush(), 500); // Executes immediately
90
- *
99
+ *
91
100
  * // Edge case: Flush after cancel does nothing
92
101
  * debouncer.cancel();
93
102
  * debouncer.flush(); // No execution
@@ -95,9 +104,9 @@ var Debouncer = class {
95
104
  */
96
105
  flush() {
97
106
  if (this.isPending) {
98
- this.cancel();
99
107
  this.invoke__();
100
108
  }
109
+ this.cancel();
101
110
  }
102
111
  /**
103
112
  * The core execution logic.
@@ -105,9 +114,12 @@ var Debouncer = class {
105
114
  invoke__() {
106
115
  if (this.lastArgs__) {
107
116
  this.config__.func.apply(this.config__.thisContext, this.lastArgs__);
117
+ this.lastArgs__ = void 0;
108
118
  }
109
119
  }
110
120
  };
121
+
122
+ // src/main.ts
111
123
  function createDebouncer(config) {
112
124
  return new Debouncer(config);
113
125
  }
package/dist/main.mjs.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/debounce.ts"],
4
- "sourcesContent": ["import type {DebouncerConfig} from './type.ts';\n\n/**\n * A powerful and type-safe Debouncer class.\n * \n * It encapsulates the debouncing logic, state, and provides a rich control API.\n * Debouncing delays function execution until after a specified delay has passed since the last invocation.\n * Useful for optimizing performance in scenarios like search inputs, resize events, or API calls.\n * \n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: (text: string) => console.log('Searching:', text),\n * delay: 300,\n * leading: false,\n * trailing: true,\n * });\n * \n * // Debounce search input\n * debouncer.trigger('hello');\n * debouncer.trigger('hello world'); // Only 'hello world' will log after 300ms\n * \n * // Advanced: With leading edge\n * const leadingDebouncer = new Debouncer({\n * func: () => console.log('Immediate and delayed'),\n * delay: 500,\n * leading: true,\n * trailing: true,\n * });\n * leadingDebouncer.trigger(); // Logs immediately, then again after 500ms if not cancelled\n * ```\n */\nexport class Debouncer<F extends AnyFunction> {\n private timerId__?: number | NodeJS.Timeout;\n private lastArgs__?: Parameters<F>;\n\n public constructor(private readonly config__: DebouncerConfig<F>) {\n this.config__.trailing ??= true;\n }\n\n /**\n * Checks if there is a pending execution scheduled.\n * Returns true if a timer is active, indicating a debounced call is waiting.\n */\n public get isPending(): boolean {\n return this.timerId__ !== undefined;\n }\n\n /**\n * Triggers the debounced function with the stored `thisContext`.\n * @param args The arguments to pass to the `func`.\n * \n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: (value: number) => console.log('Value:', value),\n * delay: 500,\n * });\n * debouncer.trigger(42); // Logs after 500ms if not triggered again\n * \n * // Edge case: Rapid triggers only execute the last one\n * debouncer.trigger(1);\n * debouncer.trigger(2); // Only 2 will execute after delay\n * ```\n */\n public trigger(...args: Parameters<F>): void {\n this.lastArgs__ = args;\n const wasPending = this.isPending;\n\n if (wasPending) {\n clearTimeout(this.timerId__!);\n }\n\n if (this.config__.leading === true && !wasPending) {\n this.invoke__();\n }\n\n this.timerId__ = setTimeout(() => {\n if (this.config__.trailing === true && wasPending) {\n this.invoke__();\n }\n this.cleanup__();\n }, this.config__.delay);\n }\n\n /**\n * Cancels any pending debounced execution and cleans up internal state.\n * Useful for stopping execution when the operation is no longer needed (e.g., component unmount).\n * \n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: () => console.log('Executed'),\n * delay: 1000,\n * });\n * debouncer.trigger();\n * debouncer.cancel(); // Prevents execution\n * \n * // Note: After cancel, isPending becomes false\n * ```\n */\n public cancel(): void {\n if (this.isPending) {\n clearTimeout(this.timerId__!);\n }\n this.cleanup__();\n }\n\n /**\n * Cleans up internal state by deleting timer and arguments.\n */\n private cleanup__(): void {\n delete this.timerId__;\n delete this.lastArgs__;\n }\n\n /**\n * Immediately executes the pending function if one exists.\n * Bypasses the delay and cleans up state. If no pending call, does nothing.\n * \n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: () => console.log('Flushed'),\n * delay: 1000,\n * });\n * debouncer.trigger();\n * setTimeout(() => debouncer.flush(), 500); // Executes immediately\n * \n * // Edge case: Flush after cancel does nothing\n * debouncer.cancel();\n * debouncer.flush(); // No execution\n * ```\n */\n public flush(): void {\n if (this.isPending) {\n this.cancel();\n this.invoke__();\n }\n }\n\n /**\n * The core execution logic.\n */\n private invoke__(): void {\n if (this.lastArgs__) {\n // `thisContext` is now read directly from the stored config.\n this.config__.func.apply(this.config__.thisContext, this.lastArgs__);\n }\n }\n}\n\n/**\n * Factory function for creating a Debouncer instance for better type inference.\n * @param config Configuration for the debouncer.\n * \n * @example\n * ```typescript\n * const debouncer = createDebouncer({\n * func: (text: string) => console.log('Searching:', text),\n * delay: 300,\n * leading: false,\n * trailing: true,\n * });\n * \n * // Debounce search input\n * debouncer.trigger('hello');\n * debouncer.trigger('hello world'); // Only 'hello world' will log after 300ms\n * \n * // With custom thisContext\n * const obj = { log: (msg: string) => console.log('Obj:', msg) };\n * const debouncerWithContext = createDebouncer({\n * func: obj.log,\n * thisContext: obj,\n * delay: 200,\n * });\n * debouncerWithContext.trigger('test'); // Logs 'Obj: test'\n * ```\n */\nexport function createDebouncer<F extends AnyFunction>(config: DebouncerConfig<F>): Debouncer<F> {\n return new Debouncer(config);\n}\n"],
5
- "mappings": ";;;AAgCO,IAAM,YAAN,MAAuC;AAAA,EAIrC,YAA6B,UAA8B;AAA9B;AAClC,SAAK,SAAS,aAAa;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAW,YAAqB;AAC9B,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBO,WAAW,MAA2B;AAC3C,SAAK,aAAa;AAClB,UAAM,aAAa,KAAK;AAExB,QAAI,YAAY;AACd,mBAAa,KAAK,SAAU;AAAA,IAC9B;AAEA,QAAI,KAAK,SAAS,YAAY,QAAQ,CAAC,YAAY;AACjD,WAAK,SAAS;AAAA,IAChB;AAEA,SAAK,YAAY,WAAW,MAAM;AAChC,UAAI,KAAK,SAAS,aAAa,QAAQ,YAAY;AACjD,aAAK,SAAS;AAAA,MAChB;AACA,WAAK,UAAU;AAAA,IACjB,GAAG,KAAK,SAAS,KAAK;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBO,SAAe;AACpB,QAAI,KAAK,WAAW;AAClB,mBAAa,KAAK,SAAU;AAAA,IAC9B;AACA,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAkB;AACxB,WAAO,KAAK;AACZ,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBO,QAAc;AACnB,QAAI,KAAK,WAAW;AAClB,WAAK,OAAO;AACZ,WAAK,SAAS;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,WAAiB;AACvB,QAAI,KAAK,YAAY;AAEnB,WAAK,SAAS,KAAK,MAAM,KAAK,SAAS,aAAa,KAAK,UAAU;AAAA,IACrE;AAAA,EACF;AACF;AA6BO,SAAS,gBAAuC,QAA0C;AAC/F,SAAO,IAAI,UAAU,MAAM;AAC7B;",
3
+ "sources": ["../src/debounce.ts", "../src/main.ts"],
4
+ "sourcesContent": ["import type {DebouncerConfig} from './type.ts';\n\n/**\n * A powerful and type-safe Debouncer class.\n *\n * It encapsulates the debouncing logic, state, and provides a rich control API.\n * Debouncing delays function execution until after a specified delay has passed since the last invocation.\n * Useful for optimizing performance in scenarios like search inputs, resize events, or API calls.\n *\n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: (text: string) => console.log('Searching:', text),\n * delay: 300,\n * leading: false,\n * trailing: true,\n * });\n *\n * // Debounce search input\n * debouncer.trigger('hello');\n * debouncer.trigger('hello world'); // Only 'hello world' will log after 300ms\n *\n * // Advanced: With leading edge\n * const leadingDebouncer = new Debouncer({\n * func: () => console.log('Immediate and delayed'),\n * delay: 500,\n * leading: true,\n * trailing: true,\n * });\n * leadingDebouncer.trigger(); // Logs immediately, then again after 500ms if not cancelled\n * ```\n */\nexport class Debouncer<F extends AnyFunction> {\n private timerId__?: number | NodeJS.Timeout;\n private maxWaitTimerId__?: number | NodeJS.Timeout;\n private lastArgs__?: Parameters<F>;\n\n public constructor(private readonly config__: DebouncerConfig<F>) {\n this.config__.trailing ??= true;\n this.flush = this.flush.bind(this);\n }\n\n /**\n * Checks if there is a pending execution scheduled.\n * Returns true if a timer is active, indicating a debounced call is waiting.\n */\n public get isPending(): boolean {\n return this.timerId__ !== undefined;\n }\n\n /**\n * Triggers the debounced function with the stored `thisContext`.\n * @param args The arguments to pass to the `func`.\n *\n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: (value: number) => console.log('Value:', value),\n * delay: 500,\n * });\n * debouncer.trigger(42); // Logs after 500ms if not triggered again\n *\n * // Edge case: Rapid triggers only execute the last one\n * debouncer.trigger(1);\n * debouncer.trigger(2); // Only 2 will execute after delay\n * ```\n */\n public trigger(...args: Parameters<F>): void {\n this.lastArgs__ = args; // its an array even if triggered without any args\n const firstTrigger = !this.isPending;\n\n if (firstTrigger) {\n if (this.config__.maxWait) {\n this.maxWaitTimerId__ = setTimeout(this.flush, this.config__.maxWait);\n }\n if (this.config__.leading === true) {\n this.invoke__();\n }\n }\n else {\n clearTimeout(this.timerId__!);\n }\n\n this.timerId__ = setTimeout(() => {\n if (this.config__.trailing === true) {\n this.invoke__();\n }\n this.cleanup__();\n }, this.config__.delay);\n }\n\n /**\n * Cancels any pending debounced execution and cleans up internal state.\n * Useful for stopping execution when the operation is no longer needed (e.g., component unmount).\n *\n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: () => console.log('Executed'),\n * delay: 1000,\n * });\n * debouncer.trigger();\n * debouncer.cancel(); // Prevents execution\n *\n * // Note: After cancel, isPending becomes false\n * ```\n */\n public cancel(): void {\n if (this.timerId__) {\n clearTimeout(this.timerId__);\n }\n if (this.maxWaitTimerId__) {\n clearTimeout(this.maxWaitTimerId__);\n }\n this.cleanup__();\n }\n\n /**\n * Cleans up internal state by deleting timer and arguments.\n */\n private cleanup__(): void {\n delete this.timerId__;\n delete this.maxWaitTimerId__;\n delete this.lastArgs__;\n }\n\n /**\n * Immediately executes the pending function if one exists.\n * Bypasses the delay and cleans up state. If no pending call, does nothing.\n *\n * @example\n * ```typescript\n * const debouncer = new Debouncer({\n * func: () => console.log('Flushed'),\n * delay: 1000,\n * });\n * debouncer.trigger();\n * setTimeout(() => debouncer.flush(), 500); // Executes immediately\n *\n * // Edge case: Flush after cancel does nothing\n * debouncer.cancel();\n * debouncer.flush(); // No execution\n * ```\n */\n public flush(): void {\n if (this.isPending) {\n this.invoke__();\n }\n this.cancel();\n }\n\n /**\n * The core execution logic.\n */\n private invoke__(): void {\n if (this.lastArgs__) { // only call if we have new args (skip trailing call if leading already called)\n this.config__.func.apply(this.config__.thisContext, this.lastArgs__);\n this.lastArgs__ = undefined;\n }\n }\n}\n", "import {Debouncer} from './debounce.js';\n\nimport type {DebouncerConfig} from './type.js';\n\nexport * from './debounce.js';\nexport type * from './type.js';\n\n/**\n * Factory function for creating a Debouncer instance for better type inference.\n * @param config Configuration for the debouncer.\n *\n * @example\n * ```typescript\n * const debouncer = createDebouncer({\n * func: (text: string) => console.log('Searching:', text),\n * delay: 300,\n * leading: false,\n * trailing: true,\n * });\n *\n * // Debounce search input\n * debouncer.trigger('hello');\n * debouncer.trigger('hello world'); // Only 'hello world' will log after 300ms\n *\n * // With custom thisContext\n * const obj = { log: (msg: string) => console.log('Obj:', msg) };\n * const debouncerWithContext = createDebouncer({\n * func: obj.log,\n * thisContext: obj,\n * delay: 200,\n * });\n * debouncerWithContext.trigger('test'); // Logs 'Obj: test'\n * ```\n */\nexport function createDebouncer<F extends AnyFunction>(config: DebouncerConfig<F>): Debouncer<F> {\n return new Debouncer(config);\n}\n"],
5
+ "mappings": ";;;AAgCO,IAAM,YAAN,MAAuC;AAAA,EAKrC,YAA6B,UAA8B;AAA9B;AAClC,SAAK,SAAS,aAAa;AAC3B,SAAK,QAAQ,KAAK,MAAM,KAAK,IAAI;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAW,YAAqB;AAC9B,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBO,WAAW,MAA2B;AAC3C,SAAK,aAAa;AAClB,UAAM,eAAe,CAAC,KAAK;AAE3B,QAAI,cAAc;AAChB,UAAI,KAAK,SAAS,SAAS;AACzB,aAAK,mBAAmB,WAAW,KAAK,OAAO,KAAK,SAAS,OAAO;AAAA,MACtE;AACA,UAAI,KAAK,SAAS,YAAY,MAAM;AAClC,aAAK,SAAS;AAAA,MAChB;AAAA,IACF,OACK;AACH,mBAAa,KAAK,SAAU;AAAA,IAC9B;AAEA,SAAK,YAAY,WAAW,MAAM;AAChC,UAAI,KAAK,SAAS,aAAa,MAAM;AACnC,aAAK,SAAS;AAAA,MAChB;AACA,WAAK,UAAU;AAAA,IACjB,GAAG,KAAK,SAAS,KAAK;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBO,SAAe;AACpB,QAAI,KAAK,WAAW;AAClB,mBAAa,KAAK,SAAS;AAAA,IAC7B;AACA,QAAI,KAAK,kBAAkB;AACzB,mBAAa,KAAK,gBAAgB;AAAA,IACpC;AACA,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAkB;AACxB,WAAO,KAAK;AACZ,WAAO,KAAK;AACZ,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBO,QAAc;AACnB,QAAI,KAAK,WAAW;AAClB,WAAK,SAAS;AAAA,IAChB;AACA,SAAK,OAAO;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKQ,WAAiB;AACvB,QAAI,KAAK,YAAY;AACnB,WAAK,SAAS,KAAK,MAAM,KAAK,SAAS,aAAa,KAAK,UAAU;AACnE,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AACF;;;AC9HO,SAAS,gBAAuC,QAA0C;AAC/F,SAAO,IAAI,UAAU,MAAM;AAC7B;",
6
6
  "names": []
7
7
  }
package/dist/type.d.ts CHANGED
@@ -25,6 +25,12 @@ export interface DebouncerConfig<F extends AnyFunction> {
25
25
  * Must be a positive integer; affects performance and responsiveness.
26
26
  */
27
27
  delay: number;
28
+ /**
29
+ * The maximum time the `func` is allowed to be delayed before it's invoked.
30
+ * This is useful for guaranteeing execution of a function that's continuously triggered.
31
+ * If set, the function will be called after `maxWait` milliseconds, even if triggers are still occurring.
32
+ */
33
+ maxWait?: number;
28
34
  /**
29
35
  * If `true`, the function is called on the leading edge of the timeout.
30
36
  * Useful for immediate feedback (e.g., button press).
@@ -1 +1 @@
1
- {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,WAAW;IACpD;;;OAGG;IACH,IAAI,EAAE,CAAC,CAAC;IAER;;;;OAIG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAEnC;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB"}
1
+ {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,WAAW;IACpD;;;OAGG;IACH,IAAI,EAAE,CAAC,CAAC;IAER;;;;OAIG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAEnC;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alwatr/debounce",
3
3
  "description": "A powerful, modern, and type-safe debouncer utility designed for high-performance applications. It's framework-agnostic, works seamlessly in both Node.js and browsers, and provides a rich API for fine-grained control over function execution.",
4
- "version": "1.0.1",
4
+ "version": "1.1.0",
5
5
  "author": "S. Ali Mihandoost <ali.mihandoost@gmail.com>",
6
6
  "bugs": "https://github.com/Alwatr/nanolib/issues",
7
7
  "devDependencies": {
@@ -9,6 +9,7 @@
9
9
  "@alwatr/prettier-config": "5.0.3",
10
10
  "@alwatr/tsconfig-base": "6.0.1",
11
11
  "@alwatr/type-helper": "6.0.2",
12
+ "@jest/globals": "^30.1.2",
12
13
  "@types/node": "^22.18.3",
13
14
  "typescript": "^5.9.2"
14
15
  },
@@ -77,5 +78,5 @@
77
78
  },
78
79
  "type": "module",
79
80
  "types": "./dist/main.d.ts",
80
- "gitHead": "0ee186f1ec8973608f06155e739d55ab5d4bd816"
81
+ "gitHead": "4cb799bb384e7482c9cdb32eafcdc91cf52c26d6"
81
82
  }
@@ -0,0 +1,388 @@
1
+ import {describe, beforeEach, afterEach, it, expect, jest} from '@jest/globals';
2
+ import {createDebouncer} from '@alwatr/debounce';
3
+
4
+ describe('Debouncer', () => {
5
+ /**
6
+ * @type {import("jest-mock").Mock<import("jest-mock").UnknownFunction>}
7
+ */
8
+ let mockFunc;
9
+ /**
10
+ * @type {import("@alwatr/debounce").Debouncer<typeof mockFunc>}
11
+ */
12
+ let debouncer;
13
+
14
+ beforeEach(() => {
15
+ mockFunc = jest.fn();
16
+ jest.useFakeTimers();
17
+ });
18
+
19
+ afterEach(() => {
20
+ jest.clearAllTimers();
21
+ jest.useRealTimers();
22
+ });
23
+
24
+ describe('Basic Trailing Debounce (default)', () => {
25
+ beforeEach(() => {
26
+ debouncer = createDebouncer({
27
+ func: mockFunc,
28
+ delay: 300,
29
+ });
30
+ });
31
+
32
+ it('should execute after delay on single trigger', () => {
33
+ debouncer.trigger('test');
34
+ expect(mockFunc).not.toHaveBeenCalled();
35
+ jest.advanceTimersByTime(300);
36
+ expect(mockFunc).toHaveBeenCalledWith('test');
37
+ });
38
+
39
+ it('should reset delay on multiple triggers', () => {
40
+ debouncer.trigger('first');
41
+ jest.advanceTimersByTime(200);
42
+ debouncer.trigger('second');
43
+ jest.advanceTimersByTime(200);
44
+ expect(mockFunc).not.toHaveBeenCalled();
45
+ jest.advanceTimersByTime(100);
46
+ expect(mockFunc).toHaveBeenCalledWith('second');
47
+ });
48
+
49
+ it('should not execute if cancelled before delay', () => {
50
+ debouncer.trigger('test');
51
+ debouncer.cancel();
52
+ jest.advanceTimersByTime(300);
53
+ expect(mockFunc).not.toHaveBeenCalled();
54
+ });
55
+ });
56
+
57
+ describe('Leading Debounce', () => {
58
+ beforeEach(() => {
59
+ debouncer = createDebouncer({
60
+ func: mockFunc,
61
+ delay: 300,
62
+ leading: true,
63
+ trailing: false,
64
+ });
65
+ });
66
+
67
+ it('should execute immediately on first trigger', () => {
68
+ debouncer.trigger('test');
69
+ expect(mockFunc).toHaveBeenCalledWith('test');
70
+ });
71
+
72
+ it('should not execute again within delay', () => {
73
+ debouncer.trigger('first');
74
+ expect(mockFunc).toHaveBeenCalledTimes(1);
75
+ debouncer.trigger('second');
76
+ jest.advanceTimersByTime(300);
77
+ expect(mockFunc).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ it('should execute again after delay', () => {
81
+ debouncer.trigger('first');
82
+ jest.advanceTimersByTime(300);
83
+ debouncer.trigger('second');
84
+ expect(mockFunc).toHaveBeenCalledTimes(2);
85
+ });
86
+ });
87
+
88
+ describe('Both Leading and Trailing', () => {
89
+ beforeEach(() => {
90
+ debouncer = createDebouncer({
91
+ func: mockFunc,
92
+ delay: 300,
93
+ leading: true,
94
+ trailing: true,
95
+ });
96
+ });
97
+
98
+ it('should execute immediately but not after delay on single trigger', () => {
99
+ debouncer.trigger('test');
100
+ expect(mockFunc).toHaveBeenCalledWith('test');
101
+ jest.advanceTimersByTime(300);
102
+ expect(mockFunc).toHaveBeenCalledTimes(1);
103
+ });
104
+
105
+ it('should execute immediately, then trailing on last trigger', () => {
106
+ debouncer.trigger('first');
107
+ expect(mockFunc).toHaveBeenCalledTimes(1);
108
+ expect(mockFunc).toHaveBeenCalledWith('first');
109
+ jest.advanceTimersByTime(200);
110
+ debouncer.trigger('second');
111
+ jest.advanceTimersByTime(100);
112
+ debouncer.trigger('third');
113
+ jest.advanceTimersByTime(100);
114
+ expect(mockFunc).toHaveBeenCalledTimes(1);
115
+ jest.advanceTimersByTime(300);
116
+ expect(mockFunc).toHaveBeenCalledTimes(2);
117
+ expect(mockFunc).toHaveBeenLastCalledWith('third');
118
+ });
119
+ });
120
+
121
+ describe('Cancel Functionality', () => {
122
+ beforeEach(() => {
123
+ debouncer = createDebouncer({
124
+ func: mockFunc,
125
+ delay: 300,
126
+ });
127
+ });
128
+
129
+ it('should cancel pending execution', () => {
130
+ debouncer.trigger('test');
131
+ expect(debouncer.isPending).toBe(true);
132
+ debouncer.cancel();
133
+ expect(debouncer.isPending).toBe(false);
134
+ jest.advanceTimersByTime(300);
135
+ expect(mockFunc).not.toHaveBeenCalled();
136
+ });
137
+
138
+ it('should handle cancel on leading debounce', () => {
139
+ debouncer = createDebouncer({
140
+ func: mockFunc,
141
+ delay: 300,
142
+ leading: true,
143
+ });
144
+ debouncer.trigger('test');
145
+ expect(mockFunc).toHaveBeenCalledTimes(1);
146
+ debouncer.cancel();
147
+ jest.advanceTimersByTime(300);
148
+ expect(mockFunc).toHaveBeenCalledTimes(1); // No trailing call
149
+ });
150
+ });
151
+
152
+ describe('Flush Functionality', () => {
153
+ beforeEach(() => {
154
+ debouncer = createDebouncer({
155
+ func: mockFunc,
156
+ delay: 300,
157
+ });
158
+ });
159
+
160
+ it('should execute immediately and cancel pending', () => {
161
+ debouncer.trigger('test');
162
+ expect(mockFunc).not.toHaveBeenCalled();
163
+ debouncer.flush();
164
+ expect(mockFunc).toHaveBeenCalledWith('test');
165
+ expect(debouncer.isPending).toBe(false);
166
+ jest.advanceTimersByTime(300);
167
+ expect(mockFunc).toHaveBeenCalledTimes(1);
168
+ });
169
+
170
+ it('should do nothing if no pending call', () => {
171
+ debouncer.flush();
172
+ expect(mockFunc).not.toHaveBeenCalled();
173
+ });
174
+ });
175
+
176
+ describe('MaxWait Functionality', () => {
177
+ beforeEach(() => {
178
+ debouncer = createDebouncer({
179
+ func: mockFunc,
180
+ delay: 300,
181
+ maxWait: 1000,
182
+ });
183
+ });
184
+
185
+ it('should execute after maxWait even with continuous triggers', () => {
186
+ debouncer.trigger('first');
187
+ jest.advanceTimersByTime(500);
188
+ debouncer.trigger('second');
189
+ jest.advanceTimersByTime(500);
190
+ expect(mockFunc).toHaveBeenCalledWith('first'); // After maxWait
191
+ debouncer.trigger('third');
192
+ jest.advanceTimersByTime(300);
193
+ expect(mockFunc).toHaveBeenCalledWith('third');
194
+ });
195
+ });
196
+
197
+ describe('ThisContext Binding', () => {
198
+ /**
199
+ * @type {{ value: string; }}
200
+ */
201
+ let context;
202
+
203
+ beforeEach(() => {
204
+ context = {value: 'test'};
205
+ mockFunc = jest.fn(function () {
206
+ this.value = 'changed';
207
+ });
208
+ debouncer = createDebouncer({
209
+ func: mockFunc,
210
+ thisContext: context,
211
+ delay: 300,
212
+ });
213
+ });
214
+
215
+ it('should bind thisContext correctly', () => {
216
+ debouncer.trigger();
217
+ jest.advanceTimersByTime(300);
218
+ expect(context.value).toBe('changed');
219
+ });
220
+ });
221
+
222
+ describe('Edge Cases', () => {
223
+ it('should handle zero delay', () => {
224
+ debouncer = createDebouncer({
225
+ func: mockFunc,
226
+ delay: 0,
227
+ });
228
+ debouncer.trigger('test');
229
+ jest.advanceTimersByTime(0);
230
+ expect(mockFunc).toHaveBeenCalledWith('test');
231
+ });
232
+
233
+ it('should handle no arguments', () => {
234
+ debouncer = createDebouncer({
235
+ func: mockFunc,
236
+ delay: 300,
237
+ });
238
+ debouncer.trigger();
239
+ jest.advanceTimersByTime(300);
240
+ expect(mockFunc).toHaveBeenCalledWith();
241
+ });
242
+
243
+ it('should handle multiple arguments', () => {
244
+ debouncer = createDebouncer({
245
+ func: mockFunc,
246
+ delay: 300,
247
+ });
248
+ debouncer.trigger('arg1', 'arg2', 123);
249
+ jest.advanceTimersByTime(300);
250
+ expect(mockFunc).toHaveBeenCalledWith('arg1', 'arg2', 123);
251
+ });
252
+
253
+ it('should not execute if func throws', () => {
254
+ mockFunc = jest.fn(() => {
255
+ throw new Error('test');
256
+ });
257
+ debouncer = createDebouncer({
258
+ func: mockFunc,
259
+ delay: 300,
260
+ });
261
+ debouncer.trigger();
262
+ expect(() => jest.advanceTimersByTime(300)).toThrow('test');
263
+ });
264
+
265
+ it('should double-invocation with leading: true, trailing: false, and maxWait on multiple trigger', () => {
266
+ debouncer = createDebouncer({
267
+ func: mockFunc,
268
+ delay: 300,
269
+ leading: true,
270
+ trailing: false,
271
+ maxWait: 500,
272
+ });
273
+ debouncer.trigger('first');
274
+ expect(mockFunc).toHaveBeenCalledTimes(1); // Leading call
275
+ jest.advanceTimersByTime(200);
276
+ debouncer.trigger('second');
277
+ jest.advanceTimersByTime(200);
278
+ expect(mockFunc).toHaveBeenCalledTimes(1); // Should not call again yet
279
+ jest.advanceTimersByTime(200); // Trigger maxWait, which may call flush
280
+ expect(mockFunc).toHaveBeenCalledTimes(2); // Should be called a second time due to maxWait
281
+ });
282
+
283
+ it('should prevent double-invocation with leading: true, trailing: false, and maxWait on single trigger', () => {
284
+ debouncer = createDebouncer({
285
+ func: mockFunc,
286
+ delay: 300,
287
+ leading: true,
288
+ trailing: false,
289
+ maxWait: 200,
290
+ });
291
+ debouncer.trigger('first');
292
+ expect(mockFunc).toHaveBeenCalledTimes(1); // Leading call
293
+ jest.advanceTimersByTime(300);
294
+ expect(mockFunc).toHaveBeenCalledTimes(1); // Should not call again
295
+ });
296
+
297
+ it('should prevent double-invocation with leading: true, trailing: false, and flush', () => {
298
+ debouncer = createDebouncer({
299
+ func: mockFunc,
300
+ delay: 300,
301
+ leading: true,
302
+ trailing: false,
303
+ });
304
+ debouncer.trigger('first');
305
+ expect(mockFunc).toHaveBeenCalledTimes(1); // Leading call
306
+ jest.advanceTimersByTime(100);
307
+ debouncer.flush();
308
+ debouncer.flush();
309
+ expect(mockFunc).toHaveBeenCalledTimes(1); // Should not call again
310
+ });
311
+ });
312
+
313
+ it('should execute after delay for trailing debounce', () => {
314
+ debouncer = createDebouncer({
315
+ func: mockFunc,
316
+ delay: 300,
317
+ leading: false,
318
+ trailing: true,
319
+ });
320
+ debouncer.trigger('first');
321
+ expect(mockFunc).toHaveBeenCalledTimes(0); // No call yet
322
+ jest.advanceTimersByTime(300);
323
+ expect(mockFunc).toHaveBeenCalledTimes(1); // Trailing call
324
+ });
325
+ it('should execute leading but skip trailing on single trigger', () => {
326
+ debouncer = createDebouncer({
327
+ func: mockFunc,
328
+ delay: 300,
329
+ leading: true,
330
+ trailing: true,
331
+ });
332
+ debouncer.trigger('first');
333
+ expect(mockFunc).toHaveBeenCalledTimes(1); // leading call
334
+ jest.advanceTimersByTime(300);
335
+ expect(mockFunc).toHaveBeenCalledTimes(1); // no trailing call on same argument
336
+ });
337
+
338
+ it('should execute leading and skip trailing after flush', () => {
339
+ debouncer = createDebouncer({
340
+ func: mockFunc,
341
+ delay: 300,
342
+ leading: true,
343
+ trailing: true,
344
+ });
345
+ debouncer.trigger('first');
346
+ expect(mockFunc).toHaveBeenCalledTimes(1); // leading call
347
+ jest.advanceTimersByTime(100);
348
+ debouncer.flush();
349
+ jest.advanceTimersByTime(100);
350
+ expect(mockFunc).toHaveBeenCalledTimes(1); // no trailing call on same argument
351
+ });
352
+
353
+ it('should execute leading and trailing on multiple triggers', () => {
354
+ debouncer = createDebouncer({
355
+ func: mockFunc,
356
+ delay: 300,
357
+ leading: true,
358
+ trailing: true,
359
+ });
360
+ debouncer.trigger('first');
361
+ expect(mockFunc).toHaveBeenCalledTimes(1); // leading call
362
+ debouncer.trigger('second');
363
+ expect(mockFunc).toHaveBeenCalledTimes(1); // No call again yet
364
+ jest.advanceTimersByTime(300);
365
+ expect(mockFunc).toHaveBeenCalledTimes(2); // Trailing call
366
+ expect(mockFunc).toHaveBeenLastCalledWith('second');
367
+ });
368
+
369
+ it('should execute leading and flush, skip trailing', () => {
370
+ debouncer = createDebouncer({
371
+ func: mockFunc,
372
+ delay: 300,
373
+ leading: true,
374
+ trailing: false,
375
+ });
376
+ debouncer.trigger('first');
377
+ expect(mockFunc).toHaveBeenCalledTimes(1); // leading call
378
+ debouncer.trigger('second');
379
+ expect(mockFunc).toHaveBeenCalledTimes(1); // No call again yet
380
+ debouncer.flush();
381
+ debouncer.flush();
382
+ expect(mockFunc).toHaveBeenCalledTimes(2);
383
+ expect(mockFunc).toHaveBeenLastCalledWith('second');
384
+ jest.advanceTimersByTime(300);
385
+ debouncer.flush();
386
+ expect(mockFunc).toHaveBeenCalledTimes(2); // no trailing call
387
+ });
388
+ });