import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
describe("cache", () => {
beforeEach(() => {
vi.resetModules();
});
afterEach(() => {
vi.useRealTimers();
});
it("bypasses cache when not in browser", async () => {
vi.doMock("$app/env", () => ({ browser: false }));
const { cached } = await import("./cache");
const fn = vi.fn().mockResolvedValue("result-1");
const res1 = await cached("test-key", 30_000, fn);
const res2 = await cached("test-key", 30_000, fn);
expect(res1).toBe("result-1");
expect(res2).toBe("result-1");
expect(fn).toHaveBeenCalledTimes(2);
});
it("caches value in browser and serves from cache before ttl expires", async () => {
vi.doMock("$app/env", () => ({ browser: true }));
const { cached } = await import("./cache");
const fn = vi.fn().mockResolvedValue({ hello: "world" });
const res1 = await cached("test-key", 30_000, fn);
const res2 = await cached("test-key", 30_000, fn);
expect(res1).toEqual({ hello: "world" });
expect(res2).toEqual({ hello: "world" });
expect(fn).toHaveBeenCalledTimes(1);
});
it("refetches after ttl expires", async () => {
vi.useFakeTimers();
vi.doMock("$app/env", () => ({ browser: true }));
const { cached } = await import("./cache");
let count = 0;
const fn = vi.fn().mockImplementation(async () => ++count);
const res1 = await cached("test-key", 30_000, fn);
expect(res1).toBe(1);
vi.advanceTimersByTime(29_999);
const res2 = await cached("test-key", 30_000, fn);
expect(res2).toBe(1);
expect(fn).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(2);
const res3 = await cached("test-key", 30_000, fn);
expect(res3).toBe(2);
expect(fn).toHaveBeenCalledTimes(2);
});
it("cleans up cache and rethrows when loader rejects", async () => {
vi.doMock("$app/env", () => ({ browser: true }));
const { cached } = await import("./cache");
const err = new Error("network error");
const fn = vi.fn().mockRejectedValue(err);
await expect(cached("err-key", 30_000, fn)).rejects.toThrow("network error");
fn.mockResolvedValueOnce("recovered");
const res = await cached("err-key", 30_000, fn);
expect(res).toBe("recovered");
});
it("clears every key", async () => {
vi.doMock("$app/env", () => ({ browser: true }));
const { cached, clear } = await import("./cache");
const fnIssues = vi.fn().mockResolvedValue("issues-data");
const fnPulls = vi.fn().mockResolvedValue("pulls-data");
await cached("issues:did:plc:repo:open:", 30_000, fnIssues);
await cached("pulls:did:plc:repo:open:", 30_000, fnPulls);
clear();
await cached("issues:did:plc:repo:open:", 30_000, fnIssues);
await cached("pulls:did:plc:repo:open:", 30_000, fnPulls);
expect(fnIssues).toHaveBeenCalledTimes(2);
expect(fnPulls).toHaveBeenCalledTimes(2);
});
it("invalidate drops one key so the next call reloads", async () => {
vi.doMock("$app/env", () => ({ browser: true }));
const { cached, invalidate } = await import("./cache");
const fn = vi.fn().mockResolvedValueOnce("stale").mockResolvedValue("fresh");
await expect(cached("k", 30_000, fn)).resolves.toBe("stale");
invalidate("k");
await expect(cached("k", 30_000, fn)).resolves.toBe("fresh");
expect(fn).toHaveBeenCalledTimes(2);
});
});