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