Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
85 changes: 49 additions & 36 deletions packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader,
import { NextRequest, NextFetchEvent } from "next/server";
import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary";
import { LayoutSegmentProvider } from "vinext/layout-segment-context";
import { MetadataHead, mergeMetadata, resolveModuleMetadata, ViewportHead, mergeViewport, resolveModuleViewport } from "vinext/metadata";
import { MetadataHead, mergeMetadata, mergeMetadataForParent, resolveModuleMetadata, ViewportHead, mergeViewport, resolveModuleViewport } from "vinext/metadata";
${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(fileURLToPath(new URL("../server/metadata-routes.js", import.meta.url)).replace(/\\/g, "/"))};` : ""}
Expand Down Expand Up @@ -702,7 +702,7 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
.catch((err) => { console.error("[vinext] Layout generateMetadata() failed:", err); return null; });
_layoutMetaPromises.push(_metaP);
_accumulatedMeta = _metaP.then(async (_r) =>
_r ? mergeMetadata([await _parentForLayout, _r]) : await _parentForLayout
_r ? mergeMetadataForParent([await _parentForLayout, _r]) : await _parentForLayout
);
}
const [_metaResults, _vpResults] = await Promise.all([
Expand Down Expand Up @@ -1077,7 +1077,7 @@ async function buildPageElement(route, params, opts, searchParams) {
layoutMetaPromises.push(metaPromise);
// Advance accumulator: resolves to merged(layouts[0..i]) once layout[i] is done.
accumulatedMetaPromise = metaPromise.then(async (result) =>
result ? mergeMetadata([await parentForThisLayout, result]) : await parentForThisLayout
result ? mergeMetadataForParent([await parentForThisLayout, result]) : await parentForThisLayout
);
}
// Page's parent is the fully-accumulated layout metadata.
Expand Down Expand Up @@ -1560,6 +1560,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {

const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component");
let cleanPathname = pathname.replace(/\\.rsc$/, "");
const navigationPathname = cleanPathname;
const __cachePathname = navigationPathname;
let pageSearchParams = url.searchParams;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (non-blocking): __cachePathname is an alias for navigationPathname with no divergence point — they're always the same value. Consider whether the separate name adds clarity or whether using navigationPathname directly for cache keys would be simpler. The separate name does communicate intent ("this is for cache key computation"), so it's fine either way.


// Middleware response headers and custom rewrite status are stored in
// _mwCtx (per-request container) so handler() can merge them into
Expand Down Expand Up @@ -1619,10 +1622,11 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
if (rewriteUrl) {
const rewriteParsed = new URL(rewriteUrl, request.url);
cleanPathname = rewriteParsed.pathname;
// Carry over query params from the rewrite URL so that
// searchParams props, useSearchParams(), and navigation context
// reflect the rewrite destination, not the original request.
url.search = rewriteParsed.search;
// Carry the rewrite query into the server-rendered page props
// without mutating the original request URL or client navigation state.
if (rewriteParsed.search) {
pageSearchParams = rewriteParsed.searchParams;
}
// Capture custom status code from rewrite (e.g. NextResponse.rewrite(url, { status: 403 }))
if (mwResponse.status !== 200) {
_mwCtx.status = mwResponse.status;
Expand Down Expand Up @@ -1766,7 +1770,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
// Set navigation context for Server Components.
// Note: Headers context is already set by runWithRequestContext in the handler wrapper.
setNavigationContext({
pathname: cleanPathname,
pathname: navigationPathname,
searchParams: url.searchParams,
params: {},
});
Expand Down Expand Up @@ -1882,11 +1886,11 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
if (match) {
const { route: actionRoute, params: actionParams } = match;
setNavigationContext({
pathname: cleanPathname,
pathname: navigationPathname,
searchParams: url.searchParams,
params: actionParams,
});
element = buildPageElement(actionRoute, actionParams, undefined, url.searchParams);
element = buildPageElement(actionRoute, actionParams, undefined, pageSearchParams);
} else {
element = createElement("div", null, "Page not found");
}
Expand Down Expand Up @@ -1979,7 +1983,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {

// Update navigation context with matched params
setNavigationContext({
pathname: cleanPathname,
pathname: navigationPathname,
searchParams: url.searchParams,
Comment thread
JaredStowell marked this conversation as resolved.
params,
});
Expand Down Expand Up @@ -2158,7 +2162,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
if (isForceStatic) {
setHeadersContext({ headers: new Headers(), cookies: new Map() });
setNavigationContext({
pathname: cleanPathname,
pathname: navigationPathname,
searchParams: new URLSearchParams(),
params,
});
Expand All @@ -2177,7 +2181,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
accessError: new Error(errorMsg),
});
setNavigationContext({
pathname: cleanPathname,
pathname: navigationPathname,
searchParams: new URLSearchParams(),
params,
});
Expand Down Expand Up @@ -2205,15 +2209,15 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
!isForceDynamic &&
revalidateSeconds !== null && revalidateSeconds > 0 && revalidateSeconds !== Infinity
) {
const __isrKey = isRscRequest ? __isrRscKey(cleanPathname) : __isrHtmlKey(cleanPathname);
const __isrKey = isRscRequest ? __isrRscKey(__cachePathname) : __isrHtmlKey(__cachePathname);
try {
const __cached = await __isrGet(__isrKey);
if (__cached && !__cached.isStale && __cached.value.value && __cached.value.value.kind === "APP_PAGE") {
const __cachedValue = __cached.value.value;
const __hasRsc = !!__cachedValue.rscData;
const __hasHtml = typeof __cachedValue.html === "string" && __cachedValue.html.length > 0;
if (isRscRequest && __hasRsc) {
__isrDebug?.("HIT (RSC)", cleanPathname);
__isrDebug?.("HIT (RSC)", __cachePathname);
setHeadersContext(null);
setNavigationContext(null);
return new Response(__cachedValue.rscData, {
Expand All @@ -2227,7 +2231,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
});
}
if (!isRscRequest && __hasHtml) {
__isrDebug?.("HIT (HTML)", cleanPathname);
__isrDebug?.("HIT (HTML)", __cachePathname);
setHeadersContext(null);
setNavigationContext(null);
return new Response(__cachedValue.html, {
Expand All @@ -2240,15 +2244,15 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
},
});
}
__isrDebug?.("MISS (empty cached entry)", cleanPathname);
__isrDebug?.("MISS (empty cached entry)", __cachePathname);
}
if (__cached && __cached.isStale && __cached.value.value && __cached.value.value.kind === "APP_PAGE") {
// Stale cache hit — serve stale immediately, trigger background regeneration.
// Regen writes both keys independently so neither path blocks on the other.
const __staleValue = __cached.value.value;
const __staleStatus = __staleValue.status || 200;
const __revalSecs = revalidateSeconds;
__triggerBackgroundRegeneration(cleanPathname, async function() {
__triggerBackgroundRegeneration(__cachePathname, async function() {
// Re-render the page to produce fresh HTML + RSC data for the cache
// Use an empty headers context for background regeneration — not the original
// user request — to prevent user-specific cookies/auth headers from leaking
Expand All @@ -2260,8 +2264,17 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
});
const __revalResult = await _runWithUnifiedCtx(__revalUCtx, async () => {
_ensureFetchPatch();
setNavigationContext({ pathname: cleanPathname, searchParams: url.searchParams, params });
const __revalElement = await buildPageElement(route, params, undefined, url.searchParams);
setNavigationContext({
pathname: navigationPathname,
searchParams: url.searchParams,
params,
});
const __revalElement = await buildPageElement(
route,
params,
undefined,
pageSearchParams,
);
const __revalOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern);
const __revalRscStream = renderToReadableStream(__revalElement, { onError: __revalOnError });
// Tee RSC stream: one for SSR, one to capture rscData
Expand Down Expand Up @@ -2299,18 +2312,18 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
__revalChunks.push(__revalDecoder.decode());
const __freshHtml = __revalChunks.join("");
const __freshRscData = await __rscDataPromise;
const __pageTags = __pageCacheTags(cleanPathname, getCollectedFetchTags());
const __pageTags = __pageCacheTags(__cachePathname, getCollectedFetchTags());
return { html: __freshHtml, rscData: __freshRscData, tags: __pageTags };
});
// Write HTML and RSC to their own keys independently — no races
await Promise.all([
__isrSet(__isrHtmlKey(cleanPathname), { kind: "APP_PAGE", html: __revalResult.html, rscData: undefined, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __revalResult.tags),
__isrSet(__isrRscKey(cleanPathname), { kind: "APP_PAGE", html: "", rscData: __revalResult.rscData, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __revalResult.tags),
__isrSet(__isrHtmlKey(__cachePathname), { kind: "APP_PAGE", html: __revalResult.html, rscData: undefined, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __revalResult.tags),
__isrSet(__isrRscKey(__cachePathname), { kind: "APP_PAGE", html: "", rscData: __revalResult.rscData, headers: undefined, postponed: undefined, status: 200 }, __revalSecs, __revalResult.tags),
]);
__isrDebug?.("regen complete", cleanPathname);
__isrDebug?.("regen complete", __cachePathname);
});
if (isRscRequest && __staleValue.rscData) {
__isrDebug?.("STALE (RSC)", cleanPathname);
__isrDebug?.("STALE (RSC)", __cachePathname);
setHeadersContext(null);
setNavigationContext(null);
return new Response(__staleValue.rscData, {
Expand All @@ -2324,7 +2337,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
});
}
if (!isRscRequest && typeof __staleValue.html === "string" && __staleValue.html.length > 0) {
__isrDebug?.("STALE (HTML)", cleanPathname);
__isrDebug?.("STALE (HTML)", __cachePathname);
setHeadersContext(null);
setNavigationContext(null);
return new Response(__staleValue.html, {
Expand All @@ -2338,10 +2351,10 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
});
}
// Stale entry exists but is empty for this request type — fall through to render
__isrDebug?.("STALE MISS (empty stale entry)", cleanPathname);
__isrDebug?.("STALE MISS (empty stale entry)", __cachePathname);
}
if (!__cached) {
__isrDebug?.("MISS (no cache entry)", cleanPathname);
__isrDebug?.("MISS (no cache entry)", __cachePathname);
}
} catch (__isrReadErr) {
// Cache read failure — fall through to normal rendering
Expand Down Expand Up @@ -2393,15 +2406,15 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
const sourceMatch = matchRoute(sourceRoute.pattern);
const sourceParams = sourceMatch ? sourceMatch.params : {};
setNavigationContext({
pathname: cleanPathname,
pathname: navigationPathname,
searchParams: url.searchParams,
params: intercept.matchedParams,
});
const interceptElement = await buildPageElement(sourceRoute, sourceParams, {
interceptSlot: intercept.slotName,
interceptPage: intercept.page,
interceptParams: intercept.matchedParams,
}, url.searchParams);
}, pageSearchParams);
const interceptOnError = createRscOnErrorHandler(
request,
cleanPathname,
Expand All @@ -2427,7 +2440,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {

let element;
try {
element = await buildPageElement(route, params, interceptOpts, url.searchParams);
element = await buildPageElement(route, params, interceptOpts, pageSearchParams);
} catch (buildErr) {
// Check for redirect/notFound/forbidden/unauthorized thrown during metadata resolution or async components
if (buildErr && typeof buildErr === "object" && "digest" in buildErr) {
Expand Down Expand Up @@ -2713,12 +2726,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
// these writes never race or clobber each other.
if (process.env.NODE_ENV === "production" && __isrRscDataPromise) {
responseHeaders["X-Vinext-Cache"] = "MISS";
const __isrKeyRsc = __isrRscKey(cleanPathname);
const __isrKeyRsc = __isrRscKey(__cachePathname);
const __revalSecsRsc = revalidateSeconds;
const __rscWritePromise = (async () => {
try {
const __rscDataForCache = await __isrRscDataPromise;
const __pageTags = __pageCacheTags(cleanPathname, getCollectedFetchTags());
const __pageTags = __pageCacheTags(__cachePathname, getCollectedFetchTags());
await __isrSet(__isrKeyRsc, { kind: "APP_PAGE", html: "", rscData: __rscDataForCache, headers: undefined, postponed: undefined, status: 200 }, __revalSecsRsc, __pageTags);
__isrDebug?.("RSC cache written", __isrKeyRsc);
} catch (__rscWriteErr) {
Expand Down Expand Up @@ -2908,8 +2921,8 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
}));
if (__isrResponseProd.body) {
const [__streamForClient, __streamForCache] = __isrResponseProd.body.tee();
const __isrKey = __isrHtmlKey(cleanPathname);
const __isrKeyRscFromHtml = __isrRscKey(cleanPathname);
const __isrKey = __isrHtmlKey(__cachePathname);
const __isrKeyRscFromHtml = __isrRscKey(__cachePathname);
const __revalSecs = revalidateSeconds;
const __capturedRscDataPromise = __isrRscDataPromise;
const __cachePromise = (async () => {
Expand All @@ -2924,7 +2937,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
}
__chunks.push(__decoder.decode());
const __fullHtml = __chunks.join("");
const __pageTags = __pageCacheTags(cleanPathname, getCollectedFetchTags());
const __pageTags = __pageCacheTags(__cachePathname, getCollectedFetchTags());
// Write HTML and RSC to their own keys independently.
// RSC data was captured by the tee above (before isRscRequest branch)
// so an initial browser visit (HTML request) also populates the RSC key,
Expand Down
Loading
Loading