diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 694026f423755b..108569a6dd1802 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -171,10 +171,7 @@ import './clean-async-snapshot.external' import { INFINITE_CACHE } from '../../lib/constants' import { createComponentStylesAndScripts } from './create-component-styles-and-scripts' import { parseLoaderTree } from './parse-loader-tree' -import { - createMutableResumeDataCache, - sealResumeDataCache, -} from '../resume-data-cache/resume-data-cache' +import { createPrerenderResumeDataCache } from '../resume-data-cache/resume-data-cache' export type GetDynamicParamFromSegment = ( // [slug] / [[slug]] / [...slug] @@ -615,12 +612,13 @@ async function warmupDevRender( // We're doing a dev warmup, so we should create a new resume data cache so // we can fill it. - const mutableResumeDataCache = createMutableResumeDataCache() + const devWarmupPrerenderResumeDataCache = createPrerenderResumeDataCache() // Attach this to the request store so that it can be used during the // render. const { requestStore } = ctx - requestStore.mutableResumeDataCache = mutableResumeDataCache + requestStore.devWarmupPrerenderResumeDataCache = + devWarmupPrerenderResumeDataCache const rscPayload = await workUnitAsyncStorage.run( requestStore, @@ -650,14 +648,16 @@ async function warmupDevRender( } } + // As we're finished rendering, remove the reference to the prerender resume + // data cache so it can't be written to again. + requestStore.devWarmupPrerenderResumeDataCache = null + // We don't really want to return a result here but the stack of functions // that calls into renderToHTML... expects a result. We should refactor this to // lift the warmup pathway outside of renderToHTML... but for now this suffices return new FlightRenderResult('', { fetchMetrics: ctx.workStore.fetchMetrics, - devWarmupImmutableResumeDataCache: sealResumeDataCache( - mutableResumeDataCache - ), + devWarmupPrerenderResumeDataCache, }) } @@ -1433,17 +1433,17 @@ export const renderToHTMLOrFlight: AppPageRender = ( } if ( - postponedState?.immutableResumeDataCache && - renderOpts.devWarmupImmutableResumeDataCache + postponedState?.renderResumeDataCache && + renderOpts.devWarmupRenderResumeDataCache ) { throw new InvariantError( 'postponed state and dev warmup immutable resume data cache should not be provided together' ) } - const immutableResumeDataCache = - renderOpts.devWarmupImmutableResumeDataCache ?? - postponedState?.immutableResumeDataCache + const renderResumeDataCache = + renderOpts.devWarmupRenderResumeDataCache ?? + postponedState?.renderResumeDataCache const implicitTags = getImplicitTags( renderOpts.routeModule.definition.page, @@ -1461,7 +1461,7 @@ export const renderToHTMLOrFlight: AppPageRender = ( url, implicitTags, renderOpts.onUpdateCookies, - immutableResumeDataCache, + renderResumeDataCache, renderOpts.previewProps, isHmrRefresh, serverComponentsHmrCache @@ -1984,7 +1984,7 @@ async function spawnDynamicValidationInDev( const initialServerRenderController = new AbortController() const cacheSignal = new CacheSignal() - const mutableResumeDataCache = createMutableResumeDataCache() + const prerenderResumeDataCache = createPrerenderResumeDataCache() const initialServerPrerenderStore: PrerenderStore = { type: 'prerender', phase: 'render', @@ -1997,7 +1997,7 @@ async function spawnDynamicValidationInDev( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, } const initialClientController = new AbortController() @@ -2013,7 +2013,7 @@ async function spawnDynamicValidationInDev( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, } // We're not going to use the result of this render because the only time it could be used @@ -2145,7 +2145,7 @@ async function spawnDynamicValidationInDev( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, } const finalClientController = new AbortController() @@ -2165,7 +2165,7 @@ async function spawnDynamicValidationInDev( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, } const finalServerPayload = await workUnitAsyncStorage.run( @@ -2437,7 +2437,7 @@ async function prerenderToStream( // performing a fresh prerender. If we get to implementing the // prerendering of an already prerendered page, we should use the passed // resume data cache instead. - const mutableResumeDataCache = createMutableResumeDataCache() + const prerenderResumeDataCache = createPrerenderResumeDataCache() const initialServerPrerenderStore: PrerenderStore = (prerenderStore = { type: 'prerender', @@ -2451,7 +2451,7 @@ async function prerenderToStream( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, }) // We're not going to use the result of this render because the only time it could be used @@ -2538,7 +2538,7 @@ async function prerenderToStream( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, } const prerender = require('react-dom/static.edge') @@ -2618,7 +2618,7 @@ async function prerenderToStream( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, }) const finalAttemptRSCPayload = await workUnitAsyncStorage.run( @@ -2675,7 +2675,7 @@ async function prerenderToStream( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, } let clientIsDynamic = false @@ -2769,12 +2769,12 @@ async function prerenderToStream( metadata.postponed = await getDynamicHTMLPostponedState( postponed, fallbackRouteParams, - sealResumeDataCache(mutableResumeDataCache) + prerenderResumeDataCache ) } else { // Dynamic Data case metadata.postponed = await getDynamicDataPostponedState( - sealResumeDataCache(mutableResumeDataCache) + prerenderResumeDataCache ) } reactServerResult.consume() @@ -2893,7 +2893,7 @@ async function prerenderToStream( const initialServerRenderController = new AbortController() const cacheSignal = new CacheSignal() - const mutableResumeDataCache = createMutableResumeDataCache() + const prerenderResumeDataCache = createPrerenderResumeDataCache() const initialServerPrerenderStore: PrerenderStore = (prerenderStore = { type: 'prerender', @@ -2907,7 +2907,7 @@ async function prerenderToStream( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, }) const initialClientController = new AbortController() @@ -2923,7 +2923,7 @@ async function prerenderToStream( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, }) // We're not going to use the result of this render because the only time it could be used @@ -3066,7 +3066,7 @@ async function prerenderToStream( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, }) let clientIsDynamic = false @@ -3089,7 +3089,7 @@ async function prerenderToStream( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, }) const finalServerPayload = await workUnitAsyncStorage.run( @@ -3268,7 +3268,7 @@ async function prerenderToStream( renderOpts.isDebugDynamicAccesses ) - const mutableResumeDataCache = createMutableResumeDataCache() + const prerenderResumeDataCache = createPrerenderResumeDataCache() const reactServerPrerenderStore: PrerenderStore = (prerenderStore = { type: 'prerender-ppr', phase: 'render', @@ -3278,7 +3278,7 @@ async function prerenderToStream( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, }) const RSCPayload = await workUnitAsyncStorage.run( reactServerPrerenderStore, @@ -3310,7 +3310,7 @@ async function prerenderToStream( expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...ctx.requestStore.implicitTags], - mutableResumeDataCache, + prerenderResumeDataCache, } const prerender = require('react-dom/static.edge') .prerender as (typeof import('react-dom/static.edge'))['prerender'] @@ -3383,12 +3383,12 @@ async function prerenderToStream( metadata.postponed = await getDynamicHTMLPostponedState( postponed, fallbackRouteParams, - sealResumeDataCache(mutableResumeDataCache) + prerenderResumeDataCache ) } else { // Dynamic Data case. metadata.postponed = await getDynamicDataPostponedState( - sealResumeDataCache(mutableResumeDataCache) + prerenderResumeDataCache ) } // Regardless of whether this is the Dynamic HTML or Dynamic Data case we need to ensure we include @@ -3412,7 +3412,7 @@ async function prerenderToStream( } else if (fallbackRouteParams && fallbackRouteParams.size > 0) { // Rendering the fallback case. metadata.postponed = await getDynamicDataPostponedState( - sealResumeDataCache(mutableResumeDataCache) + prerenderResumeDataCache ) return { diff --git a/packages/next/src/server/app-render/postponed-state.test.ts b/packages/next/src/server/app-render/postponed-state.test.ts index 070ed8c2ba644c..dbc03f17760b39 100644 --- a/packages/next/src/server/app-render/postponed-state.test.ts +++ b/packages/next/src/server/app-render/postponed-state.test.ts @@ -1,7 +1,4 @@ -import { - createMutableResumeDataCache, - sealResumeDataCache, -} from '../resume-data-cache/resume-data-cache' +import { createPrerenderResumeDataCache } from '../resume-data-cache/resume-data-cache' import { streamFromString } from '../stream-utils/node-web-streams-helper' import { DynamicState, @@ -14,9 +11,9 @@ describe('getDynamicHTMLPostponedState', () => { it('serializes a HTML postponed state with fallback params', async () => { const key = '%%drp:slug:e9615126684e5%%' const fallbackRouteParams = new Map([['slug', key]]) - const mutableResumeDataCache = createMutableResumeDataCache() + const prerenderResumeDataCache = createPrerenderResumeDataCache() - mutableResumeDataCache.cache.set( + prerenderResumeDataCache.cache.set( '1', Promise.resolve({ value: streamFromString('hello'), @@ -31,7 +28,7 @@ describe('getDynamicHTMLPostponedState', () => { const state = await getDynamicHTMLPostponedState( { [key]: key, nested: { [key]: key } }, fallbackRouteParams, - sealResumeDataCache(mutableResumeDataCache) + prerenderResumeDataCache ) expect(state).toMatchInlineSnapshot( @@ -43,7 +40,7 @@ describe('getDynamicHTMLPostponedState', () => { const state = await getDynamicHTMLPostponedState( { key: 'value' }, null, - sealResumeDataCache(createMutableResumeDataCache()) + createPrerenderResumeDataCache() ) expect(state).toMatchInlineSnapshot(`"15:{"key":"value"}null"`) }) @@ -54,7 +51,7 @@ describe('getDynamicHTMLPostponedState', () => { const state = await getDynamicHTMLPostponedState( { [key]: key }, fallbackRouteParams, - sealResumeDataCache(createMutableResumeDataCache()) + createPrerenderResumeDataCache() ) const value = 'hello' @@ -63,9 +60,7 @@ describe('getDynamicHTMLPostponedState', () => { expect(parsed).toEqual({ type: DynamicState.HTML, data: { [value]: value }, - immutableResumeDataCache: sealResumeDataCache( - createMutableResumeDataCache() - ), + renderResumeDataCache: createPrerenderResumeDataCache(), }) // The replacements have been replaced. @@ -76,7 +71,7 @@ describe('getDynamicHTMLPostponedState', () => { describe('getDynamicDataPostponedState', () => { it('serializes a data postponed state with fallback params', async () => { const state = await getDynamicDataPostponedState( - sealResumeDataCache(createMutableResumeDataCache()) + createPrerenderResumeDataCache() ) expect(state).toMatchInlineSnapshot(`"4:nullnull"`) }) @@ -94,9 +89,7 @@ describe('parsePostponedState', () => { expect(parsed).toEqual({ type: DynamicState.HTML, data: expect.any(Object), - immutableResumeDataCache: sealResumeDataCache( - createMutableResumeDataCache() - ), + renderResumeDataCache: createPrerenderResumeDataCache(), }) // Ensure that the replacement worked and removed all the placeholders. @@ -112,9 +105,7 @@ describe('parsePostponedState', () => { expect(parsed).toEqual({ type: DynamicState.HTML, data: expect.any(Object), - immutableResumeDataCache: sealResumeDataCache( - createMutableResumeDataCache() - ), + renderResumeDataCache: createPrerenderResumeDataCache(), }) }) @@ -125,9 +116,7 @@ describe('parsePostponedState', () => { // Ensure that it parsed it correctly. expect(parsed).toEqual({ type: DynamicState.DATA, - immutableResumeDataCache: sealResumeDataCache( - createMutableResumeDataCache() - ), + renderResumeDataCache: createPrerenderResumeDataCache(), }) }) }) diff --git a/packages/next/src/server/app-render/postponed-state.ts b/packages/next/src/server/app-render/postponed-state.ts index 980d8ac0e89ebe..978a675093fa4e 100644 --- a/packages/next/src/server/app-render/postponed-state.ts +++ b/packages/next/src/server/app-render/postponed-state.ts @@ -1,6 +1,6 @@ import type { FallbackRouteParams } from '../../server/request/fallback-params' import type { Params } from '../request/params' -import type { ImmutableResumeDataCache } from '../resume-data-cache/resume-data-cache' +import type { RenderResumeDataCache } from '../resume-data-cache/resume-data-cache' import { parseResumeDataCache, stringifyResumeDataCache, @@ -30,7 +30,7 @@ export type DynamicDataPostponedState = { /** * The immutable resume data cache. */ - readonly immutableResumeDataCache: ImmutableResumeDataCache + readonly renderResumeDataCache: RenderResumeDataCache } /** @@ -50,7 +50,7 @@ export type DynamicHTMLPostponedState = { /** * The immutable resume data cache. */ - readonly immutableResumeDataCache: ImmutableResumeDataCache + readonly renderResumeDataCache: RenderResumeDataCache } export type PostponedState = @@ -60,14 +60,14 @@ export type PostponedState = export async function getDynamicHTMLPostponedState( data: object, fallbackRouteParams: FallbackRouteParams | null, - immutableResumeDataCache: ImmutableResumeDataCache + renderResumeDataCache: RenderResumeDataCache ): Promise { if (!fallbackRouteParams || fallbackRouteParams.size === 0) { const postponedString = JSON.stringify(data) - // Serialized as `:` + // Serialized as `:` return `${postponedString.length}:${postponedString}${await stringifyResumeDataCache( - immutableResumeDataCache + renderResumeDataCache )}` } @@ -78,14 +78,14 @@ export async function getDynamicHTMLPostponedState( // Serialized as `` const postponedString = `${replacementsString.length}${replacementsString}${dataString}` - // Serialized as `:` - return `${postponedString.length}:${postponedString}${await stringifyResumeDataCache(immutableResumeDataCache)}` + // Serialized as `:` + return `${postponedString.length}:${postponedString}${await stringifyResumeDataCache(renderResumeDataCache)}` } export async function getDynamicDataPostponedState( - immutableResumeDataCache: ImmutableResumeDataCache + renderResumeDataCache: RenderResumeDataCache ): Promise { - return `4:null${await stringifyResumeDataCache(immutableResumeDataCache)}` + return `4:null${await stringifyResumeDataCache(renderResumeDataCache)}` } export function parsePostponedState( @@ -106,13 +106,13 @@ export function parsePostponedState( postponedStringLengthMatch.length + postponedStringLength + 1 ) - const immutableResumeDataCache = parseResumeDataCache( + const renderResumeDataCache = parseResumeDataCache( state.slice(postponedStringLengthMatch.length + postponedStringLength + 1) ) try { if (postponedString === 'null') { - return { type: DynamicState.DATA, immutableResumeDataCache } + return { type: DynamicState.DATA, renderResumeDataCache } } if (/^[0-9]/.test(postponedString)) { @@ -143,18 +143,18 @@ export function parsePostponedState( return { type: DynamicState.HTML, data: JSON.parse(postponed), - immutableResumeDataCache, + renderResumeDataCache, } } return { type: DynamicState.HTML, data: JSON.parse(postponedString), - immutableResumeDataCache, + renderResumeDataCache, } } catch (err) { console.error('Failed to parse postponed state', err) - return { type: DynamicState.DATA, immutableResumeDataCache } + return { type: DynamicState.DATA, renderResumeDataCache } } } diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 136bce4d8ccca2..19c904ba167934 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -16,7 +16,7 @@ import type { InstrumentationOnRequestError } from '../instrumentation/types' import type { NextRequestHint } from '../web/adapter' import type { BaseNextRequest } from '../base-http' import type { IncomingMessage } from 'http' -import type { ImmutableResumeDataCache } from '../resume-data-cache/resume-data-cache' +import type { RenderResumeDataCache } from '../resume-data-cache/resume-data-cache' export type DynamicParamTypes = | 'catchall' @@ -187,9 +187,9 @@ export interface RenderOptsPartial { /** * The resume data cache that was generated for this partially prerendered - * page or during rendering. + * page during dev warmup. */ - devWarmupImmutableResumeDataCache?: ImmutableResumeDataCache + devWarmupRenderResumeDataCache?: RenderResumeDataCache /** * When true, only the static shell of the page will be rendered. This will diff --git a/packages/next/src/server/app-render/work-unit-async-storage.external.ts b/packages/next/src/server/app-render/work-unit-async-storage.external.ts index ac249a94310f02..7b9c6cac2ba5c9 100644 --- a/packages/next/src/server/app-render/work-unit-async-storage.external.ts +++ b/packages/next/src/server/app-render/work-unit-async-storage.external.ts @@ -10,8 +10,8 @@ import type { DynamicTrackingState } from './dynamic-rendering' import { workUnitAsyncStorage } from './work-unit-async-storage-instance' with { 'turbopack-transition': 'next-shared' } import type { ServerComponentsHmrCache } from '../response-cache' import type { - ImmutableResumeDataCache, - MutableResumeDataCache, + RenderResumeDataCache, + PrerenderResumeDataCache, } from '../resume-data-cache/resume-data-cache' type WorkUnitPhase = 'action' | 'render' | 'after' @@ -56,16 +56,16 @@ export type RequestStore = { /** * The resume data cache for this request. This will be a immutable cache. */ - immutableResumeDataCache: ImmutableResumeDataCache | null - - /** - * The resume data cache for this request. This will be a mutable cache. - */ - mutableResumeDataCache: MutableResumeDataCache | null + renderResumeDataCache: RenderResumeDataCache | null // DEV-only usedDynamic?: boolean prerenderPhase?: boolean + + /** + * The resume data cache for this request. This will be a mutable cache. + */ + devWarmupPrerenderResumeDataCache: PrerenderResumeDataCache | null } & PhasePartial /** @@ -114,7 +114,7 @@ export type PrerenderStoreModern = { /** * The resume data cache for this prerender. */ - mutableResumeDataCache: MutableResumeDataCache | null + prerenderResumeDataCache: PrerenderResumeDataCache | null // DEV ONLY // When used this flag informs certain APIs to skip logging because we're @@ -136,7 +136,7 @@ export type PrerenderStorePPR = { /** * The resume data cache for this prerender. */ - mutableResumeDataCache: MutableResumeDataCache + prerenderResumeDataCache: PrerenderResumeDataCache } & PhasePartial export type PrerenderStoreLegacy = { @@ -216,35 +216,39 @@ export function getExpectedRequestStore( ) } -export function getMutableResumeDataCache( +export function getPrerenderResumeDataCache( workUnitStore: WorkUnitStore -): MutableResumeDataCache | null { +): PrerenderResumeDataCache | null { if ( workUnitStore.type !== 'prerender-legacy' && workUnitStore.type !== 'cache' && workUnitStore.type !== 'unstable-cache' ) { - return workUnitStore.mutableResumeDataCache + if (workUnitStore.type === 'request') { + return workUnitStore.devWarmupPrerenderResumeDataCache + } + + return workUnitStore.prerenderResumeDataCache } return null } -export function getImmutableResumeDataCache( +export function getRenderResumeDataCache( workUnitStore: WorkUnitStore -): ImmutableResumeDataCache | null { +): RenderResumeDataCache | null { if ( workUnitStore.type !== 'prerender-legacy' && workUnitStore.type !== 'cache' && workUnitStore.type !== 'unstable-cache' ) { if (workUnitStore.type === 'request') { - return workUnitStore.immutableResumeDataCache + return workUnitStore.renderResumeDataCache } // We return the mutable resume data cache here as an immutable version of // the cache as it can also be used for reading. - return workUnitStore.mutableResumeDataCache + return workUnitStore.prerenderResumeDataCache } return null diff --git a/packages/next/src/server/async-storage/request-store.ts b/packages/next/src/server/async-storage/request-store.ts index 92282a029266cc..68391d8d2f6789 100644 --- a/packages/next/src/server/async-storage/request-store.ts +++ b/packages/next/src/server/async-storage/request-store.ts @@ -21,7 +21,7 @@ import { ResponseCookies, RequestCookies } from '../web/spec-extension/cookies' import { DraftModeProvider } from './draft-mode-provider' import { splitCookiesString } from '../web/utils' import type { ServerComponentsHmrCache } from '../response-cache' -import type { ImmutableResumeDataCache } from '../resume-data-cache/resume-data-cache' +import type { RenderResumeDataCache } from '../resume-data-cache/resume-data-cache' function getHeaders(headers: Headers | IncomingHttpHeaders): ReadonlyHeaders { const cleaned = HeadersAdapter.from(headers) @@ -108,7 +108,7 @@ export function createRequestStoreForRender( url: RequestContext['url'], implicitTags: RequestContext['implicitTags'], onUpdateCookies: RenderOpts['onUpdateCookies'], - immutableResumeDataCache: ImmutableResumeDataCache | undefined, + renderResumeDataCache: RenderResumeDataCache | undefined, previewProps: WrapperRenderOpts['previewProps'], isHmrRefresh: RequestContext['isHmrRefresh'], serverComponentsHmrCache: RequestContext['serverComponentsHmrCache'] @@ -121,7 +121,7 @@ export function createRequestStoreForRender( url, implicitTags, onUpdateCookies, - immutableResumeDataCache, + renderResumeDataCache, previewProps, isHmrRefresh, serverComponentsHmrCache @@ -157,7 +157,7 @@ function createRequestStoreImpl( url: RequestContext['url'], implicitTags: RequestContext['implicitTags'], onUpdateCookies: RenderOpts['onUpdateCookies'], - immutableResumeDataCache: ImmutableResumeDataCache | undefined, + renderResumeDataCache: RenderResumeDataCache | undefined, previewProps: WrapperRenderOpts['previewProps'], isHmrRefresh: RequestContext['isHmrRefresh'], serverComponentsHmrCache: RequestContext['serverComponentsHmrCache'] @@ -247,8 +247,8 @@ function createRequestStoreImpl( return cache.draftMode }, - immutableResumeDataCache: immutableResumeDataCache ?? null, - mutableResumeDataCache: null, + renderResumeDataCache: renderResumeDataCache ?? null, + devWarmupPrerenderResumeDataCache: null, isHmrRefresh, serverComponentsHmrCache: serverComponentsHmrCache || diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 77ae1a3121fe5b..b7ed3323576876 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -2692,9 +2692,9 @@ export default abstract class Server< // If the warmup is successful, we should use the resume data // cache from the warmup. - if (warmup.metadata.devWarmupImmutableResumeDataCache) { - renderOpts.devWarmupImmutableResumeDataCache = - warmup.metadata.devWarmupImmutableResumeDataCache + if (warmup.metadata.devWarmupPrerenderResumeDataCache) { + renderOpts.devWarmupRenderResumeDataCache = + warmup.metadata.devWarmupPrerenderResumeDataCache } } diff --git a/packages/next/src/server/lib/incremental-cache/index.ts b/packages/next/src/server/lib/incremental-cache/index.ts index 26fde009036ee8..9c88abfcd436e4 100644 --- a/packages/next/src/server/lib/incremental-cache/index.ts +++ b/packages/next/src/server/lib/incremental-cache/index.ts @@ -23,7 +23,10 @@ import { import { toRoute } from '../to-route' import { SharedRevalidateTimings } from './shared-revalidate-timings' import { workUnitAsyncStorage } from '../../app-render/work-unit-async-storage-instance' -import { getMutableResumeDataCache } from '../../app-render/work-unit-async-storage.external' +import { + getPrerenderResumeDataCache, + getRenderResumeDataCache, +} from '../../app-render/work-unit-async-storage.external' export interface CacheHandlerContext { fs?: CacheFs @@ -404,7 +407,7 @@ export class IncrementalCache implements IncrementalCacheType { if (this.hasDynamicIO && ctx.kind === IncrementalCacheKind.FETCH) { const workUnitStore = workUnitAsyncStorage.getStore() const resumeDataCache = workUnitStore - ? getMutableResumeDataCache(workUnitStore) + ? getRenderResumeDataCache(workUnitStore) : null if (resumeDataCache) { const memoryCacheData = resumeDataCache.fetch.get(cacheKey) @@ -548,11 +551,11 @@ export class IncrementalCache implements IncrementalCacheType { // to allow the RSC debug info to have the right environment associated to it. if (this.hasDynamicIO && data?.kind === CachedRouteKind.FETCH) { const workUnitStore = workUnitAsyncStorage.getStore() - const mutableResumeDataCache = workUnitStore - ? getMutableResumeDataCache(workUnitStore) + const prerenderResumeDataCache = workUnitStore + ? getPrerenderResumeDataCache(workUnitStore) : null - if (mutableResumeDataCache) { - mutableResumeDataCache.fetch.set(pathname, data) + if (prerenderResumeDataCache) { + prerenderResumeDataCache.fetch.set(pathname, data) } } diff --git a/packages/next/src/server/render-result.ts b/packages/next/src/server/render-result.ts index ef13be991d9271..f8231693dd943e 100644 --- a/packages/next/src/server/render-result.ts +++ b/packages/next/src/server/render-result.ts @@ -10,7 +10,7 @@ import { streamToString, } from './stream-utils/node-web-streams-helper' import { isAbortError, pipeToNodeResponse } from './pipe-readable' -import type { ImmutableResumeDataCache } from './resume-data-cache/resume-data-cache' +import type { PrerenderResumeDataCache } from './resume-data-cache/resume-data-cache' type ContentTypeOption = string | undefined @@ -40,7 +40,7 @@ export type AppPageRenderResultMetadata = { * In development, the cache is warmed up before the render. This is attached * to the metadata so that it can be used during the render. */ - devWarmupImmutableResumeDataCache?: ImmutableResumeDataCache + devWarmupPrerenderResumeDataCache?: PrerenderResumeDataCache } export type PagesRenderResultMetadata = { diff --git a/packages/next/src/server/resume-data-cache/cache-store.ts b/packages/next/src/server/resume-data-cache/cache-store.ts index 4ad14467a47483..94f2820f9717c5 100644 --- a/packages/next/src/server/resume-data-cache/cache-store.ts +++ b/packages/next/src/server/resume-data-cache/cache-store.ts @@ -6,59 +6,40 @@ import type { CacheEntry } from '../lib/cache-handlers/types' import type { CachedFetchValue } from '../response-cache/types' /** - * A generic cache store interface. + * A generic cache store type that provides a subset of Map functionality */ -interface CacheStore { - get(key: string): T | undefined - set(key: string, value: T): void - entries(): Promise<[string, S][]> - seal(): void - readonly size: number -} +type CacheStore = Pick, 'entries' | 'size' | 'get' | 'set'> /** - * A mutable cache store for the fetch cache. + * A cache store specifically for fetch cache values */ -export class FetchCacheStore implements CacheStore { - private readonly store: Map - - /** - * Whether the store is immutable. - */ - private immutable: boolean = false - public seal() { - this.immutable = true - } - - constructor(entries?: Iterable<[string, CachedFetchValue]>) { - if (entries) { - this.immutable = true - this.store = new Map(entries) - } else { - this.store = new Map() - } - } +export type FetchCacheStore = CacheStore - public set(key: string, value: CachedFetchValue): void { - if (this.immutable) { - throw new Error('FetchCacheStore is immutable') - } - this.store.set(key, value) - } - - public get size(): number { - return this.store.size - } - - public get(key: string): CachedFetchValue | undefined { - return this.store.get(key) - } +/** + * Parses fetch cache entries into a FetchCacheStore + * @param entries - The entries to parse into the store + * @returns A new FetchCacheStore containing the entries + */ +export function parseFetchCacheStore( + entries: Iterable<[string, CachedFetchValue]> +): FetchCacheStore { + return new Map(entries) +} - public async entries(): Promise<[string, CachedFetchValue][]> { - return Array.from(this.store.entries()) - } +/** + * Stringifies a FetchCacheStore into an array of key-value pairs + * @param store - The store to stringify + * @returns A promise that resolves to an array of key-value pairs + */ +export function stringifyFetchCacheStore( + entries: IterableIterator<[string, CachedFetchValue]> +): [string, CachedFetchValue][] { + return Array.from(entries) } +/** + * Serialized format for cache entries + */ interface CacheCacheStoreSerialized { value: string tags: string[] @@ -69,98 +50,89 @@ interface CacheCacheStoreSerialized { } /** - * A mutable cache store for the "use cache" cache. + * A cache store specifically for "use cache" values that stores promises of + * cache entries. */ -export class UseCacheCacheStore - implements CacheStore, CacheCacheStoreSerialized> -{ - private readonly store = new Map>() +export type UseCacheCacheStore = Pick< + Map>, + 'entries' | 'size' | 'get' | 'set' +> - /** - * Whether the store is immutable. - */ - private immutable: boolean = false - public seal() { - this.immutable = true +/** + * Parses serialized cache entries into a UseCacheCacheStore + * @param entries - The serialized entries to parse + * @returns A new UseCacheCacheStore containing the parsed entries + */ +export function parseUseCacheCacheStore( + entries: Iterable<[string, CacheCacheStoreSerialized]> +): UseCacheCacheStore { + const store = new Map>() + + for (const [ + key, + { value, tags, stale, timestamp, expire, revalidate }, + ] of entries) { + store.set( + key, + Promise.resolve({ + // Create a ReadableStream from the Uint8Array + value: new ReadableStream({ + start(controller) { + // Enqueue the Uint8Array to the stream + controller.enqueue(stringToUint8Array(atob(value))) + + // Close the stream + controller.close() + }, + }), + tags, + stale, + timestamp, + expire, + revalidate, + }) + ) } - constructor(entries?: Iterable<[string, CacheCacheStoreSerialized]>) { - if (entries) { - this.immutable = true + return store +} - for (const [ - key, - { value, tags, stale, timestamp, expire, revalidate }, - ] of entries) { - this.store.set( +/** + * Stringifies a UseCacheCacheStore into an array of key-value pairs + * @param store - The store to stringify + * @returns A promise that resolves to an array of key-value pairs with serialized values + */ +export async function stringifyUseCacheCacheStore( + entries: IterableIterator<[string, Promise]> +): Promise<[string, CacheCacheStoreSerialized][]> { + return Promise.all( + Array.from(entries).map(([key, value]) => { + return value.then(async (entry) => { + const [left, right] = entry.value.tee() + entry.value = right + + let binaryString: string = '' + + // We want to encode the value as a string, but we aren't sure if the + // value is a a stream of UTF-8 bytes or not, so let's just encode it + // as a string using base64. + for await (const chunk of left) { + binaryString += arrayBufferToString(chunk) + } + + return [ key, - Promise.resolve({ - // Create a ReadableStream from the Uint8Array - value: new ReadableStream({ - start(controller) { - // Enqueue the Uint8Array to the stream - controller.enqueue(stringToUint8Array(atob(value))) - - // Close the stream - controller.close() - }, - }), - tags, - stale, - timestamp, - expire, - revalidate, - }) - ) - } - } - } - - public set(key: string, value: Promise): void { - if (this.immutable) { - throw new Error('CacheCacheStore is immutable') - } - this.store.set(key, value) - } - - public get(key: string): Promise | undefined { - return this.store.get(key) - } - - public get size(): number { - return this.store.size - } - - public async entries(): Promise<[string, CacheCacheStoreSerialized][]> { - return Promise.all( - Array.from(this.store.entries()).map(([key, value]) => { - return value.then(async (entry) => { - const [left, right] = entry.value.tee() - entry.value = right - - let binaryString: string = '' - - // We want to encode the value as a string, but we aren't sure if the - // value is a a stream of UTF-8 bytes or not, so let's just encode it - // as a string using base64. - for await (const chunk of left) { - binaryString += arrayBufferToString(chunk) - } - - return [ - key, - { - // Encode the value as a base64 string. - value: btoa(binaryString), - tags: entry.tags, - stale: entry.stale, - timestamp: entry.timestamp, - expire: entry.expire, - revalidate: entry.revalidate, - }, - ] as [string, CacheCacheStoreSerialized] - }) + { + // Encode the value as a base64 string. + value: btoa(binaryString), + tags: entry.tags, + stale: entry.stale, + timestamp: entry.timestamp, + expire: entry.expire, + revalidate: entry.revalidate, + }, + ] as [string, CacheCacheStoreSerialized] }) - ) - } + }) + ) } diff --git a/packages/next/src/server/resume-data-cache/resume-data-cache.ts b/packages/next/src/server/resume-data-cache/resume-data-cache.ts index 9709220f6125ff..139cc3d1c279d7 100644 --- a/packages/next/src/server/resume-data-cache/resume-data-cache.ts +++ b/packages/next/src/server/resume-data-cache/resume-data-cache.ts @@ -1,24 +1,24 @@ -import { UseCacheCacheStore, FetchCacheStore } from './cache-store' +import type { UseCacheCacheStore, FetchCacheStore } from './cache-store' /** * An immutable version of the resume data cache. */ -export interface ImmutableResumeDataCache { +export interface RenderResumeDataCache { /** * The cache store for the "use cache" cache. */ - readonly cache: Omit + readonly cache: Omit /** * The cache store for the fetch cache. */ - readonly fetch: Omit + readonly fetch: Omit } /** * A mutable version of the resume data cache. */ -export interface MutableResumeDataCache { +export interface PrerenderResumeDataCache { /** * The cache store for the "use cache" cache. */ @@ -34,26 +34,9 @@ export interface MutableResumeDataCache { * Creates a new mutable resume data cache. This cache can be mutated and then * sealed to create an immutable version of the cache. */ -export function createMutableResumeDataCache(): MutableResumeDataCache { +export function createPrerenderResumeDataCache(): PrerenderResumeDataCache { return { - cache: new UseCacheCacheStore(), - fetch: new FetchCacheStore(), - } -} - -/** - * Seals a mutable resume data cache to create an immutable version of the - * cache. - */ -export function sealResumeDataCache( - mutableResumeDataCache: MutableResumeDataCache -): ImmutableResumeDataCache { - // Prevent further mutations by sealing the cache entries. - mutableResumeDataCache.cache.seal() - mutableResumeDataCache.fetch.seal() - - return { - cache: mutableResumeDataCache.cache, - fetch: mutableResumeDataCache.fetch, + cache: new Map(), + fetch: new Map(), } } diff --git a/packages/next/src/server/resume-data-cache/serialization.test.ts b/packages/next/src/server/resume-data-cache/serialization.test.ts index 4beb37cb9376d2..ede11adada18c1 100644 --- a/packages/next/src/server/resume-data-cache/serialization.test.ts +++ b/packages/next/src/server/resume-data-cache/serialization.test.ts @@ -1,18 +1,15 @@ import { stringifyResumeDataCache, parseResumeDataCache } from './serialization' -import { - createMutableResumeDataCache, - sealResumeDataCache, -} from './resume-data-cache' +import { createPrerenderResumeDataCache } from './resume-data-cache' import { streamFromString } from '../stream-utils/node-web-streams-helper' describe('stringifyResumeDataCache', () => { it('serializes an empty cache', async () => { - const cache = sealResumeDataCache(createMutableResumeDataCache()) + const cache = createPrerenderResumeDataCache() expect(await stringifyResumeDataCache(cache)).toBe('null') }) it('serializes a cache with a single entry', async () => { - const cache = createMutableResumeDataCache() + const cache = createPrerenderResumeDataCache() cache.cache.set( 'key', Promise.resolve({ @@ -25,9 +22,7 @@ describe('stringifyResumeDataCache', () => { }) ) - expect( - await stringifyResumeDataCache(sealResumeDataCache(cache)) - ).toMatchInlineSnapshot( + expect(await stringifyResumeDataCache(cache)).toMatchInlineSnapshot( `"{"store":{"fetch":{},"cache":{"key":{"value":"dmFsdWU=","tags":[],"stale":0,"timestamp":0,"expire":0,"revalidate":0}}}}"` ) }) @@ -36,7 +31,7 @@ describe('stringifyResumeDataCache', () => { describe('parseResumeDataCache', () => { it('parses an empty cache', () => { expect(parseResumeDataCache('null')).toEqual( - sealResumeDataCache(createMutableResumeDataCache()) + createPrerenderResumeDataCache() ) }) }) diff --git a/packages/next/src/server/resume-data-cache/serialization.ts b/packages/next/src/server/resume-data-cache/serialization.ts index 0507440a85bd5c..e7bbe573c82b6f 100644 --- a/packages/next/src/server/resume-data-cache/serialization.ts +++ b/packages/next/src/server/resume-data-cache/serialization.ts @@ -1,5 +1,10 @@ -import type { ImmutableResumeDataCache } from './resume-data-cache' -import { UseCacheCacheStore, FetchCacheStore } from './cache-store' +import type { RenderResumeDataCache } from './resume-data-cache' +import { + parseUseCacheCacheStore, + parseFetchCacheStore, + stringifyFetchCacheStore, + stringifyUseCacheCacheStore, +} from './cache-store' type ResumeStoreSerialized = { store: { @@ -16,7 +21,7 @@ type ResumeStoreSerialized = { * Serializes an immutable resume data cache into a JSON string. */ export async function stringifyResumeDataCache( - resumeDataCache: ImmutableResumeDataCache + resumeDataCache: RenderResumeDataCache ): Promise { if (resumeDataCache.fetch.size === 0 && resumeDataCache.cache.size === 0) { return 'null' @@ -24,8 +29,12 @@ export async function stringifyResumeDataCache( const json: ResumeStoreSerialized = { store: { - fetch: Object.fromEntries(await resumeDataCache.fetch.entries()), - cache: Object.fromEntries(await resumeDataCache.cache.entries()), + fetch: Object.fromEntries( + stringifyFetchCacheStore(resumeDataCache.fetch.entries()) + ), + cache: Object.fromEntries( + await stringifyUseCacheCacheStore(resumeDataCache.cache.entries()) + ), }, } @@ -36,17 +45,17 @@ export async function stringifyResumeDataCache( * Parses a serialized resume data cache into an immutable version of the cache. * This cache cannot be mutated further, and is returned sealed. */ -export function parseResumeDataCache(text: string): ImmutableResumeDataCache { +export function parseResumeDataCache(text: string): RenderResumeDataCache { if (text === 'null') { return { - cache: new UseCacheCacheStore([]), - fetch: new FetchCacheStore([]), + cache: new Map(), + fetch: new Map(), } } const json: ResumeStoreSerialized = JSON.parse(text) return { - cache: new UseCacheCacheStore(Object.entries(json.store.cache)), - fetch: new FetchCacheStore(Object.entries(json.store.fetch)), + cache: parseUseCacheCacheStore(Object.entries(json.store.cache)), + fetch: parseFetchCacheStore(Object.entries(json.store.fetch)), } } diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index 029c649e088a48..a1889ea8d13a1d 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -357,7 +357,7 @@ export class AppRouteRouteModule extends RouteModule< expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...implicitTags], - mutableResumeDataCache: null, + prerenderResumeDataCache: null, }) let prospectiveResult @@ -441,7 +441,7 @@ export class AppRouteRouteModule extends RouteModule< expire: INFINITE_CACHE, stale: INFINITE_CACHE, tags: [...implicitTags], - mutableResumeDataCache: null, + prerenderResumeDataCache: null, }) let responseHandled = false diff --git a/packages/next/src/server/use-cache/use-cache-wrapper.ts b/packages/next/src/server/use-cache/use-cache-wrapper.ts index 9992d7d196b77a..ffd0c0c93d0c3e 100644 --- a/packages/next/src/server/use-cache/use-cache-wrapper.ts +++ b/packages/next/src/server/use-cache/use-cache-wrapper.ts @@ -19,8 +19,8 @@ import type { WorkUnitStore, } from '../app-render/work-unit-async-storage.external' import { - getImmutableResumeDataCache, - getMutableResumeDataCache, + getRenderResumeDataCache, + getPrerenderResumeDataCache, workUnitAsyncStorage, } from '../app-render/work-unit-async-storage.external' import { runInCleanSnapshot } from '../app-render/clean-async-snapshot.external' @@ -522,14 +522,14 @@ export function cache(kind: string, id: string, fn: any) { let stream: undefined | ReadableStream = undefined // Get an immutable and mutable versions of the resume data cache. - const mutableResumeDataCache = workUnitStore - ? getMutableResumeDataCache(workUnitStore) + const prerenderResumeDataCache = workUnitStore + ? getPrerenderResumeDataCache(workUnitStore) : null - const immutableResumeDataCache = workUnitStore - ? getImmutableResumeDataCache(workUnitStore) + const renderResumeDataCache = workUnitStore + ? getRenderResumeDataCache(workUnitStore) : null - if (immutableResumeDataCache) { + if (renderResumeDataCache) { const cacheSignal = workUnitStore && workUnitStore.type === 'prerender' ? workUnitStore.cacheSignal @@ -538,8 +538,7 @@ export function cache(kind: string, id: string, fn: any) { if (cacheSignal) { cacheSignal.beginRead() } - const cachedEntry = - immutableResumeDataCache.cache.get(serializedCacheKey) + const cachedEntry = renderResumeDataCache.cache.get(serializedCacheKey) if (cachedEntry !== undefined) { const existingEntry = await cachedEntry propagateCacheLifeAndTags(workUnitStore, existingEntry) @@ -644,11 +643,11 @@ export function cache(kind: string, id: string, fn: any) { ) let savedCacheEntry - if (mutableResumeDataCache) { + if (prerenderResumeDataCache) { // Create a clone that goes into the cache scope memory cache. const split = clonePendingCacheEntry(pendingCacheEntry) savedCacheEntry = getNthCacheEntry(split, 0) - mutableResumeDataCache.cache.set( + prerenderResumeDataCache.cache.set( serializedCacheKey, getNthCacheEntry(split, 1) ) @@ -672,7 +671,7 @@ export function cache(kind: string, id: string, fn: any) { // If we have a cache scope, we need to clone the entry and set it on // the inner cache scope. - if (mutableResumeDataCache) { + if (prerenderResumeDataCache) { const [entryLeft, entryRight] = cloneCacheEntry(entry) if (cacheSignal) { stream = createTrackedReadableStream(entryLeft.value, cacheSignal) @@ -680,7 +679,7 @@ export function cache(kind: string, id: string, fn: any) { stream = entryLeft.value } - mutableResumeDataCache.cache.set( + prerenderResumeDataCache.cache.set( serializedCacheKey, Promise.resolve(entryRight) ) @@ -703,10 +702,10 @@ export function cache(kind: string, id: string, fn: any) { ) let savedCacheEntry: Promise - if (mutableResumeDataCache) { + if (prerenderResumeDataCache) { const split = clonePendingCacheEntry(pendingCacheEntry) savedCacheEntry = getNthCacheEntry(split, 0) - mutableResumeDataCache.cache.set( + prerenderResumeDataCache.cache.set( serializedCacheKey, getNthCacheEntry(split, 1) )