diff --git a/.changeset/mean-tables-beg.md b/.changeset/mean-tables-beg.md new file mode 100644 index 00000000..a5965932 --- /dev/null +++ b/.changeset/mean-tables-beg.md @@ -0,0 +1,5 @@ +--- +"@codedazur/essentials": minor +--- + +The debounce utility was added. diff --git a/packages/essentials/index.ts b/packages/essentials/index.ts index a1132f6d..aca23244 100644 --- a/packages/essentials/index.ts +++ b/packages/essentials/index.ts @@ -1,5 +1,6 @@ export * from "./types/optional"; export * from "./utilities/array/shuffle"; +export * from "./utilities/assert"; export * from "./utilities/geometry/Angle"; export * from "./utilities/geometry/Direction"; export * from "./utilities/geometry/Origin"; @@ -20,6 +21,6 @@ export * from "./utilities/string/camelCase"; export * from "./utilities/string/pascalCase"; export * from "./utilities/string/timecode"; export * from "./utilities/system/env"; -export * from "./utilities/timing/sleep"; export * from "./utilities/timing/Timer"; -export * from "./utilities/assert"; +export * from "./utilities/timing/debounce"; +export * from "./utilities/timing/sleep"; diff --git a/packages/essentials/utilities/timing/debounce.test.ts b/packages/essentials/utilities/timing/debounce.test.ts new file mode 100644 index 00000000..864d501e --- /dev/null +++ b/packages/essentials/utilities/timing/debounce.test.ts @@ -0,0 +1,72 @@ +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from "vitest"; +import { debounce } from "./debounce"; + +beforeAll(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + vi.clearAllTimers(); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("debounce", () => { + it("should debounce the function", async () => { + const callback = vi.fn(); + const [debounced] = debounce(callback, 100); + + debounced(); + debounced(); + + expect(callback).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(50); + expect(callback).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(50); + expect(callback).toHaveBeenCalledOnce(); + }); + + it("should support canceling the debounced function", async () => { + const callback = vi.fn(); + const [debounced, cancel] = debounce(callback, 100); + + debounced(); + cancel(); + + await vi.advanceTimersByTimeAsync(100); + expect(callback).not.toHaveBeenCalled(); + }); + + it("should support arguments", async () => { + const callback = vi.fn(); + const [debounced] = debounce(callback, 100); + + debounced(1, 2, 3); + + await vi.advanceTimersByTimeAsync(100); + expect(callback).toHaveBeenCalledWith(1, 2, 3); + }); + + it("should support return values", async () => { + const [debounced] = debounce(() => 42, 100); + + let result; + debounced().then((value) => (result = value)); + expect(result).toBe(undefined); + + await vi.advanceTimersByTimeAsync(100); + expect(result).toBe(42); + }); +}); diff --git a/packages/essentials/utilities/timing/debounce.ts b/packages/essentials/utilities/timing/debounce.ts new file mode 100644 index 00000000..a282de60 --- /dev/null +++ b/packages/essentials/utilities/timing/debounce.ts @@ -0,0 +1,27 @@ +type DebouncedFunction ReturnType> = ( + ...args: Parameters +) => Promise>; + +type CancelFunction = () => void; + +export function debounce ReturnType>( + callback: F, + ms = 50, +): [DebouncedFunction, CancelFunction] { + let timer: NodeJS.Timeout | undefined; + + return [ + (...args: Parameters) => + new Promise((resolve) => { + if (timer) { + clearTimeout(timer); + } + + timer = setTimeout(() => { + timer = undefined; + resolve(callback(...args)); + }, ms); + }), + () => clearTimeout(timer), + ]; +}