From 3c7637cdd6050e286a5e7cd439bdd238756e9c24 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Mon, 23 Jun 2025 11:23:53 +0200 Subject: [PATCH] Revert "[ppr] RDC for RSCs" This reverts commit 6c70938cb748c0d196da9344d072a52d31e62914. --- packages/next/errors.json | 4 +- packages/next/src/build/index.ts | 2 - packages/next/src/build/templates/app-page.ts | 122 +++++----- .../next/src/server/app-render/app-render.tsx | 10 +- .../src/server/async-storage/request-store.ts | 6 +- packages/next/src/server/base-server.ts | 67 ++++-- .../incremental-cache/file-system-cache.ts | 37 +-- .../src/server/lib/incremental-cache/index.ts | 40 +--- packages/next/src/server/request-meta.ts | 27 +-- .../next/src/server/response-cache/types.ts | 5 - .../custom-routes/test/index.test.js | 1 - .../dynamic-routing/test/index.test.js | 1 - test/ppr-tests-manifest.json | 3 +- .../app-dir/resume-data-cache/app/layout.tsx | 9 - .../app-dir/resume-data-cache/app/page.tsx | 32 --- .../resume-data-cache/app/revalidate/route.ts | 6 - .../app-dir/resume-data-cache/next.config.js | 11 - .../resume-data-cache.test.ts | 109 --------- .../app/dyn/[slug]/page.js | 2 +- .../app/rewrite/[slug]/page.js | 12 +- .../required-server-files-ppr.test.ts | 212 ++++++------------ 21 files changed, 206 insertions(+), 512 deletions(-) delete mode 100644 test/production/app-dir/resume-data-cache/app/layout.tsx delete mode 100644 test/production/app-dir/resume-data-cache/app/page.tsx delete mode 100644 test/production/app-dir/resume-data-cache/app/revalidate/route.ts delete mode 100644 test/production/app-dir/resume-data-cache/next.config.js delete mode 100644 test/production/app-dir/resume-data-cache/resume-data-cache.test.ts diff --git a/packages/next/errors.json b/packages/next/errors.json index b2f99a20c313..c5896248569a 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -712,7 +712,5 @@ "711": "Can't resolve %s", "712": "`rspack.warnForEdgeRuntime` is not supported by the wasm bindings.", "713": "Unexpected error during process lookup", - "714": "cannot run loadNative when `NEXT_TEST_WASM` is set", - "715": "expected a result to be returned", - "716": "expected a page response, got %s" + "714": "cannot run loadNative when `NEXT_TEST_WASM` is set" } diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index f9ceb148a266..95adb78da216 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -447,7 +447,6 @@ export type RoutesManifest = { prefetchSegmentHeader: typeof NEXT_ROUTER_SEGMENT_PREFETCH_HEADER prefetchSegmentDirSuffix: typeof RSC_SEGMENTS_DIR_SUFFIX prefetchSegmentSuffix: typeof RSC_SEGMENT_SUFFIX - dynamicRSCPrerender: boolean } rewriteHeaders: { pathHeader: typeof NEXT_REWRITTEN_PATH_HEADER @@ -1371,7 +1370,6 @@ export default async function build( prefetchSegmentHeader: NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, prefetchSegmentSuffix: RSC_SEGMENT_SUFFIX, prefetchSegmentDirSuffix: RSC_SEGMENTS_DIR_SUFFIX, - dynamicRSCPrerender: isAppPPREnabled, }, rewriteHeaders: { pathHeader: NEXT_REWRITTEN_PATH_HEADER, diff --git a/packages/next/src/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts index f15aa8ef18d0..aad1b98bfade 100644 --- a/packages/next/src/build/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -36,7 +36,6 @@ import { import { getBotType, isBot } from '../../shared/lib/router/utils/is-bot' import { CachedRouteKind, - IncrementalCacheKind, type CachedAppPageValue, type CachedPageValue, type ResponseCacheEntry, @@ -127,6 +126,7 @@ export async function handler( const multiZoneDraftMode = process.env .__NEXT_MULTI_ZONE_DRAFT_MODE as any as boolean + const initialPostponed = getRequestMeta(req, 'postponed') // TODO: replace with more specific flags const minimalMode = getRequestMeta(req, 'minimalMode') @@ -260,9 +260,7 @@ export async function handler( // If we're in minimal mode, then try to get the postponed information from // the request metadata. If available, use it for resuming the postponed // render. - const minimalPostponed = isRoutePPREnabled - ? getRequestMeta(req, 'postponed') - : undefined + const minimalPostponed = isRoutePPREnabled ? initialPostponed : undefined // If PPR is enabled, and this is a RSC request (but not a prefetch), then // we can use this fact to only generate the flight data for the request @@ -299,10 +297,10 @@ export async function handler( !isSSG || // If this request has provided postponed data, it supports dynamic // HTML. - typeof minimalPostponed === 'string' || + typeof initialPostponed === 'string' || // If this is a dynamic RSC request, then this render supports dynamic // HTML (it's dynamic). - (isDynamicRSCRequest && !minimalMode) + isDynamicRSCRequest // When html bots request PPR page, perform the full dynamic rendering. const shouldWaitOnAllReady = isHtmlBot && isRoutePPREnabled @@ -417,8 +415,6 @@ export async function handler( }) } - const incrementalCache = getRequestMeta(req, 'incrementalCache') - const doRender = async ({ span, postponed, @@ -436,9 +432,6 @@ export async function handler( */ fallbackRouteParams: FallbackRouteParams | null }): Promise => { - // When we're resuming a render, we should allow dynamic response. - if (typeof postponed === 'string') supportsDynamicResponse = true - const context: AppPageRouteHandlerContext = { query, params, @@ -464,7 +457,8 @@ export async function handler( postponed, shouldWaitOnAllReady, serveStreamingMetadata, - supportsDynamicResponse, + supportsDynamicResponse: + typeof postponed === 'string' || supportsDynamicResponse, buildManifest, nextFontManifest, reactLoadableManifest, @@ -492,11 +486,21 @@ export async function handler( reactMaxHeadersLength: nextConfig.reactMaxHeadersLength, multiZoneDraftMode, - incrementalCache, + incrementalCache: getRequestMeta(req, 'incrementalCache'), cacheLifeProfiles: nextConfig.experimental.cacheLife, basePath: nextConfig.basePath, serverActions: nextConfig.experimental.serverActions, + ...(isDebugStaticShell || isDebugDynamicAccesses + ? { + nextExport: true, + supportsDynamicResponse: false, + isStaticGeneration: true, + isRevalidate: true, + isDebugDynamicAccesses: isDebugDynamicAccesses, + } + : {}), + experimental: { isRoutePPREnabled, expireTime: nextConfig.expireTime, @@ -530,14 +534,6 @@ export async function handler( }, } - if (isDebugStaticShell || isDebugDynamicAccesses) { - context.renderOpts.nextExport = true - context.renderOpts.supportsDynamicResponse = false - context.renderOpts.isStaticGeneration = true - context.renderOpts.isRevalidate = true - context.renderOpts.isDebugDynamicAccesses = isDebugDynamicAccesses - } - const result = await invokeRouteModule(span, context) const { metadata } = result @@ -703,42 +699,13 @@ export async function handler( } } } - // Only requests that aren't revalidating can be resumed. If we have the // minimal postponed data, then we should resume the render with it. - let postponed = + const postponed = !isOnDemandRevalidate && !isRevalidating && minimalPostponed ? minimalPostponed : undefined - // If this is a dynamic RSC request, we should use the postponed data from - // the static render (if available). This ensures that we can utilize the - // resume data cache (RDC) from the static render to ensure that the data - // is consistent between the static and dynamic renders. - if ( - process.env.NEXT_RUNTIME !== 'edge' && - !minimalMode && - incrementalCache && - isDynamicRSCRequest - ) { - const cachedEntry = await incrementalCache.get(resolvedPathname, { - kind: IncrementalCacheKind.APP_PAGE, - isRoutePPREnabled: true, - isFallback: false, - allowStale: true, - }) - - // If the cache entry is found, we should use the postponed data from - // the cache. - if ( - cachedEntry && - cachedEntry.value && - cachedEntry.value.kind === CachedRouteKind.APP_PAGE - ) { - postponed = cachedEntry.value.postponed - } - } - // When we're in minimal mode, if we're trying to debug the static shell, // we should just return nothing instead of resuming the dynamic render. if ( @@ -864,7 +831,12 @@ export async function handler( // If this is in minimal mode and this is a flight request that isn't a // prefetch request while PPR is enabled, it cannot be cached as it contains // dynamic content. - else if (isDynamicRSCRequest) { + else if ( + minimalMode && + isRSCRequest && + !isPrefetchRSCRequest && + isRoutePPREnabled + ) { cacheControl = { revalidate: 0, expire: undefined } } else if (!routeModule.isDev) { // If this is a preview mode request, we shouldn't cache it @@ -961,15 +933,34 @@ export async function handler( // If there's a callback for `onCacheEntry`, call it with the cache entry // and the revalidate options. - const onCacheEntry = - getRequestMeta(req, 'onCacheEntryV2') ?? - // TODO: Remove this once we've migrated to `onCacheEntryV2` - getRequestMeta(req, 'onCacheEntry') + const onCacheEntry = getRequestMeta(req, 'onCacheEntry') if (onCacheEntry) { - const finished = await onCacheEntry(cacheEntry, { - url: getRequestMeta(req, 'initURL') ?? req.url, - }) - if (finished) return null + const finished = await onCacheEntry( + { + ...cacheEntry, + // TODO: remove this when upstream doesn't + // always expect this value to be "PAGE" + value: { + ...cacheEntry.value, + kind: 'PAGE', + }, + }, + { + url: getRequestMeta(req, 'initURL'), + } + ) + if (finished) { + // TODO: maybe we have to end the request? + return null + } + } + + // If the request has a postponed state and it's a resume request we + // should error. + if (didPostpone && minimalPostponed) { + throw new Error( + 'Invariant: postponed state should not be present on a resume request' + ) } if (cachedData.headers) { @@ -1042,7 +1033,14 @@ export async function handler( generateEtags: nextConfig.generateEtags, poweredByHeader: nextConfig.poweredByHeader, result: cachedData.html, - cacheControl: cacheEntry.cacheControl, + // Dynamic RSC responses cannot be cached, even if they're + // configured with `force-static` because we have no way of + // distinguishing between `force-static` and pages that have no + // postponed state. + // TODO: distinguish `force-static` from pages with no postponed state (static) + cacheControl: isDynamicRSCRequest + ? { revalidate: 0, expire: undefined } + : cacheEntry.cacheControl, }) } @@ -1060,7 +1058,7 @@ export async function handler( } // This is a request for HTML data. - const body = cachedData.html + let body = cachedData.html // If there's no postponed state, we should just serve the HTML. This // should also be the case for a resume request because it's completed diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 11c3dbd19680..9cc898a553cd 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -1508,9 +1508,7 @@ async function renderToHTMLOrFlightImpl( } else { // We're rendering dynamically const renderResumeDataCache = - renderOpts.renderResumeDataCache ?? - postponedState?.renderResumeDataCache ?? - null + renderOpts.renderResumeDataCache ?? postponedState?.renderResumeDataCache const rootParams = getRootParams(loaderTree, ctx.getDynamicParamFromSegment) const requestStore = createRequestStoreForRender( @@ -1564,9 +1562,6 @@ async function renderToHTMLOrFlightImpl( let formState: null | any = null if (isPossibleActionRequest) { - // For action requests, we don't want to use the resume data cache. - requestStore.renderResumeDataCache = null - // For action requests, we handle them differently with a special render result. const actionRequestResult = await handleAction({ req, @@ -1607,9 +1602,6 @@ async function renderToHTMLOrFlightImpl( } } } - - // Restore the resume data cache - requestStore.renderResumeDataCache = renderResumeDataCache } const options: RenderResultOptions = { diff --git a/packages/next/src/server/async-storage/request-store.ts b/packages/next/src/server/async-storage/request-store.ts index 2334db7fb731..89558398a700 100644 --- a/packages/next/src/server/async-storage/request-store.ts +++ b/packages/next/src/server/async-storage/request-store.ts @@ -114,7 +114,7 @@ export function createRequestStoreForRender( previewProps: WrapperRenderOpts['previewProps'], isHmrRefresh: RequestContext['isHmrRefresh'], serverComponentsHmrCache: RequestContext['serverComponentsHmrCache'], - renderResumeDataCache: RenderResumeDataCache | null + renderResumeDataCache: RenderResumeDataCache | undefined ): RequestStore { return createRequestStoreImpl( // Pages start in render phase by default @@ -148,7 +148,7 @@ export function createRequestStoreForAPI( {}, implicitTags, onUpdateCookies, - null, + undefined, previewProps, false, undefined @@ -163,7 +163,7 @@ function createRequestStoreImpl( rootParams: Params, implicitTags: RequestContext['implicitTags'], onUpdateCookies: RenderOpts['onUpdateCookies'], - renderResumeDataCache: RenderResumeDataCache | null, + renderResumeDataCache: RenderResumeDataCache | undefined, previewProps: WrapperRenderOpts['previewProps'], isHmrRefresh: RequestContext['isHmrRefresh'], serverComponentsHmrCache: RequestContext['serverComponentsHmrCache'] diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index b80d933e43ab..821179b23c0d 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -3034,7 +3034,9 @@ export default abstract class Server< fallbackResponse = await this.responseCache.get( isProduction ? (locale ? `/${locale}${pathname}` : pathname) : null, // This is the response generator for the fallback shell. - ({ previousCacheEntry: previousFallbackCacheEntry = null }) => { + async ({ + previousCacheEntry: previousFallbackCacheEntry = null, + }) => { // For the pages router, fallbacks cannot be revalidated or // generated in production. In the case of a missing fallback, // we return null, but if it's being revalidated, we just return @@ -3075,7 +3077,7 @@ export default abstract class Server< fallbackResponse = await this.responseCache.get( isProduction ? pathname : null, // This is the response generator for the fallback shell. - () => + async () => doRender({ // We pass `undefined` as rendering a fallback isn't resumed // here. @@ -3105,7 +3107,7 @@ export default abstract class Server< if (fallbackResponse) { // Remove the cache control from the response to prevent it from being // used in the surrounding cache. - fallbackResponse.cacheControl = undefined + delete fallbackResponse.cacheControl return fallbackResponse } @@ -3270,9 +3272,15 @@ export default abstract class Server< cacheControl = { revalidate: 0, expire: undefined } } - // If this is a flight request that isn't a pre-fetch request while PPR is - // enabled, it cannot be cached as it contains dynamic content. - else if (isDynamicRSCRequest) { + // If this is in minimal mode and this is a flight request that isn't a + // prefetch request while PPR is enabled, it cannot be cached as it contains + // dynamic content. + else if ( + this.minimalMode && + isRSCRequest && + !isPrefetchRSCRequest && + isRoutePPREnabled + ) { cacheControl = { revalidate: 0, expire: undefined } } else if (!this.renderOpts.dev || (hasServerProps && !isNextDataRequest)) { // If this is a preview mode request, we shouldn't cache it @@ -3382,15 +3390,29 @@ export default abstract class Server< // If there's a callback for `onCacheEntry`, call it with the cache entry // and the revalidate options. - const onCacheEntry = - getRequestMeta(req, 'onCacheEntryV2') ?? - // TODO: Remove this once we've migrated to `onCacheEntryV2` - getRequestMeta(req, 'onCacheEntry') + const onCacheEntry = getRequestMeta(req, 'onCacheEntry') if (onCacheEntry) { - const finished = await onCacheEntry(cacheEntry, { - url: getRequestMeta(req, 'initURL') ?? req.url, - }) - if (finished) return null + const finished = await onCacheEntry( + { + ...cacheEntry, + // TODO: remove this when upstream doesn't + // always expect this value to be "PAGE" + value: { + ...cacheEntry.value, + kind: + cacheEntry.value?.kind === CachedRouteKind.APP_PAGE + ? 'PAGE' + : cacheEntry.value?.kind, + }, + }, + { + url: getRequestMeta(req, 'initURL'), + } + ) + if (finished) { + // TODO: maybe we have to end the request? + return null + } } if (!cachedData) { @@ -3506,7 +3528,7 @@ export default abstract class Server< } // Mark that the request did postpone. - if (didPostpone && !isDynamicRSCRequest) { + if (didPostpone) { res.setHeader(NEXT_DID_POSTPONE_HEADER, '1') } @@ -3524,7 +3546,14 @@ export default abstract class Server< return { type: 'rsc', body: cachedData.html, - cacheControl: cacheEntry.cacheControl, + // Dynamic RSC responses cannot be cached, even if they're + // configured with `force-static` because we have no way of + // distinguishing between `force-static` and pages that have no + // postponed state. + // TODO: distinguish `force-static` from pages with no postponed state (static) + cacheControl: isDynamicRSCRequest + ? { revalidate: 0, expire: undefined } + : cacheEntry.cacheControl, } } @@ -3592,12 +3621,12 @@ export default abstract class Server< }) .then(async (result) => { if (!result) { - throw new InvariantError('expected a result to be returned') + throw new Error('Invariant: expected a result to be returned') } if (result.value?.kind !== CachedRouteKind.APP_PAGE) { - throw new InvariantError( - `expected a page response, got ${result.value?.kind}` + throw new Error( + `Invariant: expected a page response, got ${result.value?.kind}` ) } diff --git a/packages/next/src/server/lib/incremental-cache/file-system-cache.ts b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts index bb5c9b2182e3..51bd58dabbf4 100644 --- a/packages/next/src/server/lib/incremental-cache/file-system-cache.ts +++ b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts @@ -276,16 +276,6 @@ export default class FileSystemCache implements CacheHandler { } } - // If enabled, this will return the possibly stale data without validating - // that the tags have expired or not yet been revalidated. - if ('allowStale' in ctx && ctx.allowStale) { - if (FileSystemCache.debug) { - console.log('allow stale', ctx.allowStale) - } - - return data ?? null - } - if ( data?.value?.kind === CachedRouteKind.APP_PAGE || data?.value?.kind === CachedRouteKind.PAGES @@ -302,10 +292,6 @@ export default class FileSystemCache implements CacheHandler { // had a tag revalidated, if we want to be a background // revalidation instead we return data.lastModified = -1 if (isStale(cacheTags, data?.lastModified || Date.now())) { - if (FileSystemCache.debug) { - console.log('stale tags', cacheTags) - } - return null } } @@ -315,22 +301,17 @@ export default class FileSystemCache implements CacheHandler { ? [...(ctx.tags || []), ...(ctx.softTags || [])] : [] - // When revalidate tag is called we don't return stale data so it's - // updated right away. - if (combinedTags.some((tag) => this.revalidatedTags.includes(tag))) { - if (FileSystemCache.debug) { - console.log('was revalidated', combinedTags) - } - - return null - } - - if (isStale(combinedTags, data?.lastModified || Date.now())) { - if (FileSystemCache.debug) { - console.log('stale tags', combinedTags) + const wasRevalidated = combinedTags.some((tag) => { + if (this.revalidatedTags.includes(tag)) { + return true } - return null + return isStale([tag], data?.lastModified || Date.now()) + }) + // When revalidate tag is called we don't return + // stale data so it's updated right away + if (wasRevalidated) { + data = undefined } } diff --git a/packages/next/src/server/lib/incremental-cache/index.ts b/packages/next/src/server/lib/incremental-cache/index.ts index 088bcc7806fc..2794b4c0bf19 100644 --- a/packages/next/src/server/lib/incremental-cache/index.ts +++ b/packages/next/src/server/lib/incremental-cache/index.ts @@ -15,6 +15,7 @@ import { type SetIncrementalResponseCacheContext, } from '../../response-cache' import type { DeepReadonly } from '../../../shared/lib/deep-readonly' + import FileSystemCache from './file-system-cache' import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path' @@ -88,8 +89,8 @@ export class IncrementalCache implements IncrementalCacheType { readonly allowedRevalidateHeaderKeys?: string[] readonly minimalMode?: boolean readonly fetchCacheKeyPrefix?: string + readonly revalidatedTags?: string[] readonly isOnDemandRevalidate?: boolean - readonly revalidatedTags?: readonly string[] private static readonly debug: boolean = !!process.env.NEXT_PRIVATE_DEBUG_CACHE @@ -178,7 +179,7 @@ export class IncrementalCache implements IncrementalCacheType { } if (minimalMode) { - revalidatedTags = this.revalidatedTags = getPreviouslyRevalidatedTags( + revalidatedTags = getPreviouslyRevalidatedTags( requestHeaders, this.prerenderManifest?.preview?.previewModeId ) @@ -425,13 +426,7 @@ export class IncrementalCache implements IncrementalCacheType { if (resumeDataCache) { const memoryCacheData = resumeDataCache.fetch.get(cacheKey) if (memoryCacheData?.kind === CachedRouteKind.FETCH) { - if (IncrementalCache.debug) { - console.log('rdc:hit', cacheKey) - } - return { isStale: false, value: memoryCacheData } - } else if (IncrementalCache.debug) { - console.log('rdc:miss', cacheKey) } } } @@ -475,34 +470,9 @@ export class IncrementalCache implements IncrementalCacheType { workStore?.pendingRevalidatedTags?.includes(tag) ) ) { - if (IncrementalCache.debug) { - console.log('stale tag', cacheKey) - } - return null } - // As we're able to get the cache entry for this fetch, and the prerender - // resume data cache (RDC) is available, it must have been populated by a - // previous fetch, but was not yet present in the in-memory cache. This - // could be the case when performing multiple renders in parallel during - // build time where we de-duplicate the fetch calls. - // - // We add it to the RDC so that the next fetch call will be able to use it - // and it won't have to reach into the fetch cache implementation. - const workUnitStore = workUnitAsyncStorage.getStore() - if (workUnitStore) { - const prerenderResumeDataCache = - getPrerenderResumeDataCache(workUnitStore) - if (prerenderResumeDataCache) { - if (IncrementalCache.debug) { - console.log('rdc:set', cacheKey) - } - - prerenderResumeDataCache.fetch.set(cacheKey, cacheData.value) - } - } - const revalidate = ctx.revalidate || cacheData.value.revalidate const age = (performance.timeOrigin + @@ -601,10 +571,6 @@ export class IncrementalCache implements IncrementalCacheType { ? getPrerenderResumeDataCache(workUnitStore) : null if (prerenderResumeDataCache) { - if (IncrementalCache.debug) { - console.log('rdc:set', pathname) - } - prerenderResumeDataCache.fetch.set(pathname, data) } } diff --git a/packages/next/src/server/request-meta.ts b/packages/next/src/server/request-meta.ts index f39d20b47cf8..28a8b3be8a99 100644 --- a/packages/next/src/server/request-meta.ts +++ b/packages/next/src/server/request-meta.ts @@ -6,12 +6,8 @@ import type { BaseNextRequest } from './base-http' import type { CloneableBody } from './body-streams' import type { RouteMatch } from './route-matches/route-match' import type { NEXT_RSC_UNION_QUERY } from '../client/components/app-router-headers' -import type { - ResponseCacheEntry, - ServerComponentsHmrCache, -} from './response-cache' +import type { ServerComponentsHmrCache } from './response-cache' import type { PagesDevOverlayBridgeType } from '../next-devtools/userspace/pages/pages-dev-overlay-setup' -import type { IncrementalCache } from './lib/incremental-cache' // FIXME: (wyattjoh) this is a temporary solution to allow us to pass data between bundled modules export const NEXT_REQUEST_META = Symbol.for('NextInternalRequestMeta') @@ -72,7 +68,7 @@ export interface RequestMeta { /** * The incremental cache to use for the request. */ - incrementalCache?: IncrementalCache + incrementalCache?: any /** * The server components HMR cache, only for dev. @@ -122,25 +118,10 @@ export interface RequestMeta { /** * If provided, this will be called when a response cache entry was generated * or looked up in the cache. - * - * @deprecated Use `onCacheEntryV2` instead. */ onCacheEntry?: ( - cacheEntry: ResponseCacheEntry, - requestMeta: { - url: string | undefined - } - ) => Promise | boolean | void - - /** - * If provided, this will be called when a response cache entry was generated - * or looked up in the cache. - */ - onCacheEntryV2?: ( - cacheEntry: ResponseCacheEntry, - requestMeta: { - url: string | undefined - } + cacheEntry: any, + requestMeta: any ) => Promise | boolean | void /** diff --git a/packages/next/src/server/response-cache/types.ts b/packages/next/src/server/response-cache/types.ts index eabefeb9a45e..04b9daa8535e 100644 --- a/packages/next/src/server/response-cache/types.ts +++ b/packages/next/src/server/response-cache/types.ts @@ -219,11 +219,6 @@ export interface GetIncrementalResponseCacheContext { * True if this is a fallback request. */ isFallback: boolean - - /** - * True if stale data is allowed to be returned. - */ - allowStale?: boolean } export interface SetIncrementalFetchCacheContext { diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index 14e1de493719..137b025028f7 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -2570,7 +2570,6 @@ const runTests = (isDev = false) => { prefetchSegmentSuffix: '.segment.rsc', prefetchSuffix: '.prefetch.rsc', suffix: '.rsc', - dynamicRSCPrerender: !!process.env.__NEXT_EXPERIMENTAL_PPR, }, }) }) diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index 11b4929f456d..b6be113ce580 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -1529,7 +1529,6 @@ function runTests({ dev }) { prefetchSegmentSuffix: '.segment.rsc', prefetchSuffix: '.prefetch.rsc', suffix: '.rsc', - dynamicRSCPrerender: !!process.env.__NEXT_EXPERIMENTAL_PPR, }, }) }) diff --git a/test/ppr-tests-manifest.json b/test/ppr-tests-manifest.json index 4bcad8af0589..32d34ee70e60 100644 --- a/test/ppr-tests-manifest.json +++ b/test/ppr-tests-manifest.json @@ -174,8 +174,7 @@ "test/e2e/app-dir/use-cache-route-handler-only/**/*", "test/integration/app-dir-export/**/*", "test/production/app-dir/build-output-tree-view/build-output-tree-view.test.ts", - "test/production/app-dir/global-default-cache-handler/global-default-cache-handler.test.ts", - "test/production/app-dir/resume-data-cache/resume-data-cache.test.ts" + "test/production/app-dir/global-default-cache-handler/global-default-cache-handler.test.ts" ] } } diff --git a/test/production/app-dir/resume-data-cache/app/layout.tsx b/test/production/app-dir/resume-data-cache/app/layout.tsx deleted file mode 100644 index 9dfde1627512..000000000000 --- a/test/production/app-dir/resume-data-cache/app/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { ReactNode } from 'react' - -export default function Root({ children }: { children: ReactNode }) { - return ( - - {children} - - ) -} diff --git a/test/production/app-dir/resume-data-cache/app/page.tsx b/test/production/app-dir/resume-data-cache/app/page.tsx deleted file mode 100644 index a42bd6f524a3..000000000000 --- a/test/production/app-dir/resume-data-cache/app/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { Suspense } from 'react' -import { connection } from 'next/server' - -import { unstable_cacheTag } from 'next/cache' - -async function getRandomNumber() { - 'use cache' - unstable_cacheTag('test') - return Math.random() -} - -async function DynamicComponent() { - await connection() - return null -} - -export default async function Page() { - const randomNumber = await getRandomNumber() - const anotherRandomNumber = await fetch( - 'https://next-data-api-endpoint.vercel.app/api/random', - { cache: 'force-cache', next: { tags: ['test'] } } - ).then((res) => res.text()) - return ( - <> -

{randomNumber}

-

{anotherRandomNumber}

- - - - - ) -} diff --git a/test/production/app-dir/resume-data-cache/app/revalidate/route.ts b/test/production/app-dir/resume-data-cache/app/revalidate/route.ts deleted file mode 100644 index 4100bb53cdd0..000000000000 --- a/test/production/app-dir/resume-data-cache/app/revalidate/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { revalidateTag } from 'next/cache' - -export function POST() { - revalidateTag('test') - return new Response(null, { status: 200 }) -} diff --git a/test/production/app-dir/resume-data-cache/next.config.js b/test/production/app-dir/resume-data-cache/next.config.js deleted file mode 100644 index bea0706290af..000000000000 --- a/test/production/app-dir/resume-data-cache/next.config.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @type {import('next').NextConfig} - */ -const nextConfig = { - experimental: { - ppr: true, - useCache: true, - }, -} - -module.exports = nextConfig diff --git a/test/production/app-dir/resume-data-cache/resume-data-cache.test.ts b/test/production/app-dir/resume-data-cache/resume-data-cache.test.ts deleted file mode 100644 index b7c3a0ec7f71..000000000000 --- a/test/production/app-dir/resume-data-cache/resume-data-cache.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { nextTestSetup } from 'e2e-utils' -import { retry } from 'next-test-utils' - -describe('resume-data-cache', () => { - const { next, skipped } = nextTestSetup({ - files: __dirname, - // TODO: re-enable once support has been added - skipDeployment: true, - }) - if (skipped) return - - it.each([ - { name: 'use cache', id: 'random-number' }, - { name: 'fetch cache', id: 'another-random-number' }, - ])( - 'should have consistent data between static and dynamic renders with $name', - async ({ id }) => { - // First render the page statically, getting the random number from the - // HTML. - let $ = await next.render$('/') - const first = $(`p#${id}`).text() - - // Then get the Prefetch RSC and validate that it also contains the same - // random number. - let rsc - - await retry(async () => { - rsc = await next - .fetch('/', { - headers: { - RSC: '1', - 'Next-Router-Prefetch': '1', - }, - }) - .then((res) => res.text()) - expect(rsc).toContain(first) - }) - - // Then get the dynamic RSC and validate that it also contains the same - // random number. - await retry(async () => { - rsc = await next - .fetch('/', { - headers: { - RSC: '1', - }, - }) - .then((res) => res.text()) - expect(rsc).toContain(first) - }) - - // Then revalidate the page - await next.fetch('/revalidate', { method: 'POST' }) - - // Then get the dynamic RSC again and validate that it still contains the - // same random number. - await retry(async () => { - rsc = await next - .fetch('/', { - headers: { - RSC: '1', - }, - }) - .then((res) => res.text()) - expect(rsc).toContain(first) - }) - - // This proves that the dynamic RSC was able to use the resume data cache - // (RDC) from the static render to ensure that the data is consistent - // between the static and dynamic renders. Let's now try to render the - // page statically and see that the random number changes. - - $ = await next.render$('/') - const random2 = $(`p#${id}`).text() - expect(random2).not.toBe(first) - - // Then get the Prefetch RSC and validate that it also contains the new - // random number. - await retry(async () => { - rsc = await next - .fetch('/', { - headers: { - RSC: '1', - 'Next-Router-Prefetch': '1', - }, - }) - .then((res) => res.text()) - expect(rsc).toContain(random2) - }) - - // Then get the dynamic RSC again and validate that it also contains the - // new random number. - await retry(async () => { - rsc = await next - .fetch('/', { - headers: { - RSC: '1', - }, - }) - .then((res) => res.text()) - expect(rsc).toContain(random2) - }) - - // This proves that the dynamic RSC was able to use the resume data cache - // (RDC) from the static render to ensure that the data is consistent - // between the static and dynamic renders. - } - ) -}) diff --git a/test/production/standalone-mode/required-server-files/app/dyn/[slug]/page.js b/test/production/standalone-mode/required-server-files/app/dyn/[slug]/page.js index 051ec6f7a056..dc35c2f7d7d9 100644 --- a/test/production/standalone-mode/required-server-files/app/dyn/[slug]/page.js +++ b/test/production/standalone-mode/required-server-files/app/dyn/[slug]/page.js @@ -1,7 +1,7 @@ import { headers } from 'next/headers' export default async function Page({ params }) { - const data = await headers() + const data = headers() return ( <> diff --git a/test/production/standalone-mode/required-server-files/app/rewrite/[slug]/page.js b/test/production/standalone-mode/required-server-files/app/rewrite/[slug]/page.js index 729ce481ad76..8267d9a68933 100644 --- a/test/production/standalone-mode/required-server-files/app/rewrite/[slug]/page.js +++ b/test/production/standalone-mode/required-server-files/app/rewrite/[slug]/page.js @@ -1,24 +1,18 @@ import { Suspense } from 'react' -import { connection } from 'next/server' +import { unstable_noStore } from 'next/cache' export function generateStaticParams() { return [{ slug: 'first-cookie' }] } -async function Postpone({ children }) { - await connection() +function Postpone({ children }) { + unstable_noStore() return children } export default async function Page({ params }) { - const random = await fetch( - 'https://next-data-api-endpoint.vercel.app/api/random', - { cache: 'force-cache' } - ).then((res) => res.text()) - return ( <> -

{random}

/rewrite/[slug]

diff --git a/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts b/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts index 3834ec4818ca..07f6636fdd73 100644 --- a/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts +++ b/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts @@ -1,5 +1,6 @@ -import fs from 'node:fs/promises' -import { join } from 'node:path' +import glob from 'glob' +import fs from 'fs-extra' +import { join } from 'path' import cheerio from 'cheerio' import { createNext, FileRef } from 'e2e-utils' import { NextInstance } from 'e2e-utils' @@ -10,22 +11,27 @@ import { initNextServerScript, killApp, } from 'next-test-utils' -import { ChildProcess } from 'node:child_process' +import { ChildProcess } from 'child_process' describe('required server files app router', () => { let next: NextInstance let server: ChildProcess let appPort: number | string - let delayedPostpone: string - let rewritePostpone: string - let rewriteHTML: string + let delayedPostpone + let rewritePostpone let cliOutput = '' - beforeAll(async () => { - process.env.NOW_BUILDER = '1' + const setupNext = async ({ + nextEnv, + minimalMode, + }: { + nextEnv?: boolean + minimalMode?: boolean + }) => { + // test build against environment with next support + process.env.NOW_BUILDER = nextEnv ? '1' : '' process.env.NEXT_PRIVATE_TEST_HEADERS = '1' - // Setup the Next.js app and build it. next = await createNext({ files: { app: new FileRef(join(__dirname, 'app')), @@ -41,7 +47,6 @@ describe('required server files app router', () => { cacheHandler: './cache-handler.js', experimental: { ppr: true, - clientSegmentCache: true, }, eslint: { ignoreDuringBuilds: true, @@ -49,49 +54,50 @@ describe('required server files app router', () => { output: 'standalone', }, }) - - // Stop the server, we're going to restart it using the standalone server - // below after some cleanup. await next.stop() - // Read the postponed state and the HTML that was generated at build time - // from the output of the build. delayedPostpone = (await next.readJSON('.next/server/app/delayed.meta')) .postponed rewritePostpone = ( await next.readJSON('.next/server/app/rewrite/first-cookie.meta') ).postponed - rewriteHTML = await next.readFile( - '.next/server/app/rewrite/first-cookie.html' - ) - await fs.rename( + await fs.move( join(next.testDir, '.next/standalone'), join(next.testDir, 'standalone') ) + for (const file of await fs.readdir(next.testDir)) { + if (file !== 'standalone') { + await fs.remove(join(next.testDir, file)) + console.log('removed', file) + } + } + const files = glob.sync('**/*', { + cwd: join(next.testDir, 'standalone/.next/server/pages'), + dot: true, + }) - const serverFilePath = join(next.testDir, 'standalone/server.js') + for (const file of files) { + if (file.endsWith('.json') || file.endsWith('.html')) { + await fs.remove(join(next.testDir, '.next/server', file)) + } + } - // We're going to use the minimal mode for the server. + const testServer = join(next.testDir, 'standalone/server.js') await fs.writeFile( - serverFilePath, - (await fs.readFile(serverFilePath, 'utf8')).replace( + testServer, + (await fs.readFile(testServer, 'utf8')).replace( 'port:', - `minimalMode: true, port:` + `minimalMode: ${minimalMode},port:` ) ) - - // Find a port to use for the server. appPort = await findPort() - - // Then we can start the server with the new environment variables. server = await initNextServerScript( - serverFilePath, + testServer, /- Local:/, { ...process.env, PORT: `${appPort}`, - NEXT_PRIVATE_DEBUG_CACHE: '1', }, undefined, { @@ -104,8 +110,11 @@ describe('required server files app router', () => { }, } ) - }) + } + beforeAll(async () => { + await setupNext({ nextEnv: true, minimalMode: true }) + }) afterAll(async () => { delete process.env.NEXT_PRIVATE_TEST_HEADERS await next.destroy() @@ -116,35 +125,37 @@ describe('required server files app router', () => { expect(next.cliOutput).not.toContain('ERR_INVALID_URL') }) - it('should de-dupe client segment tree revalidate requests', async () => { - const { segmentPaths } = await next.readJSON( - 'standalone/.next/server/app/isr/first.meta' - ) - const outputIdx = cliOutput.length + // this enables client segment cache in CI + if (process.env.__NEXT_EXPERIMENTAL_PPR) { + it('should de-dupe client segment tree revalidate requests', async () => { + const { segmentPaths } = await next.readJSON( + 'standalone/.next/server/app/isr/first.meta' + ) + const outputIdx = cliOutput.length - for (const segmentPath of segmentPaths) { - const outputSegmentPath = - join('/isr/[slug].segments', segmentPath) + '.segment.rsc' + for (const segmentPath of segmentPaths) { + const outputSegmentPath = + join('/isr/[slug].segments', segmentPath) + '.segment.rsc' - require('console').error('requesting', outputSegmentPath) + require('console').error('requesting', outputSegmentPath) - const res = await fetchViaHTTP(appPort, outputSegmentPath, undefined, { - headers: { - 'x-matched-path': '/isr/[slug].segments/_tree.segment.rsc', - 'x-now-route-matches': createNowRouteMatches({ - slug: 'first', - }).toString(), - }, - }) + const res = await fetchViaHTTP(appPort, outputSegmentPath, undefined, { + headers: { + 'x-matched-path': '/isr/[slug].segments/_tree.segment.rsc', + 'x-now-route-matches': 'slug=first&1=first', + }, + }) - expect(res.status).toBe(200) - expect(res.headers.get('content-type')).toBe('text/x-component') - } + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toBe('text/x-component') + } - expect( - cliOutput.substring(outputIdx).match(/rendering \/isr\/\[slug\]/g).length - ).toBe(1) - }) + expect( + cliOutput.substring(outputIdx).match(/rendering \/isr\/\[slug\]/g) + .length + ).toBe(1) + }) + } it('should properly stream resume with Next-Resume', async () => { const res = await fetchViaHTTP(appPort, '/delayed', undefined, { @@ -170,6 +181,11 @@ describe('required server files app router', () => { const firstSuspense = chunks.find((item) => item.chunk.includes('time')) const secondSuspense = chunks.find((item) => item.chunk.includes('random')) + console.log({ + firstSuspense, + secondSuspense, + }) + expect(secondSuspense.time - firstSuspense.time).toBeGreaterThanOrEqual( 2 * 1000 ) @@ -347,15 +363,12 @@ describe('required server files app router', () => { const res = await fetchViaHTTP(appPort, '/dyn/first.rsc', undefined, { headers: { 'x-matched-path': '/dyn/[slug]', - 'x-now-route-matches': createNowRouteMatches({ - slug: 'first', - }).toString(), }, }) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toEqual('text/x-component') - expect(res.headers.has('x-nextjs-postponed')).toBeTrue() + expect(res.headers.has('x-nextjs-postponed')).toBeFalse() }) it('should handle prefetch RSC requests', async () => { @@ -366,9 +379,6 @@ describe('required server files app router', () => { { headers: { 'x-matched-path': '/dyn/[slug]', - 'x-now-route-matches': createNowRouteMatches({ - slug: 'first', - }).toString(), }, } ) @@ -378,84 +388,6 @@ describe('required server files app router', () => { expect(res.headers.has('x-nextjs-postponed')).toBeTrue() }) - it('should use the postponed state for the RSC requests', async () => { - // Let's parse the random number out of the HTML that was generated at build - // time. We want to use that value as it's the one that's tied to the - // postponed state that we also have. - const $ = cheerio.load(rewriteHTML) - - const random = $('#random').text() - expect(random).toBeDefined() - expect(random.length).toBeGreaterThan(0) - - // Record the start of the logs for this test. - let start = cliOutput.length - - // Then let's do a Dynamic RSC request and verify that the random value is - // not present in the response without passing the postponed state. - let res = await fetchViaHTTP( - appPort, - '/rewrite/first-cookie.rsc', - undefined, - { - headers: { - 'x-matched-path': '/rewrite/[slug]', - 'x-now-route-matches': createNowRouteMatches({ - slug: 'first-cookie', - }).toString(), - }, - } - ) - - expect(res.status).toBe(200) - expect(res.headers.get('content-type')).toEqual('text/x-component') - expect(res.headers.has('x-nextjs-postponed')).toBeTrue() - - // Ensure that we hit the cache handler and not the resume data cache. - expect(cliOutput.substring(start)).toContain('cache-handler get') - expect(cliOutput.substring(start)).toContain('cache-handler set') - expect(cliOutput.substring(start)).toContain('rdc:miss') - expect(cliOutput.substring(start)).not.toContain('rdc:hit') - - // We expect that the random value is not present in the response because - // we're not providing a resume data cache via the postponed state. - // Instead it'll contain another random number that's been generated at - // runtime. - let rsc = await res.text() - expect(rsc).not.toContain(random) - - // Reset the start of the logs for this test. - start = cliOutput.length - - // Then let's get the Dynamic RSC request and verify that the random value - // is present in the response by passing the postponed state. - res = await fetchViaHTTP(appPort, '/rewrite/first-cookie.rsc', undefined, { - method: 'POST', - headers: { - 'x-matched-path': '/rewrite/[slug]', - 'x-now-route-matches': createNowRouteMatches({ - slug: 'first-cookie', - }).toString(), - 'next-resume': '1', - }, - body: rewritePostpone, - }) - - expect(res.status).toBe(200) - expect(res.headers.get('content-type')).toEqual('text/x-component') - expect(res.headers.has('x-nextjs-postponed')).toBeFalse() - - // Ensure that we hit the resume data cache and not the cache handler. - expect(cliOutput.substring(start)).not.toContain('cache-handler get') - expect(cliOutput.substring(start)).not.toContain('cache-handler set') - expect(cliOutput.substring(start)).toContain('rdc:hit') - - // We expect that the random value is present in the response because - // we're providing a resume data cache via the postponed state. - rsc = await res.text() - expect(rsc).toContain(random) - }) - it('should handle revalidating the fallback page', async () => { const res = await fetchViaHTTP(appPort, '/postpone/isr/[slug]', undefined, { headers: {