mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
49d2f0966d
* 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>
169 lines
6.0 KiB
TypeScript
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),
|
|
);
|
|
});
|
|
});
|