feat: debounce helper with flush
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
41
src/lib/debounce.test.ts
Normal file
41
src/lib/debounce.test.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import { debounce } from "./debounce";
|
||||||
|
|
||||||
|
describe("debounce", () => {
|
||||||
|
beforeEach(() => vi.useFakeTimers());
|
||||||
|
afterEach(() => vi.useRealTimers());
|
||||||
|
|
||||||
|
it("delays a single call by the wait period", () => {
|
||||||
|
const fn = vi.fn();
|
||||||
|
const d = debounce(fn, 100);
|
||||||
|
d("a");
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
vi.advanceTimersByTime(100);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fn).toHaveBeenCalledWith("a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("collapses rapid calls into a single trailing call", () => {
|
||||||
|
const fn = vi.fn();
|
||||||
|
const d = debounce(fn, 100);
|
||||||
|
d("a"); d("b"); d("c");
|
||||||
|
vi.advanceTimersByTime(100);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fn).toHaveBeenCalledWith("c");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flush() invokes pending call immediately", () => {
|
||||||
|
const fn = vi.fn();
|
||||||
|
const d = debounce(fn, 100);
|
||||||
|
d("a");
|
||||||
|
d.flush();
|
||||||
|
expect(fn).toHaveBeenCalledWith("a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flush() with no pending call is a no-op", () => {
|
||||||
|
const fn = vi.fn();
|
||||||
|
const d = debounce(fn, 100);
|
||||||
|
d.flush();
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
40
src/lib/debounce.ts
Normal file
40
src/lib/debounce.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export type Debounced<Args extends unknown[]> = ((...args: Args) => void) & {
|
||||||
|
flush: () => void;
|
||||||
|
cancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function debounce<Args extends unknown[]>(
|
||||||
|
fn: (...args: Args) => void,
|
||||||
|
waitMs: number
|
||||||
|
): Debounced<Args> {
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let pendingArgs: Args | null = null;
|
||||||
|
|
||||||
|
const debounced = (...args: Args): void => {
|
||||||
|
pendingArgs = args;
|
||||||
|
if (timer !== null) clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
timer = null;
|
||||||
|
const a = pendingArgs!;
|
||||||
|
pendingArgs = null;
|
||||||
|
fn(...a);
|
||||||
|
}, waitMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
debounced.flush = (): void => {
|
||||||
|
if (timer === null) return;
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
const a = pendingArgs!;
|
||||||
|
pendingArgs = null;
|
||||||
|
fn(...a);
|
||||||
|
};
|
||||||
|
|
||||||
|
debounced.cancel = (): void => {
|
||||||
|
if (timer !== null) clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
pendingArgs = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return debounced;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user