// Regression tests for the client-navigation payload cache. // // The bug this exists to fix: `` prefetched pages with a // `` that could never be reused. Measured on a real app, // the same URL was fetched twice — 16742 bytes both times, full transferSize // on the second — because SSR pages are sent "private, no-store" and an // as="document" prefetch only feeds real navigations, not fetch(). So each // prefetch was a full server render, thrown away, and the click still paid. import { describe, expect, it } from "bun:test"; import { createNavPayloadCache } from "./ssr-nav-cache"; /** A cache with a controllable clock and a fetch that records its calls. */ function harness(opts: { ttlMs?: number; max?: number } = {}) { let clock = 1000; const calls: string[] = []; let resolveNext: ((v: string | null) => void) | null = null; const cache = createNavPayloadCache({ now: () => clock, ttlMs: opts.ttlMs, max: opts.max, fetchPage: (target) => { calls.push(target); return new Promise((res) => { resolveNext = res; // Default: resolve immediately on the microtask queue. queueMicrotask(() => res(`${target}`)); }); }, }); return { cache, calls, advance: (ms: number) => { clock += ms; }, settleWith: (v: string | null) => resolveNext?.(v), }; } describe("nav payload cache", () => { it("hands the prefetched payload to the click", async () => { const h = harness(); h.cache.prefetch("/a"); const taken = h.cache.take("/a"); expect(taken).not.toBeNull(); expect(await taken!).toBe("/a"); // The whole point: the click opened no request of its own. expect(h.calls).toEqual(["/a"]); }); it("does not re-fetch a target already in flight", () => { const h = harness(); h.cache.prefetch("/a"); h.cache.prefetch("/a"); h.cache.prefetch("/a"); expect(h.calls).toEqual(["/a"]); }); it("lets a click join a prefetch that hasn't landed yet", async () => { // Hover-to-click is usually shorter than the render, so this is the // common path, not an edge case. const h = harness(); h.cache.prefetch("/slow"); const taken = h.cache.take("/slow"); h.settleWith("late"); expect(await taken!).toBe("late"); expect(h.calls).toEqual(["/slow"]); }); it("is single-use, so a second navigation re-renders", () => { const h = harness(); h.cache.prefetch("/a"); expect(h.cache.take("/a")).not.toBeNull(); expect(h.cache.take("/a")).toBeNull(); }); it("refuses a payload past its TTL", () => { const h = harness({ ttlMs: 15000 }); h.cache.prefetch("/a"); h.advance(15000); expect(h.cache.take("/a")).toBeNull(); }); it("still serves a payload just inside its TTL", async () => { const h = harness({ ttlMs: 15000 }); h.cache.prefetch("/a"); h.advance(14999); expect(await h.cache.take("/a")!).toBe("/a"); }); it("re-warms an expired target instead of holding the stale one", async () => { const h = harness({ ttlMs: 100 }); h.cache.prefetch("/a"); h.advance(200); h.cache.prefetch("/a"); expect(h.calls).toEqual(["/a", "/a"]); expect(await h.cache.take("/a")!).toBe("/a"); }); it("evicts the oldest entry at the cap", () => { const h = harness({ max: 2 }); h.cache.prefetch("/a"); h.cache.prefetch("/b"); h.cache.prefetch("/c"); expect(h.cache.size()).toBe(2); expect(h.cache.take("/a")).toBeNull(); expect(h.cache.take("/c")).not.toBeNull(); }); it("drops everything when a navigation commits", () => { // Entries were rendered against the page the user just left. const h = harness(); h.cache.prefetch("/a"); h.cache.prefetch("/b"); h.cache.clear(); expect(h.cache.size()).toBe(0); expect(h.cache.take("/a")).toBeNull(); }); it("yields null when the fetch fails, so the click refetches", async () => { const calls: string[] = []; const cache = createNavPayloadCache({ now: () => 0, fetchPage: (t) => { calls.push(t); return Promise.reject(new Error("offline")); }, }); cache.prefetch("/a"); // A rejection must not escape as an unhandled rejection when nobody clicks. expect(await cache.take("/a")!).toBeNull(); expect(calls).toEqual(["/a"]); }); it("caches nothing for a response the fetch rejected as unusable", async () => { // fetchPage returns null for a non-200 or a redirect — the URL that // answered isn't the one navigate() would commit. const cache = createNavPayloadCache({ now: () => 0, fetchPage: () => Promise.resolve(null), }); cache.prefetch("/a"); expect(await cache.take("/a")!).toBeNull(); }); });