From afdd3f31929fb0a55a1904711bcec525816032e5 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 27 Apr 2026 03:29:43 -0400 Subject: [PATCH 1/2] feat: add debounce and throttle utilities --- src/utils/debounce.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/utils/debounce.ts diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts new file mode 100644 index 0000000..93b7d90 --- /dev/null +++ b/src/utils/debounce.ts @@ -0,0 +1,18 @@ +export function debounce void>(fn: T, delayMs: number): T { + let timer: any; + return ((...args: any[]) => { + clearTimeout(timer); + timer = setTimeout(() => fn(...args), delayMs); + }) as T; +} + +export function throttle void>(fn: T, intervalMs: number): T { + let lastCall = 0; + return ((...args: any[]) => { + const now = Date.now(); + if (now - lastCall >= intervalMs) { + lastCall = now; + fn(...args); + } + }) as T; +} From aa719c98bf9d523f76cebceab3de7edaa3a055b4 Mon Sep 17 00:00:00 2001 From: "pi-relay[bot]" Date: Mon, 27 Apr 2026 07:36:28 +0000 Subject: [PATCH 2/2] fix: address AI review findings --- package-lock.json | 66 ------------------------- src/utils/debounce.ts | 2 +- test/utils/debounce.test.ts | 97 +++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 67 deletions(-) create mode 100644 test/utils/debounce.test.ts diff --git a/package-lock.json b/package-lock.json index 172e097..aef8f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -889,9 +889,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -909,9 +906,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -929,9 +923,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -949,9 +940,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -1558,9 +1546,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1578,9 +1563,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1598,9 +1580,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1618,9 +1597,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1638,9 +1614,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1975,9 +1948,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1992,9 +1962,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2009,9 +1976,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2026,9 +1990,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2043,9 +2004,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2060,9 +2018,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2077,9 +2032,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2094,9 +2046,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2111,9 +2060,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2128,9 +2074,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2145,9 +2088,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2162,9 +2102,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2179,9 +2116,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts index 93b7d90..b8f5608 100644 --- a/src/utils/debounce.ts +++ b/src/utils/debounce.ts @@ -1,5 +1,5 @@ export function debounce void>(fn: T, delayMs: number): T { - let timer: any; + let timer: ReturnType | undefined; return ((...args: any[]) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delayMs); diff --git a/test/utils/debounce.test.ts b/test/utils/debounce.test.ts new file mode 100644 index 0000000..3ba2cdd --- /dev/null +++ b/test/utils/debounce.test.ts @@ -0,0 +1,97 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { debounce, throttle } from "../../src/utils/debounce.js"; + +describe("debounce", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("delays execution and only fires the last call", () => { + const fn = vi.fn(); + const debounced = debounce(fn, 100); + + debounced("a"); + debounced("b"); + debounced("c"); + + expect(fn).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(100); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith("c"); + }); + + it("resets the timer on re-entry", () => { + const fn = vi.fn(); + const debounced = debounce(fn, 100); + + debounced("first"); + vi.advanceTimersByTime(50); + debounced("second"); + vi.advanceTimersByTime(50); + + expect(fn).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(50); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith("second"); + }); + + it("fires immediately with zero delay", () => { + const fn = vi.fn(); + const debounced = debounce(fn, 0); + + debounced("a"); + vi.advanceTimersByTime(0); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith("a"); + }); +}); + +describe("throttle", () => { + it("limits calls within the interval", () => { + const fn = vi.fn(); + const throttled = throttle(fn, 100); + + throttled("a"); + throttled("b"); + throttled("c"); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith("a"); + }); + + it("allows calls after the interval elapses", () => { + const fn = vi.fn(); + vi.useFakeTimers(); + const throttled = throttle(fn, 100); + + throttled("first"); + expect(fn).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(100); + throttled("second"); + expect(fn).toHaveBeenCalledTimes(2); + expect(fn).toHaveBeenCalledWith("second"); + + vi.useRealTimers(); + }); + + it("fires on every call with zero interval", () => { + const fn = vi.fn(); + const throttled = throttle(fn, 0); + + throttled("a"); + throttled("b"); + throttled("c"); + + expect(fn).toHaveBeenCalledTimes(3); + }); +});