feat: debounce helper with flush

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-05-08 17:47:25 -04:00
parent 45adba1643
commit 7bfc09207d
2 changed files with 81 additions and 0 deletions

41
src/lib/debounce.test.ts Normal file
View 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
View 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;
}