Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f933043
feat(skip): enable proven static layout transport skips
NathanDrake2406 May 22, 2026
6bbc788
fix(skip): wire client reuse manifests into app page rendering
NathanDrake2406 May 22, 2026
370e5d8
fix(skip): keep partial RSC payloads out of shared caches
NathanDrake2406 May 22, 2026
58dfd28
fix(skip): send client reuse manifests during navigation
NathanDrake2406 May 22, 2026
deff83f
fix(skip): keep partial RSC payloads client-scoped
NathanDrake2406 May 22, 2026
e7ba059
fix(skip): require exact compatibility for transport omission
NathanDrake2406 May 22, 2026
d9e8205
Merge remote-tracking branch 'upstream/main' into nathan/726-skip-04-…
NathanDrake2406 May 22, 2026
60f57f0
fix(skip): gate static layout skip on param observations
NathanDrake2406 May 22, 2026
1e5d1e6
fix(skip): reject param-scoped static layout skips
NathanDrake2406 May 22, 2026
b04d9b7
fix(skip): use structural layout param scope
NathanDrake2406 May 22, 2026
e1aef58
fix(skip): reject data-dependent static layout transport skips
NathanDrake2406 May 22, 2026
7bbf954
fix(skip): block finite-revalidate layout skips
NathanDrake2406 May 22, 2026
7702c68
fix(skip): probe wrapped layout server components
NathanDrake2406 May 22, 2026
6de6236
fix(skip): isolate layout probe dependency tracking
NathanDrake2406 May 22, 2026
d0004bd
fix(skip): cap browser reuse manifests to verification budget
NathanDrake2406 May 22, 2026
1863f07
fix(skip): reject request-api layout reuse proofs
NathanDrake2406 May 23, 2026
61bd846
fix(skip): avoid consuming iterable layout children
NathanDrake2406 May 23, 2026
26ae459
fix(skip): classify unsafe layout observations as dynamic
NathanDrake2406 May 23, 2026
8b4713b
fix(skip): probe layouts with non-null children
NathanDrake2406 May 23, 2026
fcaac5d
fix(skip): reject unstable cache layout dependencies
NathanDrake2406 May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 14 additions & 17 deletions packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ const appPageRouteWiringPath = resolveEntryPath(
import.meta.url,
);
const appPageProbePath = resolveEntryPath("../server/app-page-probe.js", import.meta.url);
const appPageParamsPath = resolveEntryPath("../server/app-page-params.js", import.meta.url);
const appPageDispatchPath = resolveEntryPath("../server/app-page-dispatch.js", import.meta.url);
const appPageRequestPath = resolveEntryPath("../server/app-page-request.js", import.meta.url);
const appSegmentConfigPath = resolveEntryPath("../server/app-segment-config.js", import.meta.url);
Expand Down Expand Up @@ -255,12 +254,10 @@ import {
AppElementsWire as __AppElementsWire,
} from ${JSON.stringify(appElementsPath)};
import {
probeAppPageLayoutWithTracking as __probeAppPageLayoutWithTracking,
resolveAppPageChildSegments as __resolveAppPageChildSegments,
} from ${JSON.stringify(appPageRouteWiringPath)};
import { buildPageElements as __buildPageElements } from ${JSON.stringify(appPageElementBuilderPath)};
import {
resolveAppPageSegmentParams as __resolveAppPageSegmentParams,
} from ${JSON.stringify(appPageParamsPath)};
import { probeAppPage as __probeAppPage } from ${JSON.stringify(appPageProbePath)};
import {
dispatchAppPage as __dispatchAppPage,
Expand Down Expand Up @@ -440,7 +437,7 @@ function findIntercept(pathname, sourcePathname = null) {
return __routeMatcher.findIntercept(pathname, sourcePathname);
}

async function buildPageElements(route, params, routePath, pageRequest) {
async function buildPageElements(route, params, routePath, pageRequest, layoutParamAccess) {
return __buildPageElements({
route,
params,
Expand All @@ -451,6 +448,7 @@ async function buildPageElements(route, params, routePath, pageRequest) {
rootForbiddenModule: ${rootForbiddenVar ? rootForbiddenVar : "null"},
rootUnauthorizedModule: ${rootUnauthorizedVar ? rootUnauthorizedVar : "null"},
metadataRoutes,
layoutParamAccess,
basePath: __basePath,
});
}
Expand Down Expand Up @@ -512,6 +510,7 @@ export default __createAppRscHandler({
configRedirects: __configRedirects,
configRewrites: __configRewrites,
dispatchMatchedPage({
clientReuseManifest,
cleanPathname,
formState,
actionError,
Expand Down Expand Up @@ -544,16 +543,17 @@ export default __createAppRscHandler({
const _asyncRouteParams = makeThenableParams(params);
return __dispatchAppPage({
basePath: __basePath,
buildPageElement(targetRoute, targetParams, targetOpts, targetSearchParams) {
buildPageElement(targetRoute, targetParams, targetOpts, targetSearchParams, layoutParamAccess) {
return buildPageElements(targetRoute, targetParams, cleanPathname, {
opts: targetOpts,
searchParams: targetSearchParams,
isRscRequest,
request,
mountedSlotsHeader,
renderMode,
});
}, layoutParamAccess);
},
clientReuseManifest,
cleanPathname,
clearRequestContext() {
__clearRequestContext();
Expand Down Expand Up @@ -600,16 +600,13 @@ export default __createAppRscHandler({
mountedSlotsHeader,
params,
rootParams,
probeLayoutAt(li) {
const LayoutComp = route.layouts[li]?.default;
if (!LayoutComp) return null;
return LayoutComp({
params: makeThenableParams(__resolveAppPageSegmentParams(
route.routeSegments,
route.layoutTreePositions?.[li] ?? 0,
params,
)),
children: null,
probeLayoutAt(li, layoutParamAccess) {
return __probeAppPageLayoutWithTracking({
layoutIndex: li,
layoutParamAccess,
makeThenableParams,
matchedParams: params,
route,
});
},
probePage() {
Expand Down
170 changes: 170 additions & 0 deletions packages/vinext/src/server/app-browser-client-reuse-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import type { ArtifactCompatibilityEnvelope } from "./artifact-compatibility.js";
import {
buildCacheVariantWithRouteBudget,
DEFAULT_CACHE_VARIANT_BUDGET,
type StaticLayoutCacheProofOutputScope,
} from "./cache-proof.js";
import {
CLIENT_REUSE_MANIFEST_SKIP_VERIFICATION_ENTRY_BUDGET,
countUtf8Bytes,
DEFAULT_CLIENT_REUSE_MANIFEST_LIMITS,
serializeClientReuseManifest,
} from "./client-reuse-manifest.js";
import { AppElementsWire, type AppElements } from "./app-elements.js";
import {
createStaticLayoutClientReuseArtifactCompatibility,
createStaticLayoutClientReusePayloadHash,
createStaticLayoutClientReuseRouteId,
} from "./static-layout-client-reuse-proof.js";
import type { AppRouterState } from "./app-browser-state.js";

type ClientReuseManifestLimits = typeof DEFAULT_CLIENT_REUSE_MANIFEST_LIMITS;

type VisibleAppState = Pick<AppRouterState, "elements" | "visibleCommitVersion">;

type BrowserClientReuseManifestEntry = Readonly<{
artifactCompatibility: ArtifactCompatibilityEnvelope;
id: string;
payloadHash: string;
privacy: "public";
variantCacheKey: string;
}>;

type CreateClientReuseManifestHeaderOptions = Readonly<{
limits?: ClientReuseManifestLimits;
}>;

function capClientReuseManifestProducerLimits(
limits: ClientReuseManifestLimits,
): ClientReuseManifestLimits {
return {
...limits,
maxEntryCount: Math.min(
limits.maxEntryCount,
CLIENT_REUSE_MANIFEST_SKIP_VERIFICATION_ENTRY_BUDGET,
),
};
}

function serializeBoundedClientReuseManifest(input: {
entries: readonly BrowserClientReuseManifestEntry[];
limits: ClientReuseManifestLimits;
visibleCommitVersion: number;
}): string | null {
const entries = input.entries.slice(0, input.limits.maxEntryCount);
// Binary search for the largest prefix that fits. JSON array serialization
// is monotonic here: adding an entry cannot reduce the byte count.
let low = 1;
Comment thread
NathanDrake2406 marked this conversation as resolved.
let high = entries.length;
let best: string | null = null;

while (low <= high) {
const size = Math.floor((low + high) / 2);
const serialized = serializeClientReuseManifest({
entries: entries.slice(0, size),
replayWindow: {
validFromVisibleCommitVersion: input.visibleCommitVersion,
validUntilVisibleCommitVersion: input.visibleCommitVersion,
},
visibleCommitVersion: input.visibleCommitVersion,
});
if (countUtf8Bytes(serialized) <= input.limits.maxManifestBytes) {
best = serialized;
low = size + 1;
} else {
high = size - 1;
}
}

return best;
}

function hasRetainedElement(elements: AppElements, elementId: string): boolean {
return Object.hasOwn(elements, elementId);
}

function createStaticLayoutEntry(input: {
artifactCompatibility: ArtifactCompatibilityEnvelope;
layoutId: string;
}): BrowserClientReuseManifestEntry | null {
const routeId = createStaticLayoutClientReuseRouteId(input.layoutId);
const output: StaticLayoutCacheProofOutputScope = {
kind: "layout",
layoutId: input.layoutId,
rootBoundaryId: input.artifactCompatibility.rootBoundaryId,
routeId,
};
const candidateVariant = buildCacheVariantWithRouteBudget({
budget: DEFAULT_CACHE_VARIANT_BUDGET,
dimensions: [],
output,
routeBudget: {
routeId: output.routeId,
variantCacheKeys: [],
},
});
if (candidateVariant.kind !== "variant") {
return null;
}

const artifactCompatibility = createStaticLayoutClientReuseArtifactCompatibility({
artifactCompatibility: input.artifactCompatibility,
layoutId: input.layoutId,
rootBoundaryId: output.rootBoundaryId,
routeId: output.routeId,
variantCacheKey: candidateVariant.variant.cacheKey,
});

return {
artifactCompatibility,
id: input.layoutId,
payloadHash: createStaticLayoutClientReusePayloadHash({
artifactCompatibility,
layoutId: input.layoutId,
rootBoundaryId: output.rootBoundaryId,
routeId: output.routeId,
variantCacheKey: candidateVariant.variant.cacheKey,
}),
privacy: "public",
variantCacheKey: candidateVariant.variant.cacheKey,
};
}

export function createClientReuseManifestHeaderFromVisibleAppState(
state: VisibleAppState,
options: CreateClientReuseManifestHeaderOptions = {},
): string | null {
const limits = capClientReuseManifestProducerLimits(
options.limits ?? DEFAULT_CLIENT_REUSE_MANIFEST_LIMITS,
);
const metadata = AppElementsWire.readMetadata(state.elements);
const entries: BrowserClientReuseManifestEntry[] = [];

for (const layoutId of metadata.layoutIds) {
if (entries.length >= limits.maxEntryCount) break;
Comment thread
NathanDrake2406 marked this conversation as resolved.
if (layoutId.length > limits.maxEntryIdLength) continue;
if (metadata.layoutFlags[layoutId] !== "s") continue;
if (!hasRetainedElement(state.elements, layoutId)) continue;

const parsedKey = AppElementsWire.parseElementKey(layoutId);
if (parsedKey?.kind !== "layout") continue;

const entry = createStaticLayoutEntry({
artifactCompatibility: metadata.artifactCompatibility,
layoutId,
});
if (entry) {
entries.push(entry);
}
}

if (entries.length === 0) {
return null;
}

return serializeBoundedClientReuseManifest({
entries,
limits,
visibleCommitVersion: state.visibleCommitVersion,
});
}
14 changes: 9 additions & 5 deletions packages/vinext/src/server/app-browser-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { ElementsContext, Slot } from "vinext/shims/slot";
import type { RouteManifest } from "../routing/app-route-graph.js";
import { stripBasePath } from "../utils/base-path.js";
import { createOnUncaughtError } from "./app-browser-error.js";
import { createClientReuseManifestHeaderFromVisibleAppState } from "./app-browser-client-reuse-manifest.js";
import {
devOnCaughtError,
devOnUncaughtError,
Expand Down Expand Up @@ -131,7 +132,6 @@ import {
import {
ACTION_REDIRECT_HEADER,
ACTION_REDIRECT_TYPE_HEADER,
VINEXT_MOUNTED_SLOTS_HEADER,
VINEXT_PARAMS_HEADER,
VINEXT_RSC_REDIRECT_HEADER,
} from "./headers.js";
Expand Down Expand Up @@ -1383,16 +1383,20 @@ function bootstrapHydration(rscStream: ReadableStream<Uint8Array>): void {
// Pass navId so only this navigation (or a newer one) can clear it later.
setPendingPathname(url.pathname, navId);

const elementsAtNavStart = getBrowserRouterState().elements;
const routerStateAtNavStart = getBrowserRouterState();
const elementsAtNavStart = routerStateAtNavStart.elements;
const mountedSlotsHeader = getMountedSlotIdsHeader(elementsAtNavStart);
const clientReuseManifestHeader =
navigationKind === "navigate"
? createClientReuseManifestHeaderFromVisibleAppState(routerStateAtNavStart)
: null;
const requestHeaders = createRscRequestHeaders({
clientReuseManifestHeader,
interceptionContext: requestInterceptionContext,
mountedSlotsHeader,
Comment thread
NathanDrake2406 marked this conversation as resolved.
renderMode:
navigationKind === "refresh" ? APP_RSC_RENDER_MODE_REFRESH_PRESERVE_UI : undefined,
});
if (mountedSlotsHeader) {
requestHeaders.set(VINEXT_MOUNTED_SLOTS_HEADER, mountedSlotsHeader);
}
const rscUrl = await createRscRequestUrl(url.pathname + url.search, requestHeaders);
const cachedRoute = getVisitedResponse(
rscUrl,
Expand Down
Loading
Loading