Skip to content
Open
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
8 changes: 7 additions & 1 deletion packages/vinext/src/client/navigation-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { AppRouterScrollIntent } from "vinext/shims/app-router-scroll-state
export type NavigationRuntimeSnapshot = {
pathname: string;
searchParams: [string, string][];
searchParamsAccessMode?: "dynamic" | "bailout" | "force-static";
};

export type NavigationRuntimeRscChunk = string | [3, string];
Expand Down Expand Up @@ -131,6 +132,7 @@ function isNavigationRuntimeSnapshot(value: unknown): value is NavigationRuntime
if (!isUnknownRecord(value)) return false;
const pathname = Reflect.get(value, "pathname");
const searchParams = Reflect.get(value, "searchParams");
const searchParamsAccessMode = Reflect.get(value, "searchParamsAccessMode");
return (
typeof pathname === "string" &&
Array.isArray(searchParams) &&
Expand All @@ -140,7 +142,11 @@ function isNavigationRuntimeSnapshot(value: unknown): value is NavigationRuntime
entry.length === 2 &&
typeof entry[0] === "string" &&
typeof entry[1] === "string",
)
) &&
(searchParamsAccessMode === undefined ||
searchParamsAccessMode === "dynamic" ||
searchParamsAccessMode === "bailout" ||
searchParamsAccessMode === "force-static")
);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,7 @@ export default createAppRscHandler({
draftModeSecret: __draftModeSecret,
dispatchMatchedPage({
clientReuseManifest,
clientSearchParams,
cleanPathname,
displayPathname,
formState,
Expand Down Expand Up @@ -812,6 +813,7 @@ export default createAppRscHandler({
}, layoutParamAccess, displayPathname);
},
clientReuseManifest,
clientSearchParams,
cleanPathname,
displayPathname,
clearRequestContext() {
Expand Down
28 changes: 22 additions & 6 deletions packages/vinext/src/server/app-browser-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
registerNavigationRuntimeBootstrap,
registerNavigationRuntimeFunctions,
type NavigationRuntimeNavigate,
type NavigationRuntimeSnapshot,
type NavigationRuntimeVisibleCommitMode,
type NavigationRuntimeRscBootstrap,
} from "../client/navigation-runtime.js";
Expand Down Expand Up @@ -1218,14 +1219,33 @@ function restoreHydrationNavigationContext(
pathname: string,
searchParams: SearchParamInput,
params: Record<string, string | string[]>,
searchParamsAccessMode?: NavigationRuntimeSnapshot["searchParamsAccessMode"],
): void {
setNavigationContext({
pathname,
searchParams: new URLSearchParams(searchParams),
params,
searchParamsAccessMode,
});
}

function applyHydrationNavigationSnapshot(
nav: NavigationRuntimeSnapshot,
params: Record<string, string | string[]>,
): void {
const shouldDeferLiveSearchParams =
nav.searchParamsAccessMode === "bailout" || nav.searchParamsAccessMode === "force-static";
// Static HTML hydrates against the same neutral search params the server
// rendered. Once hydration commits, the browser URL becomes authoritative
// and updates useSearchParams() without a server/client mismatch.
restoreHydrationNavigationContext(
nav.pathname,
shouldDeferLiveSearchParams ? nav.searchParams : window.location.search,
params,
nav.searchParamsAccessMode,
);
}

function restorePopstateScrollPosition(
state: unknown,
options?: {
Expand Down Expand Up @@ -1356,11 +1376,7 @@ async function readInitialRscStream(): Promise<ReadableStream<Uint8Array> | null
applyClientParams(vinext.__VINEXT_RSC_PARAMS__);
}
if (vinext.__VINEXT_RSC_NAV__) {
restoreHydrationNavigationContext(
vinext.__VINEXT_RSC_NAV__.pathname,
vinext.__VINEXT_RSC_NAV__.searchParams,
params,
);
applyHydrationNavigationSnapshot(vinext.__VINEXT_RSC_NAV__, params);
}

return createProgressiveRscStream();
Expand Down Expand Up @@ -1421,7 +1437,7 @@ function applyRuntimeRscBootstrap(rsc: NavigationRuntimeRscBootstrap): void {
applyClientParams(rsc.params);
}
if (rsc.nav) {
restoreHydrationNavigationContext(rsc.nav.pathname, rsc.nav.searchParams, params);
applyHydrationNavigationSnapshot(rsc.nav, params);
}
}

Expand Down
8 changes: 7 additions & 1 deletion packages/vinext/src/server/app-browser-error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isNavigationSignalError } from "../utils/navigation-signal.js";
import { isUnknownRecord } from "../utils/record.js";
import { isBailoutToCSRError } from "vinext/shims/navigation-errors";

type VinextHydrateRootErrorInfo = {
componentStack?: string;
Expand Down Expand Up @@ -81,5 +82,10 @@ export function prodOnCaughtError(error: unknown, errorInfo: VinextHydrateRootEr
}

export function prodOnRecoverableError(error: unknown): void {
reportGlobalError(error instanceof Error && error.cause !== undefined ? error.cause : error);
const cause = error instanceof Error && error.cause !== undefined ? error.cause : error;
// Static Client Component hooks intentionally leave an incomplete Suspense
// boundary for the browser to render. Match Next.js and do not report that
// expected bailout as a hydration failure.
if (isBailoutToCSRError(cause)) return;
reportGlobalError(cause);
}
3 changes: 3 additions & 0 deletions packages/vinext/src/server/app-page-cache-finalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type BuildAppPageCacheRenderObservation = (input: {
type FinalizeAppPageHtmlCacheResponseOptions = {
capturedDynamicUsageBeforeContextCleanup?: () => boolean;
capturedRscDataPromise: Promise<ArrayBuffer> | null;
searchParamsAccessedPromise?: Promise<boolean>;
cleanPathname: string;
consumeDynamicUsage: () => boolean;
dynamicUsageCheckComplete?: boolean;
Expand Down Expand Up @@ -169,8 +170,10 @@ export function finalizeAppPageHtmlCacheResponse(
const cachePromise = (async () => {
try {
const cachedHtml = await readStreamAsText(streamForCache);
const searchParamsAccessed = await (options.searchParamsAccessedPromise ?? false);

if (
searchParamsAccessed ||
options.capturedDynamicUsageBeforeContextCleanup?.() === true ||
options.consumeDynamicUsage()
) {
Expand Down
22 changes: 17 additions & 5 deletions packages/vinext/src/server/app-page-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ export type DispatchAppPageOptions<TRoute extends AppPageDispatchRoute> = {
},
) => Promise<AppPageElement>;
clientReuseManifest?: ClientReuseManifestParseResult;
clientSearchParams?: URLSearchParams;
cleanPathname: string;
displayPathname?: string;
clearRequestContext: () => void;
Expand Down Expand Up @@ -354,11 +355,7 @@ export type DispatchAppPageOptions<TRoute extends AppPageDispatchRoute> = {
scheduleBackgroundRegeneration: AppPageBackgroundRegenerator;
scriptNonce?: string;
searchParams: URLSearchParams;
setNavigationContext: (context: {
params: AppPageParams;
pathname: string;
searchParams: URLSearchParams;
}) => void;
setNavigationContext: (context: NavigationContext) => void;
renderMode?: AppRscRenderMode;
};

Expand Down Expand Up @@ -523,6 +520,7 @@ async function runAppPageRevalidationContext<
pathname: options.displayPathname ?? options.cleanPathname,
searchParams: new URLSearchParams(),
params: options.params,
searchParamsAccessMode: options.dynamicConfig === "force-static" ? "force-static" : "bailout",
});
return await runWithFetchDedupe(renderFn);
});
Expand Down Expand Up @@ -578,6 +576,14 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
(!isPrerender || options.pprFallbackShell !== undefined) && serveStreamingMetadata;
const isPrefetchDynamicShell = options.renderMode === APP_RSC_RENDER_MODE_PREFETCH_DYNAMIC_SHELL;
const isDraftMode = isDraftModeRequest(options.request, options.draftModeSecret);
const shouldBailoutClientSearchParams =
options.isProduction &&
(isDynamicError ||
(!isForceDynamic &&
!isDraftMode &&
options.isProgressiveActionRender !== true &&
!options.scriptNonce &&
(isPrerender || (currentRevalidateSeconds !== null && currentRevalidateSeconds > 0))));
const requestHeadersContext = getHeadersContext();
const shouldUseEmptySearchParams = isForceStatic || isPrefetchDynamicShell;
const hasRequestSearchParams =
Expand Down Expand Up @@ -988,7 +994,13 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
options.setNavigationContext({
pathname: options.displayPathname ?? options.cleanPathname,
searchParams: pageSearchParams,
clientSearchParams: options.clientSearchParams,
params: navigationParams,
searchParamsAccessMode: isForceStatic
? "force-static"
: shouldBailoutClientSearchParams
? "bailout"
: "dynamic",
});

const layoutClassifications = getEffectiveLayoutClassifications(
Expand Down
13 changes: 10 additions & 3 deletions packages/vinext/src/server/app-page-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ export async function renderAppPageLifecycle(
options.isPrerender !== true &&
cdnCacheAdapter.pageCacheMode === "edge";
const shouldCompleteDynamicUsageBeforeResponse =
usesResponseEdgeCache &&
(usesResponseEdgeCache || options.isDynamicError) &&
options.isProgressiveActionRender !== true &&
(revalidateSeconds === null || revalidateSeconds > 0) &&
!options.isDraftMode &&
Expand Down Expand Up @@ -1110,7 +1110,11 @@ export async function renderAppPageLifecycle(
});

let dynamicUsageCheckComplete = false;
if (shouldCompleteDynamicUsageBeforeResponse && !dynamicUsedDuringRender) {
if (
shouldCompleteDynamicUsageBeforeResponse &&
!dynamicUsageCheckComplete &&
!dynamicUsedDuringRender
) {
// A CDN must decide from the response headers whether to cache the body,
// unlike the origin cache which can wait for the stream to drain before
// committing it. Inspect CDN-managed candidates within strict time/size
Expand All @@ -1120,7 +1124,9 @@ export async function renderAppPageLifecycle(
const verification = await verifyCdnCacheCandidateStream(safeHtmlStream);
safeHtmlStream = verification.stream;
if (verification.complete) {
dynamicUsedDuringRender = dynamicUsedBeforeContextCleanup || options.consumeDynamicUsage();
const searchParamsAccessed = await (htmlRender.searchParamsAccessed ?? false);
dynamicUsedDuringRender =
searchParamsAccessed || dynamicUsedBeforeContextCleanup || options.consumeDynamicUsage();
dynamicUsedDuringHtmlRender = dynamicUsedDuringRender;
dynamicUsageCheckComplete = true;
}
Expand Down Expand Up @@ -1194,6 +1200,7 @@ export async function renderAppPageLifecycle(
return dynamicUsedBeforeContextCleanup;
},
capturedRscDataPromise: capturedRscDataRef.value,
searchParamsAccessedPromise: htmlRender.searchParamsAccessed,
cleanPathname: options.cleanPathname,
consumeDynamicUsage: options.consumeDynamicUsage,
dynamicUsageCheckComplete,
Expand Down
7 changes: 7 additions & 0 deletions packages/vinext/src/server/app-page-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,13 @@ export function resolveAppPageHtmlResponsePolicy(
};
}

if (options.dynamicUsedDuringRender && options.isDynamicError) {
return {
cacheControl: NO_STORE_CACHE_CONTROL,
shouldWriteToCache: false,
};
}

if ((options.isForceStatic || options.isDynamicError) && options.revalidateSeconds === null) {
return {
cacheControl: STATIC_CACHE_CONTROL,
Expand Down
16 changes: 14 additions & 2 deletions packages/vinext/src/server/app-page-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type AppSsrRenderResult = {
htmlStream: ReadableStream<Uint8Array>;
metadataReady: Promise<void>;
capturedRscData: Promise<ArrayBuffer> | null;
searchParamsAccessed?: Promise<boolean>;
shellErrorRecovered?: boolean;
/**
* Preload `Link` header value emitted by React during SSR (via `onHeaders`),
Expand Down Expand Up @@ -56,6 +57,7 @@ function normalizeAppSsrRenderResult(
htmlStream: raw,
metadataReady: resolvedMetadataReady,
capturedRscData: fallbackCapturedRscData,
searchParamsAccessed: Promise.resolve(false),
shellErrorRecovered: false,
};
}
Expand Down Expand Up @@ -206,6 +208,7 @@ type AppPageHtmlStreamRecoveryResult = {
response: Response | null;
metadataReady: Promise<void>;
capturedRscData: Promise<ArrayBuffer> | null;
searchParamsAccessed: Promise<boolean>;
shellErrorRecovered: boolean;
/** React-emitted preload `Link` header (already capped). */
linkHeader?: string;
Expand Down Expand Up @@ -310,14 +313,21 @@ export async function renderAppPageHtmlStreamWithRecovery<TSpecialError>(
): Promise<AppPageHtmlStreamRecoveryResult> {
try {
const rawResult = await options.renderHtmlStream();
const { htmlStream, metadataReady, capturedRscData, linkHeader, shellErrorRecovered } =
normalizeAppSsrRenderResult(rawResult);
const {
htmlStream,
metadataReady,
capturedRscData,
searchParamsAccessed,
linkHeader,
shellErrorRecovered,
} = normalizeAppSsrRenderResult(rawResult);
options.onShellRendered?.();
return {
htmlStream,
response: null,
metadataReady,
capturedRscData,
searchParamsAccessed: searchParamsAccessed ?? Promise.resolve(false),
shellErrorRecovered: shellErrorRecovered === true,
linkHeader,
};
Expand All @@ -329,6 +339,7 @@ export async function renderAppPageHtmlStreamWithRecovery<TSpecialError>(
response: await options.renderSpecialErrorResponse(specialError),
metadataReady: resolvedMetadataReady,
capturedRscData: null,
searchParamsAccessed: Promise.resolve(false),
shellErrorRecovered: false,
};
}
Expand All @@ -340,6 +351,7 @@ export async function renderAppPageHtmlStreamWithRecovery<TSpecialError>(
response: boundaryResponse,
metadataReady: resolvedMetadataReady,
capturedRscData: null,
searchParamsAccessed: Promise.resolve(false),
shellErrorRecovered: false,
};
}
Expand Down
6 changes: 6 additions & 0 deletions packages/vinext/src/server/app-rsc-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ function applyMiddlewareContextToResponse(

type DispatchMatchedPageOptions<TRoute> = {
clientReuseManifest: ClientReuseManifestParseResult;
clientSearchParams: URLSearchParams;
cleanPathname: string;
displayPathname: string;
formState: ReactFormState | null;
Expand Down Expand Up @@ -271,6 +272,7 @@ type RenderPagesFallbackOptions = {
};

type NavigationContextValue = {
clientSearchParams?: URLSearchParams;
params: AppPageParams;
pathname: string;
searchParams: URLSearchParams;
Expand Down Expand Up @@ -608,6 +610,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
// has/missing matching. This mirrors Next.js' navigation middleware fixture.
const normalizedUserlandRequest = requestWithoutRscSuffix(request);
const userlandRequest = requestWithoutRscCacheBustingSearchParam(normalizedUserlandRequest);
const visibleSearchParams = new URL(userlandRequest.url).searchParams;
const middlewareContext: AppRscMiddlewareContext = {
headers: null,
requestHeaders: null,
Expand Down Expand Up @@ -797,6 +800,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
options.setNavigationContext({
pathname: canonicalPathname,
searchParams: getResolvedSearchParams(),
clientSearchParams: visibleSearchParams,
params: {},
});

Expand Down Expand Up @@ -1110,6 +1114,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
options.setNavigationContext({
pathname: canonicalPathname,
searchParams: resolvedSearchParams,
clientSearchParams: visibleSearchParams,
params: renderParams,
});
const rootParams = pickRootParams(renderParams, route.rootParamNames);
Expand Down Expand Up @@ -1150,6 +1155,7 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(

const pageResponse = await options.dispatchMatchedPage({
clientReuseManifest,
clientSearchParams: visibleSearchParams,
cleanPathname,
displayPathname: canonicalPathname,
formState,
Expand Down
Loading
Loading