mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
743f252b7a
* refactor(isr): centralise HIT/STALE/MISS + Cache-Control decision in isr-decision.ts Add server/isr-decision.ts as the single owner of the ISR cache policy decision across all four call sites (app-page-cache, app-route-handler, pages-page-data, dev-server). Every cache disposition, background-regen flag, and Cache-Control string now flows through decideIsr() or its MISS helpers. Also eliminate all hardcoded Cache-Control literals outside cache-control.ts (NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL, and raw s-maxage strings in dev-server.ts, pages-page-response.ts, pages-page-handler.ts). One deliberate behaviour change: dev STALE responses now emit s-maxage=0, stale-while-revalidate (matching prod Pages Router) instead of s-maxage=<revalidate>, stale-while-revalidate. This closes a dev/prod parity gap that was masked by the duplication. Closes #1783 * fix(isr): un-export IsrDisposition and IsrPolicyKind (knip unused-export) * docs(isr): tighten isr-decision docstring and hoist resolveRevalidate * refactor(isr): pass cacheState directly to decideIsr, drop hasUsableValue/isStale Replace the two-field hasUsableValue+isStale decomposition with a single cacheState: "HIT" | "STALE" | "MISS" parameter that callers already have. Also remove the before/after equivalence table from the module docstring, which is only relevant during the migration window. All five call sites updated; tsc --noEmit passes with no errors. * test(isr): update decideIsr tests for cacheState
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
import { describe, expect, it } from "vite-plus/test";
|
|
import {
|
|
decideIsr,
|
|
buildMissIsrCacheControl,
|
|
buildAppRouteMissIsrCacheControl,
|
|
ISR_NEVER_CACHE_CONTROL,
|
|
ISR_NO_STORE_CACHE_CONTROL,
|
|
} from "../packages/vinext/src/server/isr-decision.js";
|
|
|
|
// ─── MISS ────────────────────────────────────────────────────────────────────
|
|
|
|
describe("decideIsr — MISS", () => {
|
|
it("returns MISS when cacheState is MISS", () => {
|
|
const d = decideIsr({
|
|
cacheState: "MISS",
|
|
kind: "app-page",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.disposition).toBe("MISS");
|
|
expect(d.scheduleRegeneration).toBe(false);
|
|
expect(d.cacheControl).toBe("");
|
|
});
|
|
|
|
it("returns MISS for MISS cacheState regardless of route kind", () => {
|
|
const d = decideIsr({
|
|
cacheState: "MISS",
|
|
kind: "pages",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.disposition).toBe("MISS");
|
|
expect(d.scheduleRegeneration).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── HIT ─────────────────────────────────────────────────────────────────────
|
|
|
|
describe("decideIsr — HIT", () => {
|
|
it("app-page HIT without expire: unbounded SWR", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "app-page",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.disposition).toBe("HIT");
|
|
expect(d.scheduleRegeneration).toBe(false);
|
|
expect(d.cacheControl).toBe("s-maxage=60, stale-while-revalidate");
|
|
});
|
|
|
|
it("app-page HIT with expireSeconds only (no cacheControlMeta): expire is unknown, unbounded SWR", () => {
|
|
// expireSeconds is only a fallback for cacheControlMeta. Without metadata,
|
|
// the expire ceiling is unknown and the unbounded form is used.
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "app-page",
|
|
revalidateSeconds: 60,
|
|
expireSeconds: 300,
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=60, stale-while-revalidate");
|
|
});
|
|
|
|
it("app-page HIT with cacheControlMeta including expire: finite SWR window", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "app-page",
|
|
revalidateSeconds: 60,
|
|
expireSeconds: 300,
|
|
cacheControlMeta: { revalidate: 60, expire: 300 },
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=60, stale-while-revalidate=240");
|
|
});
|
|
|
|
it("app-page HIT: prefers cacheControlMeta revalidate over route default", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "app-page",
|
|
revalidateSeconds: 60,
|
|
cacheControlMeta: { revalidate: 30 },
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=30, stale-while-revalidate");
|
|
});
|
|
|
|
it("app-page HIT: cacheControlMeta expire overrides route expireSeconds", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "app-page",
|
|
revalidateSeconds: 60,
|
|
expireSeconds: 999,
|
|
cacheControlMeta: { revalidate: 60, expire: 300 },
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=60, stale-while-revalidate=240");
|
|
});
|
|
|
|
it("app-page HIT: cacheControlMeta present but no expire — expireSeconds used as fallback", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "app-page",
|
|
revalidateSeconds: 60,
|
|
expireSeconds: 300,
|
|
cacheControlMeta: { revalidate: 60 },
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=60, stale-while-revalidate=240");
|
|
});
|
|
|
|
it("pages HIT without cacheControlMeta: unbounded SWR", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "pages",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=60, stale-while-revalidate");
|
|
});
|
|
|
|
it("pages HIT with cacheControlMeta and expire: finite SWR window", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "pages",
|
|
revalidateSeconds: 60,
|
|
cacheControlMeta: { revalidate: 60, expire: 300 },
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=60, stale-while-revalidate=240");
|
|
});
|
|
|
|
it("app-route HIT finite revalidate (no metadata): uses route policy, unbounded SWR", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "app-route",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=60, stale-while-revalidate");
|
|
});
|
|
|
|
it("app-route HIT revalidate=0: emits NEVER_CACHE_CONTROL", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "app-route",
|
|
revalidateSeconds: 0,
|
|
});
|
|
expect(d.cacheControl).toBe(ISR_NEVER_CACHE_CONTROL);
|
|
});
|
|
|
|
it("app-route HIT revalidate=Infinity: emits STATIC_CACHE_CONTROL", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "app-route",
|
|
revalidateSeconds: Infinity,
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=31536000, stale-while-revalidate");
|
|
});
|
|
|
|
it("app-route HIT: cacheControlMeta revalidate=0 wins over route default", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "app-route",
|
|
revalidateSeconds: 60,
|
|
cacheControlMeta: { revalidate: 0 },
|
|
});
|
|
expect(d.cacheControl).toBe(ISR_NEVER_CACHE_CONTROL);
|
|
});
|
|
|
|
it("dev HIT: unbounded SWR (no special gates)", () => {
|
|
const d = decideIsr({
|
|
cacheState: "HIT",
|
|
kind: "dev",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.disposition).toBe("HIT");
|
|
expect(d.cacheControl).toBe("s-maxage=60, stale-while-revalidate");
|
|
});
|
|
});
|
|
|
|
// ─── STALE ───────────────────────────────────────────────────────────────────
|
|
|
|
describe("decideIsr — STALE", () => {
|
|
it("app-page STALE without expire: s-maxage=0 (canonical STALE fallback)", () => {
|
|
const d = decideIsr({
|
|
cacheState: "STALE",
|
|
kind: "app-page",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.disposition).toBe("STALE");
|
|
expect(d.scheduleRegeneration).toBe(true);
|
|
expect(d.cacheControl).toBe("s-maxage=0, stale-while-revalidate");
|
|
});
|
|
|
|
it("app-page STALE with cacheControlMeta and expire: uses route policy (same as HIT)", () => {
|
|
const d = decideIsr({
|
|
cacheState: "STALE",
|
|
kind: "app-page",
|
|
revalidateSeconds: 60,
|
|
cacheControlMeta: { revalidate: 60, expire: 300 },
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=60, stale-while-revalidate=240");
|
|
});
|
|
|
|
it("app-page STALE: scheduleRegeneration is true", () => {
|
|
const d = decideIsr({
|
|
cacheState: "STALE",
|
|
kind: "app-page",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.scheduleRegeneration).toBe(true);
|
|
});
|
|
|
|
it("pages STALE without expire: s-maxage=0", () => {
|
|
const d = decideIsr({
|
|
cacheState: "STALE",
|
|
kind: "pages",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=0, stale-while-revalidate");
|
|
});
|
|
|
|
it("pages STALE with cacheControlMeta and expire: finite SWR window", () => {
|
|
const d = decideIsr({
|
|
cacheState: "STALE",
|
|
kind: "pages",
|
|
revalidateSeconds: 15,
|
|
cacheControlMeta: { revalidate: 15, expire: 300 },
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=15, stale-while-revalidate=285");
|
|
});
|
|
|
|
it("app-route STALE revalidate=0: NEVER_CACHE_CONTROL", () => {
|
|
const d = decideIsr({
|
|
cacheState: "STALE",
|
|
kind: "app-route",
|
|
revalidateSeconds: 0,
|
|
});
|
|
expect(d.cacheControl).toBe(ISR_NEVER_CACHE_CONTROL);
|
|
});
|
|
|
|
it("app-route STALE revalidate=Infinity: STATIC_CACHE_CONTROL", () => {
|
|
const d = decideIsr({
|
|
cacheState: "STALE",
|
|
kind: "app-route",
|
|
revalidateSeconds: Infinity,
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=31536000, stale-while-revalidate");
|
|
});
|
|
|
|
it("app-route STALE finite (no cacheControlMeta): s-maxage=0 (STALE fallback, expire unknown)", () => {
|
|
const d = decideIsr({
|
|
cacheState: "STALE",
|
|
kind: "app-route",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.cacheControl).toBe("s-maxage=0, stale-while-revalidate");
|
|
});
|
|
|
|
it("dev STALE: s-maxage=0 (deliberate parity fix, now matches prod Pages Router)", () => {
|
|
// Previously emitted `s-maxage=<secs>, stale-while-revalidate`. Aligned to
|
|
// the canonical buildCachedRevalidateCacheControl("STALE", secs) result which
|
|
// is `s-maxage=0, stale-while-revalidate` when expire is absent, matching prod.
|
|
const d = decideIsr({
|
|
cacheState: "STALE",
|
|
kind: "dev",
|
|
revalidateSeconds: 60,
|
|
});
|
|
expect(d.disposition).toBe("STALE");
|
|
expect(d.scheduleRegeneration).toBe(true);
|
|
expect(d.cacheControl).toBe("s-maxage=0, stale-while-revalidate");
|
|
});
|
|
});
|
|
|
|
// ─── buildMissIsrCacheControl ─────────────────────────────────────────────────
|
|
|
|
describe("buildMissIsrCacheControl", () => {
|
|
it("without expire: unbounded SWR", () => {
|
|
expect(buildMissIsrCacheControl(60)).toBe("s-maxage=60, stale-while-revalidate");
|
|
});
|
|
|
|
it("with expire: finite SWR window", () => {
|
|
expect(buildMissIsrCacheControl(60, 300)).toBe("s-maxage=60, stale-while-revalidate=240");
|
|
});
|
|
|
|
it("expire <= revalidate: no SWR suffix", () => {
|
|
expect(buildMissIsrCacheControl(300, 300)).toBe("s-maxage=300");
|
|
});
|
|
});
|
|
|
|
// ─── buildAppRouteMissIsrCacheControl ─────────────────────────────────────────
|
|
|
|
describe("buildAppRouteMissIsrCacheControl", () => {
|
|
it("revalidate=0: NEVER_CACHE_CONTROL", () => {
|
|
expect(buildAppRouteMissIsrCacheControl(0)).toBe(ISR_NEVER_CACHE_CONTROL);
|
|
});
|
|
|
|
it("revalidate=Infinity: STATIC_CACHE_CONTROL", () => {
|
|
expect(buildAppRouteMissIsrCacheControl(Infinity)).toBe(
|
|
"s-maxage=31536000, stale-while-revalidate",
|
|
);
|
|
});
|
|
|
|
it("finite revalidate with expire: finite SWR window", () => {
|
|
expect(buildAppRouteMissIsrCacheControl(60, 600)).toBe(
|
|
"s-maxage=60, stale-while-revalidate=540",
|
|
);
|
|
});
|
|
|
|
it("finite revalidate without expire: unbounded SWR", () => {
|
|
expect(buildAppRouteMissIsrCacheControl(60)).toBe("s-maxage=60, stale-while-revalidate");
|
|
});
|
|
});
|
|
|
|
// ─── re-exported constants ────────────────────────────────────────────────────
|
|
|
|
describe("ISR_NEVER_CACHE_CONTROL / ISR_NO_STORE_CACHE_CONTROL", () => {
|
|
it("ISR_NEVER_CACHE_CONTROL matches the canonical value", () => {
|
|
expect(ISR_NEVER_CACHE_CONTROL).toBe("private, no-cache, no-store, max-age=0, must-revalidate");
|
|
});
|
|
|
|
it("ISR_NO_STORE_CACHE_CONTROL matches the canonical value", () => {
|
|
expect(ISR_NO_STORE_CACHE_CONTROL).toBe("no-store, must-revalidate");
|
|
});
|
|
});
|