ProvenPin Install

Vitest fake timers with es-toolkit debounce, throttle and retry: working versions (tested 2026-10-15)

Test time-based helpers from es-toolkit (debounce, throttle, delay, retry) deterministically with Vitest fake timers, advanceTimersByTime and setSystemTime, without real waiting.

passed5 passed · 0 failed2026-10-15 17:11 UTC
Status
passed
Last passed
2026-10-15 17:11 UTC
Tests
5 / 5
Language
TypeScript
Runtime
node v22.23.3
pinned versions3
es-toolkit@1.52.0@types/node@26.6.5vitest@5.0.5
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38775053802

The code

Exactly the files that passed. Your agent can pull them with get_recipe("vitest-fake-timers-es-toolkit") (install), or see them on GitHub.

package.json
{
  "name": "vitest-fake-timers-es-toolkit",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
  },
  "dependencies": {
    "es-toolkit": "1.52.0"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "vitest": "5.0.5"
  }
}
src/autosave.ts
import { debounce, throttle, retry, delay } from "es-toolkit";

/** Saves at most once per pause in typing; flush() forces a save, cancel() drops it. */
export function createAutosave(save: (text: string) => void, wait = 500) {
  const run = debounce((text: string) => save(text), wait);
  return { change: (text: string) => run(text), flush: () => run.flush(), cancel: () => run.cancel() };
}

export function createScrollTracker(report: (y: number) => void, every = 100) {
  return throttle((y: number) => report(y), every);
}

/** Retries a flaky call with a fixed delay between attempts. */
export function fetchWithRetry<T>(fn: () => Promise<T>, retries = 3, delayMs = 1000) {
  return retry(fn, { retries, delay: delayMs });
}

export async function stamp(label: string, waitMs: number) {
  await delay(waitMs);
  return `${label} at ${new Date().toISOString()}`;
}
test/autosave.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createAutosave, createScrollTracker, fetchWithRetry, stamp } from "../src/autosave.ts";

beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

describe("debounced autosave", () => {
  it("saves once after typing pauses", () => {
    const save = vi.fn();
    const auto = createAutosave(save, 500);
    auto.change("h");
    vi.advanceTimersByTime(200);
    auto.change("he");
    vi.advanceTimersByTime(200);
    auto.change("hey");
    expect(save).not.toHaveBeenCalled();
    vi.advanceTimersByTime(500);
    expect(save).toHaveBeenCalledTimes(1);
    expect(save).toHaveBeenCalledWith("hey");
  });

  it("flush saves now and cancel drops a pending save", () => {
    const save = vi.fn();
    const auto = createAutosave(save, 500);
    auto.change("draft");
    auto.flush();
    expect(save).toHaveBeenCalledWith("draft");
    auto.change("lost");
    auto.cancel();
    vi.runAllTimers();
    expect(save).toHaveBeenCalledTimes(1);
  });
});

describe("throttled scroll tracking", () => {
  it("reports the leading call and the last call per window", () => {
    const report = vi.fn();
    const onScroll = createScrollTracker(report, 100);
    for (let y = 1; y <= 5; y++) onScroll(y * 10);
    expect(report).toHaveBeenCalledTimes(1);
    expect(report).toHaveBeenLastCalledWith(10);
    vi.advanceTimersByTime(100);
    expect(report).toHaveBeenCalledTimes(2);
    expect(report).toHaveBeenLastCalledWith(50);
  });
});

describe("async timers", () => {
  it("retries with delay using advanceTimersByTimeAsync", async () => {
    let calls = 0;
    const p = fetchWithRetry(async () => {
      calls++;
      if (calls < 3) throw new Error("flaky");
      return "ok";
    }, 3, 1000);
    await vi.advanceTimersByTimeAsync(2000);
    await expect(p).resolves.toBe("ok");
    expect(calls).toBe(3);
  });

  it("setSystemTime controls Date while delays are skipped", async () => {
    vi.setSystemTime(new Date("2026-10-15T12:00:00Z"));
    const p = stamp("job", 60_000);
    await vi.advanceTimersByTimeAsync(60_000);
    await expect(p).resolves.toBe("job at 2026-10-15T12:01:00.000Z");
  });
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.