Files
cloudflare__vinext/tests/app-browser-client-reuse-manifest.test.ts
T
Nathan Nguyen 49d2f0966d feat(skip): plumb client reuse manifests through the app request path (#1717)
* feat(skip): plumb client reuse manifests through the app request path

The browser never sends a client reuse manifest and the server has no path
to receive one, so the static-layout skip planner (#1674) and observation
foundations (#1672) have nothing to act on. Skip transport stays fully off,
but the transport wiring those slices depend on is missing.

Add a bounded browser producer that derives a client reuse manifest from the
visible AppElements state and attaches it as X-Vinext-Client-Reuse-Manifest
only on soft navigations. Parse the header into a render/send-by-default
disposition and thread the parsed result through the RSC handler, the
generated app entry, page dispatch, and the render lifecycle. Wire the
per-layout observation tracker and segment-config classification into dispatch
so the enable-transport slice can read them.

No RSC payload is omitted and no response cache-control changes: the parsed
manifest is carried but never authorizes a skip yet. Unproven and absent-
manifest requests render and send exactly as before.

Slice 3 of 4 from #1437; enable-transport follows.

Refs #726

* fix(skip): address review feedback on manifest plumbing

- app-page-execution: defer layout debug classification until after the
  observation override so the emitted reason agrees with the final flag;
  emit a runtime-probe reason when an observation flips a build-time static
  layout to dynamic.
- app-page-dispatch: document classifyLayoutSegmentConfigFromModule as the
  request-time counterpart to the build-time classifier, noting the
  intentional revalidate===false divergence and the dev-mode classification.
- app-render-dependency: mark registerAppElementRenderDependencies as
  write-only until the enable slice's consumer lands.

* docs(skip): clarify layout classification comments per review

- app-page-dispatch: correct the classifyLayoutSegmentConfigFromModule
  docblock — the build-time classifier already normalizes revalidate=false
  to Infinity, so both agree on that case; the real difference is scope
  (request-time classifies dev-mode layouts not captured at build time).
- app-page-dispatch: document the merge precedence in
  createEffectiveLayoutClassifications — module segment config is
  authoritative over the build-time map.

* test(skip): pin observation override flipping build-time static layout to dynamic

Adds a parity assertion for the request-time behavioral change the review
flagged: isLayoutObservationDynamic downgrades a build-time `static` layout
to `d` (and emits a runtime-probe reason) when the observed render is unsafe
for static reuse, while a safe observation keeps it static. Complements the
existing children-subtree probe assertion in app-page-route-wiring.test.ts.

* fix(skip): fold probe-scoped dynamic usage into layout observation

The probe rewrite runs layout probes inside runWithIsolatedProbeDependencies,
which opens a child unified scope and resets dynamicUsageDetected. A
markDynamicUsage() during the probe that leaves no other observable trace
(e.g. "use cache: private") was therefore discarded on scope exit, so the
parent's consumeDynamicUsage() in the Layer-3 path returned false and the
layout could classify static where the pre-PR direct probe classified dynamic.

recordProbeDependencies now peeks the child scope's dynamic-usage flag before
it is reset and folds it into the per-layout observation
(dynamicUsageObserved), which isAppLayoutObservationUnsafeForStaticReuse
treats as unsafe. This keeps the observation signal a superset of the
markDynamicUsage() triggers the Layer-3 dynamicDetected path covered, so the
two signals can no longer diverge into a false-static. Adds peekDynamicUsage()
(non-resetting read) and a regression test.

* perf(skip): defer client reuse manifest production to the cache-miss path

The manifest header is excluded from VINEXT_RSC_VARY_HEADER, so it never
affects the cache-busting URL or the visited-response cache lookup. Producing
it up front ran createClientReuseManifestHeaderFromVisibleAppState (iterates
visible layout ids, builds a cache-variant proof per static layout, then
binary-searches the byte budget) on every soft navigate — including ones that
resolve entirely from the visited-response cache or a prefetch and never send
a request, where the work is pure waste.

Move production to just before the network fetch, gated on the same
`navigationKind === "navigate"` condition and computed from the captured
nav-start router state, so the emitted header is unchanged on the request path
while the cache-hit/prefetch-hit hot paths skip it entirely.

---------

Co-authored-by: James <james@eli.cx>
2026-06-02 20:25:25 +01:00

169 lines
6.0 KiB
TypeScript

import { describe, expect, it } from "vite-plus/test";
import { createArtifactCompatibilityEnvelope } from "../packages/vinext/src/server/artifact-compatibility.js";
import { createClientReuseManifestHeaderFromVisibleAppState } from "../packages/vinext/src/server/app-browser-client-reuse-manifest.js";
import { AppElementsWire } from "../packages/vinext/src/server/app-elements.js";
import {
CLIENT_REUSE_MANIFEST_SKIP_VERIFICATION_ENTRY_BUDGET,
DEFAULT_CLIENT_REUSE_MANIFEST_LIMITS,
parseClientReuseManifestHeader,
} from "../packages/vinext/src/server/client-reuse-manifest.js";
function createVisibleState(input: {
extraEntries?: Record<string, unknown>;
graphVersion?: string;
layoutFlags: Record<string, "s" | "d">;
layoutIds: readonly string[];
routeId?: string;
visibleCommitVersion?: number;
}) {
const artifactCompatibility = createArtifactCompatibilityEnvelope({
deploymentVersion: "deploy:test",
graphVersion: input.graphVersion ?? "graph:test",
rootBoundaryId: "/",
});
return {
elements: {
...AppElementsWire.createMetadataEntries({
interceptionContext: null,
layoutIds: input.layoutIds,
rootLayoutTreePath: "/",
routeId: input.routeId ?? "route:/current",
}),
[AppElementsWire.keys.artifactCompatibility]: artifactCompatibility,
[AppElementsWire.keys.layoutFlags]: input.layoutFlags,
...input.extraEntries,
},
visibleCommitVersion: input.visibleCommitVersion ?? 4,
};
}
describe("app browser client reuse manifest", () => {
it("builds a public manifest only for retained static layout entries", () => {
const header = createClientReuseManifestHeaderFromVisibleAppState(
createVisibleState({
extraEntries: {
"layout:/": "root layout",
"layout:/dynamic": "dynamic layout",
"page:/current": "page",
},
layoutFlags: {
"layout:/": "s",
"layout:/dynamic": "d",
"layout:/missing": "s",
},
layoutIds: ["layout:/", "layout:/dynamic", "layout:/missing"],
}),
);
const parsed = parseClientReuseManifestHeader(header);
expect(parsed.kind).toBe("parsed");
if (parsed.kind !== "parsed") {
throw new Error("Expected client reuse manifest to parse");
}
expect(parsed.manifest.visibleCommitVersion).toBe(4);
expect(parsed.manifest.replayWindow).toEqual({
validFromVisibleCommitVersion: 4,
validUntilVisibleCommitVersion: 4,
});
expect(parsed.manifest.entries.map((entry) => entry.id)).toEqual(["layout:/"]);
expect(parsed.manifest.entries[0]).toMatchObject({
kind: "layout",
privacy: "public",
});
});
it("keeps retained static layout proofs stable across source routes", () => {
function readLayoutEntry(routeId: string, graphVersion: string) {
const header = createClientReuseManifestHeaderFromVisibleAppState(
createVisibleState({
extraEntries: { "layout:/dashboard": "dashboard layout" },
graphVersion,
layoutFlags: { "layout:/dashboard": "s" },
layoutIds: ["layout:/dashboard"],
routeId,
}),
);
const parsed = parseClientReuseManifestHeader(header);
expect(parsed.kind).toBe("parsed");
if (parsed.kind !== "parsed") {
throw new Error("Expected client reuse manifest to parse");
}
return parsed.manifest.entries[0];
}
const settingsEntry = readLayoutEntry("route:/dashboard/settings", "graph:settings");
const profileEntry = readLayoutEntry("route:/dashboard/profile", "graph:profile");
expect(settingsEntry).toEqual(profileEntry);
});
it("trims entries rather than emitting an oversized manifest header", () => {
const layoutIds = Array.from({ length: 12 }, (_, index) => `layout:/section-${index}`);
const header = createClientReuseManifestHeaderFromVisibleAppState(
createVisibleState({
extraEntries: Object.fromEntries(layoutIds.map((layoutId) => [layoutId, layoutId])),
layoutFlags: Object.fromEntries(layoutIds.map((layoutId) => [layoutId, "s"])),
layoutIds,
}),
{
limits: {
...DEFAULT_CLIENT_REUSE_MANIFEST_LIMITS,
maxManifestBytes: 900,
},
},
);
expect(header).not.toBeNull();
expect(new TextEncoder().encode(header!).length).toBeLessThanOrEqual(900);
const parsed = parseClientReuseManifestHeader(header, {
limits: {
...DEFAULT_CLIENT_REUSE_MANIFEST_LIMITS,
maxManifestBytes: 900,
},
});
expect(parsed.kind).toBe("parsed");
if (parsed.kind !== "parsed") {
throw new Error("Expected trimmed client reuse manifest to parse");
}
expect(parsed.manifest.entries.length).toBeGreaterThan(0);
expect(parsed.manifest.entries.length).toBeLessThan(layoutIds.length);
});
it("caps default browser manifests to the server skip verification budget", () => {
const layoutIds = Array.from(
{ length: CLIENT_REUSE_MANIFEST_SKIP_VERIFICATION_ENTRY_BUDGET + 4 },
(_, index) => `layout:/section-${index}`,
);
const header = createClientReuseManifestHeaderFromVisibleAppState(
createVisibleState({
extraEntries: Object.fromEntries(layoutIds.map((layoutId) => [layoutId, layoutId])),
layoutFlags: Object.fromEntries(layoutIds.map((layoutId) => [layoutId, "s"])),
layoutIds,
}),
{
limits: {
...DEFAULT_CLIENT_REUSE_MANIFEST_LIMITS,
maxManifestBytes: 16_000,
},
},
);
const parsed = parseClientReuseManifestHeader(header, {
limits: {
...DEFAULT_CLIENT_REUSE_MANIFEST_LIMITS,
maxManifestBytes: 16_000,
},
});
expect(parsed.kind).toBe("parsed");
if (parsed.kind !== "parsed") {
throw new Error("Expected capped client reuse manifest to parse");
}
expect(parsed.manifest.entries.map((entry) => entry.id)).toEqual(
layoutIds.slice(0, CLIENT_REUSE_MANIFEST_SKIP_VERIFICATION_ENTRY_BUDGET),
);
});
});