~james/core

core/web/src/lib/api/cache.test.ts -rw-r--r-- 3.2 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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);
	});
});