@alwatr/debounce 1.0.0 → 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 +23 -0
- package/README.md +21 -21
- package/dist/debounce.d.ts +7 -34
- package/dist/debounce.d.ts.map +1 -1
- package/dist/main.cjs +32 -20
- package/dist/main.cjs.map +2 -2
- package/dist/main.d.ts +30 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.mjs +32 -20
- package/dist/main.mjs.map +3 -3
- package/dist/type.d.ts +10 -4
- package/dist/type.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/debounce.test.js +388 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
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
|
+
|
|
23
|
+
## [1.0.1](https://github.com/Alwatr/nanolib/compare/@alwatr/debounce@1.0.0...@alwatr/debounce@1.0.1) (2025-09-14)
|
|
24
|
+
|
|
25
|
+
### 🔨 Code Refactoring
|
|
26
|
+
|
|
27
|
+
* **debounce:** rename 'callback' to 'func' for consistency in API ([df7ede1](https://github.com/Alwatr/nanolib/commit/df7ede1a78109831cca22389bd2d69df2e0ae366))
|
|
28
|
+
|
|
6
29
|
## [1.0.0](https://github.com/Alwatr/nanolib/compare/@alwatr/debounce@1.0.0-rc.0...@alwatr/debounce@1.0.0) (2025-09-14)
|
|
7
30
|
|
|
8
31
|
### 🔨 Code Refactoring
|
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ import {createDebouncer} from '@alwatr/debounce';
|
|
|
73
73
|
// 1. Create a debouncer instance
|
|
74
74
|
const debouncer = createDebouncer({
|
|
75
75
|
// The function you want to debounce
|
|
76
|
-
|
|
76
|
+
func: (query: string) => {
|
|
77
77
|
console.log(`Searching for: ${query}`);
|
|
78
78
|
},
|
|
79
79
|
// The delay in milliseconds
|
|
@@ -100,13 +100,13 @@ A factory function that creates a new `Debouncer` instance. It's the recommended
|
|
|
100
100
|
|
|
101
101
|
This is the configuration object passed to `createDebouncer` or the `Debouncer` constructor.
|
|
102
102
|
|
|
103
|
-
| Property | Type | Description
|
|
104
|
-
| :------------ | :---------------------- |
|
|
105
|
-
| `
|
|
106
|
-
| `delay` | `number` | **(Required)** The debounce delay in milliseconds.
|
|
107
|
-
| `thisContext` | `ThisParameterType<F>` | The `this` context for the
|
|
108
|
-
| `leading` | `boolean` | If `true`, executes the function on the leading edge.
|
|
109
|
-
| `trailing` | `boolean` | If `true`, executes the function on the trailing edge.
|
|
103
|
+
| Property | Type | Description | Default |
|
|
104
|
+
| :------------ | :---------------------- | :------------------------------------------------------------------- | :---------- |
|
|
105
|
+
| `func` | `F extends AnyFunction` | **(Required)** The function to be debounced. | - |
|
|
106
|
+
| `delay` | `number` | **(Required)** The debounce delay in milliseconds. | - |
|
|
107
|
+
| `thisContext` | `ThisParameterType<F>` | The `this` context for the `func`. Essential when using class methods. | `undefined` |
|
|
108
|
+
| `leading` | `boolean` | If `true`, executes the function on the leading edge. | `false` |
|
|
109
|
+
| `trailing` | `boolean` | If `true`, executes the function on the trailing edge. | `true` |
|
|
110
110
|
|
|
111
111
|
### `Debouncer` Instance
|
|
112
112
|
|
|
@@ -120,7 +120,7 @@ An instance of the `Debouncer` class returned by `createDebouncer`.
|
|
|
120
120
|
#### Methods
|
|
121
121
|
|
|
122
122
|
- **`trigger(...args: Parameters<F>): void`**
|
|
123
|
-
Triggers the debounce timer. Each call resets the timer. The arguments passed here will be forwarded to the `
|
|
123
|
+
Triggers the debounce timer. Each call resets the timer. The arguments passed here will be forwarded to the `func` function.
|
|
124
124
|
|
|
125
125
|
- **`cancel(): void`**
|
|
126
126
|
Cancels any pending execution and clears internal state. This is crucial for preventing memory leaks.
|
|
@@ -143,7 +143,7 @@ In modern Single-Page Applications (SPAs) or any component-based architecture, c
|
|
|
143
143
|
```typescript
|
|
144
144
|
class MyComponent {
|
|
145
145
|
private debouncer = createDebouncer({
|
|
146
|
-
|
|
146
|
+
func: this.doSomething,
|
|
147
147
|
thisContext: this, // Bind `this` correctly!
|
|
148
148
|
delay: 500,
|
|
149
149
|
});
|
|
@@ -175,7 +175,7 @@ function MyComponent() {
|
|
|
175
175
|
const debouncedApiCall = useMemo(
|
|
176
176
|
() =>
|
|
177
177
|
createDebouncer({
|
|
178
|
-
|
|
178
|
+
func: (query) => fetch(`/api/search?q=${query}`),
|
|
179
179
|
delay: 300,
|
|
180
180
|
}),
|
|
181
181
|
[],
|
|
@@ -195,12 +195,12 @@ function MyComponent() {
|
|
|
195
195
|
|
|
196
196
|
### Using with `thisContext`
|
|
197
197
|
|
|
198
|
-
When your
|
|
198
|
+
When your `func` is a method on a class, `this` can lose its context. Pass the class instance to `thisContext` to ensure it's bound correctly.
|
|
199
199
|
|
|
200
200
|
```typescript
|
|
201
201
|
class ApiService {
|
|
202
202
|
private debouncer = createDebouncer({
|
|
203
|
-
|
|
203
|
+
func: this.sendRequest,
|
|
204
204
|
thisContext: this, // Ensures `this` inside `sendRequest` is `ApiService`
|
|
205
205
|
delay: 500,
|
|
206
206
|
});
|
|
@@ -309,7 +309,7 @@ import {createDebouncer} from '@alwatr/debounce';
|
|
|
309
309
|
// ۱. یک نمونه دیبانسر بسازید
|
|
310
310
|
const debouncer = createDebouncer({
|
|
311
311
|
// تابعی که میخواهید دیبانس کنید
|
|
312
|
-
|
|
312
|
+
func: (query: string) => {
|
|
313
313
|
console.log(`در حال جستجو برای: ${query}`);
|
|
314
314
|
},
|
|
315
315
|
// تأخیر بر حسب میلیثانیه
|
|
@@ -338,9 +338,9 @@ debouncer.trigger('علی');
|
|
|
338
338
|
|
|
339
339
|
| ویژگی | نوع | توضیحات | پیشفرض |
|
|
340
340
|
| :------------ | :---------------------- | :-------------------------------------------------------------------- | :---------- |
|
|
341
|
-
| `
|
|
341
|
+
| `func` | `F extends AnyFunction` | **(الزامی)** تابعی که باید دیبانس شود. | - |
|
|
342
342
|
| `delay` | `number` | **(الزامی)** تأخیر دیبانس بر حسب میلیثانیه. | - |
|
|
343
|
-
| `thisContext` | `ThisParameterType<F>` | کانتکست `this` برای
|
|
343
|
+
| `thisContext` | `ThisParameterType<F>` | کانتکست `this` برای `func`. هنگام استفاده از متدهای کلاس ضروری است. | `undefined` |
|
|
344
344
|
| `leading` | `boolean` | اگر `true` باشد، تابع در لبه بالارونده (leading edge) اجرا میشود. | `false` |
|
|
345
345
|
| `trailing` | `boolean` | اگر `true` باشد، تابع در لبه پایینرونده (trailing edge) اجرا میشود. | `true` |
|
|
346
346
|
|
|
@@ -356,7 +356,7 @@ debouncer.trigger('علی');
|
|
|
356
356
|
#### متدها
|
|
357
357
|
|
|
358
358
|
- **`trigger(...args: Parameters<F>): void`**
|
|
359
|
-
تایمر دیبانس را فعال میکند. هر فراخوانی، تایمر را ریست میکند. آرگومانهای پاس داده شده به این متد، به تابع `
|
|
359
|
+
تایمر دیبانس را فعال میکند. هر فراخوانی، تایمر را ریست میکند. آرگومانهای پاس داده شده به این متد، به تابع `func` ارسال میشوند.
|
|
360
360
|
|
|
361
361
|
- **`cancel(): void`**
|
|
362
362
|
هرگونه اجرای در حال انتظار را لغو کرده و وضعیت داخلی را پاک میکند. این متد برای جلوگیری از نشت حافظه بسیار حیاتی است.
|
|
@@ -379,7 +379,7 @@ debouncer.trigger('علی');
|
|
|
379
379
|
```typescript
|
|
380
380
|
class MyComponent {
|
|
381
381
|
private debouncer = createDebouncer({
|
|
382
|
-
|
|
382
|
+
func: this.doSomething,
|
|
383
383
|
thisContext: this, // `this` را به درستی متصل کنید!
|
|
384
384
|
delay: 500,
|
|
385
385
|
});
|
|
@@ -411,7 +411,7 @@ function MyComponent() {
|
|
|
411
411
|
const debouncedApiCall = useMemo(
|
|
412
412
|
() =>
|
|
413
413
|
createDebouncer({
|
|
414
|
-
|
|
414
|
+
func: (query) => fetch(`/api/search?q=${query}`),
|
|
415
415
|
delay: 300,
|
|
416
416
|
}),
|
|
417
417
|
[],
|
|
@@ -431,12 +431,12 @@ function MyComponent() {
|
|
|
431
431
|
|
|
432
432
|
### استفاده با `thisContext`
|
|
433
433
|
|
|
434
|
-
زمانی که `
|
|
434
|
+
زمانی که `func` شما یک متد از یک کلاس است، `this` ممکن است کانتکست خود را از دست بدهد. برای اطمینان از اتصال صحیح، نمونه کلاس را به `thisContext` پاس دهید.
|
|
435
435
|
|
|
436
436
|
```typescript
|
|
437
437
|
class ApiService {
|
|
438
438
|
private debouncer = createDebouncer({
|
|
439
|
-
|
|
439
|
+
func: this.sendRequest,
|
|
440
440
|
thisContext: this, // تضمین میکند که `this` در داخل `sendRequest` همان `ApiService` است
|
|
441
441
|
delay: 500,
|
|
442
442
|
});
|
package/dist/debounce.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { DebouncerConfig } from './type.ts';
|
|
|
9
9
|
* @example
|
|
10
10
|
* ```typescript
|
|
11
11
|
* const debouncer = new Debouncer({
|
|
12
|
-
*
|
|
12
|
+
* func: (text: string) => console.log('Searching:', text),
|
|
13
13
|
* delay: 300,
|
|
14
14
|
* leading: false,
|
|
15
15
|
* trailing: true,
|
|
@@ -21,7 +21,7 @@ import type { DebouncerConfig } from './type.ts';
|
|
|
21
21
|
*
|
|
22
22
|
* // Advanced: With leading edge
|
|
23
23
|
* const leadingDebouncer = new Debouncer({
|
|
24
|
-
*
|
|
24
|
+
* func: () => console.log('Immediate and delayed'),
|
|
25
25
|
* delay: 500,
|
|
26
26
|
* leading: true,
|
|
27
27
|
* trailing: true,
|
|
@@ -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
|
/**
|
|
@@ -41,12 +42,12 @@ export declare class Debouncer<F extends AnyFunction> {
|
|
|
41
42
|
get isPending(): boolean;
|
|
42
43
|
/**
|
|
43
44
|
* Triggers the debounced function with the stored `thisContext`.
|
|
44
|
-
* @param args The arguments to pass to the
|
|
45
|
+
* @param args The arguments to pass to the `func`.
|
|
45
46
|
*
|
|
46
47
|
* @example
|
|
47
48
|
* ```typescript
|
|
48
49
|
* const debouncer = new Debouncer({
|
|
49
|
-
*
|
|
50
|
+
* func: (value: number) => console.log('Value:', value),
|
|
50
51
|
* delay: 500,
|
|
51
52
|
* });
|
|
52
53
|
* debouncer.trigger(42); // Logs after 500ms if not triggered again
|
|
@@ -64,7 +65,7 @@ export declare class Debouncer<F extends AnyFunction> {
|
|
|
64
65
|
* @example
|
|
65
66
|
* ```typescript
|
|
66
67
|
* const debouncer = new Debouncer({
|
|
67
|
-
*
|
|
68
|
+
* func: () => console.log('Executed'),
|
|
68
69
|
* delay: 1000,
|
|
69
70
|
* });
|
|
70
71
|
* debouncer.trigger();
|
|
@@ -85,7 +86,7 @@ export declare class Debouncer<F extends AnyFunction> {
|
|
|
85
86
|
* @example
|
|
86
87
|
* ```typescript
|
|
87
88
|
* const debouncer = new Debouncer({
|
|
88
|
-
*
|
|
89
|
+
* func: () => console.log('Flushed'),
|
|
89
90
|
* delay: 1000,
|
|
90
91
|
* });
|
|
91
92
|
* debouncer.trigger();
|
|
@@ -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
|
-
* callback: (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
|
-
* callback: 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
|
package/dist/debounce.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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.
|
|
@@ -41,16 +42,16 @@ var Debouncer = class {
|
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
43
44
|
* Triggers the debounced function with the stored `thisContext`.
|
|
44
|
-
* @param args The arguments to pass to the
|
|
45
|
-
*
|
|
45
|
+
* @param args The arguments to pass to the `func`.
|
|
46
|
+
*
|
|
46
47
|
* @example
|
|
47
48
|
* ```typescript
|
|
48
49
|
* const debouncer = new Debouncer({
|
|
49
|
-
*
|
|
50
|
+
* func: (value: number) => console.log('Value:', value),
|
|
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
|
|
62
|
-
if (
|
|
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
|
|
74
|
+
if (this.config__.trailing === true) {
|
|
70
75
|
this.invoke__();
|
|
71
76
|
}
|
|
72
77
|
this.cleanup__();
|
|
@@ -75,23 +80,26 @@ 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({
|
|
82
|
-
*
|
|
87
|
+
* func: () => console.log('Executed'),
|
|
83
88
|
* delay: 1000,
|
|
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.
|
|
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,21 +107,22 @@ 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({
|
|
111
|
-
*
|
|
120
|
+
* func: () => console.log('Flushed'),
|
|
112
121
|
* delay: 1000,
|
|
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,19 +130,22 @@ 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.
|
|
130
139
|
*/
|
|
131
140
|
invoke__() {
|
|
132
141
|
if (this.lastArgs__) {
|
|
133
|
-
this.config__.
|
|
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": ["
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgCO,IAAM,YAAN,MAAuC;AAAA,
|
|
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
|
package/dist/main.d.ts.map
CHANGED
|
@@ -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.
|
|
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.
|
|
@@ -15,16 +16,16 @@ var Debouncer = class {
|
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* Triggers the debounced function with the stored `thisContext`.
|
|
18
|
-
* @param args The arguments to pass to the
|
|
19
|
-
*
|
|
19
|
+
* @param args The arguments to pass to the `func`.
|
|
20
|
+
*
|
|
20
21
|
* @example
|
|
21
22
|
* ```typescript
|
|
22
23
|
* const debouncer = new Debouncer({
|
|
23
|
-
*
|
|
24
|
+
* func: (value: number) => console.log('Value:', value),
|
|
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
|
|
36
|
-
if (
|
|
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
|
|
48
|
+
if (this.config__.trailing === true) {
|
|
44
49
|
this.invoke__();
|
|
45
50
|
}
|
|
46
51
|
this.cleanup__();
|
|
@@ -49,23 +54,26 @@ 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({
|
|
56
|
-
*
|
|
61
|
+
* func: () => console.log('Executed'),
|
|
57
62
|
* delay: 1000,
|
|
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.
|
|
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,21 +81,22 @@ 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({
|
|
85
|
-
*
|
|
94
|
+
* func: () => console.log('Flushed'),
|
|
86
95
|
* delay: 1000,
|
|
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,19 +104,22 @@ 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.
|
|
104
113
|
*/
|
|
105
114
|
invoke__() {
|
|
106
115
|
if (this.lastArgs__) {
|
|
107
|
-
this.config__.
|
|
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
|
|
5
|
-
"mappings": ";;;AAgCO,IAAM,YAAN,MAAuC;AAAA,
|
|
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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Key notes:
|
|
6
6
|
* - `leading` and `trailing` control execution timing: leading executes immediately on first trigger, trailing after delay.
|
|
7
7
|
* - If both are true, execution happens on first trigger and last trigger (after delay).
|
|
8
|
-
* - `thisContext` ensures the
|
|
8
|
+
* - `thisContext` ensures the `func` is bound to the correct `this` value, useful in class methods or event handlers.
|
|
9
9
|
* - `delay` must be a positive number.
|
|
10
10
|
*/
|
|
11
11
|
export interface DebouncerConfig<F extends AnyFunction> {
|
|
@@ -13,11 +13,11 @@ export interface DebouncerConfig<F extends AnyFunction> {
|
|
|
13
13
|
* The function to be executed after the delay.
|
|
14
14
|
* Can be any function type, with type safety enforced by generics.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
func: F;
|
|
17
17
|
/**
|
|
18
|
-
* The `this` context to be used when invoking the
|
|
18
|
+
* The `this` context to be used when invoking the func.
|
|
19
19
|
* If provided, it will be stored and used for all invocations.
|
|
20
|
-
* Omit if the
|
|
20
|
+
* Omit if the func doesn't rely on `this` or uses arrow functions.
|
|
21
21
|
*/
|
|
22
22
|
thisContext?: ThisParameterType<F>;
|
|
23
23
|
/**
|
|
@@ -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).
|
package/dist/type.d.ts.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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": "
|
|
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
|
+
});
|