Skip to content

Commit

Permalink
refactor: move resume data cache into postponed state
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Nov 5, 2024
1 parent f933447 commit 4070640
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 157 deletions.
12 changes: 0 additions & 12 deletions packages/next/src/export/routes/app-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
RSC_SUFFIX,
RSC_SEGMENTS_DIR_SUFFIX,
RSC_SEGMENT_SUFFIX,
NEXT_STATIC_DATA_CACHE_SUFFIX,
} from '../../lib/constants'
import { hasNextSupport } from '../../server/ci-info'
import { lazyRenderAppPage } from '../../server/route-modules/app-page/module.render'
Expand All @@ -28,7 +27,6 @@ import type { WorkStore } from '../../server/app-render/work-async-storage.exter
import type { FallbackRouteParams } from '../../server/request/fallback-params'
import { AfterRunner } from '../../server/after/run-with-after'
import type { RequestLifecycleOpts } from '../../server/base-server'
import { stringifyResumeDataCache } from '../../server/resume-data-cache/serialization'

export const enum ExportedAppPageFiles {
HTML = 'HTML',
Expand All @@ -37,7 +35,6 @@ export const enum ExportedAppPageFiles {
PREFETCH_FLIGHT_SEGMENT = 'PREFETCH_FLIGHT_SEGMENT',
META = 'META',
POSTPONED = 'POSTPONED',
RESUME_CACHE = 'RESUME_CACHE',
}

export async function prospectiveRenderAppPage(
Expand Down Expand Up @@ -150,7 +147,6 @@ export async function exportAppPage(
fetchTags,
fetchMetrics,
segmentFlightData,
immutableResumeDataCache,
} = metadata

// Ensure we don't postpone without having PPR enabled.
Expand Down Expand Up @@ -257,14 +253,6 @@ export async function exportAppPage(
'utf8'
)

if (immutableResumeDataCache) {
await fileWriter(
ExportedAppPageFiles.RESUME_CACHE,
htmlFilepath.replace(/\.html$/, NEXT_STATIC_DATA_CACHE_SUFFIX),
await stringifyResumeDataCache(immutableResumeDataCache)
)
}

const isParallelRoute = /\/@\w+/.test(page)
const isNonSuccessfulStatusCode = res.statusCode > 300

Expand Down
1 change: 0 additions & 1 deletion packages/next/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const ACTION_SUFFIX = '.action'
export const NEXT_DATA_SUFFIX = '.json'
export const NEXT_META_SUFFIX = '.meta'
export const NEXT_BODY_SUFFIX = '.body'
export const NEXT_STATIC_DATA_CACHE_SUFFIX = '.rdc.json'

export const NEXT_CACHE_TAGS_HEADER = 'x-next-cache-tags'
export const NEXT_CACHE_SOFT_TAGS_HEADER = 'x-next-cache-soft-tags'
Expand Down
56 changes: 35 additions & 21 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,9 @@ async function warmupDevRender(
// lift the warmup pathway outside of renderToHTML... but for now this suffices
return new FlightRenderResult('', {
fetchMetrics: ctx.workStore.fetchMetrics,
immutableResumeDataCache: sealResumeDataCache(mutableResumeDataCache),
devWarmupImmutableResumeDataCache: sealResumeDataCache(
mutableResumeDataCache
),
})
}

Expand Down Expand Up @@ -1418,9 +1420,9 @@ export const renderToHTMLOrFlight: AppPageRender = (
// If provided, the postpone state should be parsed so it can be provided to
// React.
if (typeof renderOpts.postponed === 'string') {
if (fallbackRouteParams && fallbackRouteParams.size > 0) {
throw new Error(
'Invariant: postponed state should not be provided when fallback params are provided'
if (fallbackRouteParams) {
throw new InvariantError(
'postponed state should not be provided when fallback params are provided'
)
}

Expand All @@ -1430,6 +1432,19 @@ export const renderToHTMLOrFlight: AppPageRender = (
)
}

if (
postponedState?.immutableResumeDataCache &&
renderOpts.devWarmupImmutableResumeDataCache
) {
throw new InvariantError(
'postponed state and dev warmup immutable resume data cache should not be provided together'
)
}

const immutableResumeDataCache =
renderOpts.devWarmupImmutableResumeDataCache ??
postponedState?.immutableResumeDataCache

const implicitTags = getImplicitTags(
renderOpts.routeModule.definition.page,
url,
Expand All @@ -1446,7 +1461,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
url,
implicitTags,
renderOpts.onUpdateCookies,
renderOpts.immutableResumeDataCache,
immutableResumeDataCache,
renderOpts.previewProps,
isHmrRefresh,
serverComponentsHmrCache
Expand Down Expand Up @@ -2741,9 +2756,6 @@ async function prerenderToStream(

const flightData = await streamToBuffer(reactServerResult.asStream())
metadata.flightData = flightData
metadata.immutableResumeDataCache = sealResumeDataCache(
mutableResumeDataCache
)
metadata.segmentFlightData = await collectSegmentData(
finalAttemptRSCPayload,
flightData,
Expand All @@ -2754,13 +2766,16 @@ async function prerenderToStream(
if (serverIsDynamic || clientIsDynamic) {
if (postponed != null) {
// Dynamic HTML case
metadata.postponed = getDynamicHTMLPostponedState(
metadata.postponed = await getDynamicHTMLPostponedState(
postponed,
fallbackRouteParams
fallbackRouteParams,
sealResumeDataCache(mutableResumeDataCache)
)
} else {
// Dynamic Data case
metadata.postponed = getDynamicDataPostponedState()
metadata.postponed = await getDynamicDataPostponedState(
sealResumeDataCache(mutableResumeDataCache)
)
}
reactServerResult.consume()
return {
Expand Down Expand Up @@ -3203,9 +3218,6 @@ async function prerenderToStream(
}
}

metadata.immutableResumeDataCache = sealResumeDataCache(
mutableResumeDataCache
)
const flightData = await streamToBuffer(
serverPrerenderStreamResult.asStream()
)
Expand Down Expand Up @@ -3349,9 +3361,6 @@ async function prerenderToStream(
renderOpts
)
}
metadata.immutableResumeDataCache = sealResumeDataCache(
mutableResumeDataCache
)

/**
* When prerendering there are three outcomes to consider
Expand All @@ -3371,13 +3380,16 @@ async function prerenderToStream(
if (accessedDynamicData(dynamicTracking.dynamicAccesses)) {
if (postponed != null) {
// Dynamic HTML case.
metadata.postponed = getDynamicHTMLPostponedState(
metadata.postponed = await getDynamicHTMLPostponedState(
postponed,
fallbackRouteParams
fallbackRouteParams,
sealResumeDataCache(mutableResumeDataCache)
)
} else {
// Dynamic Data case.
metadata.postponed = getDynamicDataPostponedState()
metadata.postponed = await getDynamicDataPostponedState(
sealResumeDataCache(mutableResumeDataCache)
)
}
// Regardless of whether this is the Dynamic HTML or Dynamic Data case we need to ensure we include
// server inserted html in the static response because the html that is part of the prerender may depend on it
Expand All @@ -3399,7 +3411,9 @@ async function prerenderToStream(
}
} else if (fallbackRouteParams && fallbackRouteParams.size > 0) {
// Rendering the fallback case.
metadata.postponed = getDynamicDataPostponedState()
metadata.postponed = await getDynamicDataPostponedState(
sealResumeDataCache(mutableResumeDataCache)
)

return {
digestErrorsMap: reactServerErrorsByDigest,
Expand Down
71 changes: 55 additions & 16 deletions packages/next/src/server/app-render/postponed-state.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
createMutableResumeDataCache,
sealResumeDataCache,
} from '../resume-data-cache/resume-data-cache'
import { streamFromString } from '../stream-utils/node-web-streams-helper'
import {
DynamicState,
getDynamicDataPostponedState,
Expand All @@ -6,30 +11,50 @@ import {
} from './postponed-state'

describe('getDynamicHTMLPostponedState', () => {
it('serializes a HTML postponed state with fallback params', () => {
it('serializes a HTML postponed state with fallback params', async () => {
const key = '%%drp:slug:e9615126684e5%%'
const fallbackRouteParams = new Map([['slug', key]])
const state = getDynamicHTMLPostponedState(
const mutableResumeDataCache = createMutableResumeDataCache()

mutableResumeDataCache.cache.set(
'1',
Promise.resolve({
value: streamFromString('hello'),
tags: [],
stale: 0,
timestamp: 0,
expire: 0,
revalidate: 0,
})
)

const state = await getDynamicHTMLPostponedState(
{ [key]: key, nested: { [key]: key } },
fallbackRouteParams
fallbackRouteParams,
sealResumeDataCache(mutableResumeDataCache)
)

expect(state).toMatchInlineSnapshot(
`"39[["slug","%%drp:slug:e9615126684e5%%"]]{"%%drp:slug:e9615126684e5%%":"%%drp:slug:e9615126684e5%%","nested":{"%%drp:slug:e9615126684e5%%":"%%drp:slug:e9615126684e5%%"}}"`
`"169:39[["slug","%%drp:slug:e9615126684e5%%"]]{"%%drp:slug:e9615126684e5%%":"%%drp:slug:e9615126684e5%%","nested":{"%%drp:slug:e9615126684e5%%":"%%drp:slug:e9615126684e5%%"}}{"store":{"fetch":{},"cache":{"1":{"value":"aGVsbG8=","tags":[],"stale":0,"timestamp":0,"expire":0,"revalidate":0}}}}"`
)
})

it('serializes a HTML postponed state without fallback params', () => {
const state = getDynamicHTMLPostponedState({ key: 'value' }, null)
expect(state).toMatchInlineSnapshot(`"{"key":"value"}"`)
it('serializes a HTML postponed state without fallback params', async () => {
const state = await getDynamicHTMLPostponedState(
{ key: 'value' },
null,
sealResumeDataCache(createMutableResumeDataCache())
)
expect(state).toMatchInlineSnapshot(`"15:{"key":"value"}null"`)
})

it('can serialize and deserialize a HTML postponed state with fallback params', () => {
it('can serialize and deserialize a HTML postponed state with fallback params', async () => {
const key = '%%drp:slug:e9615126684e5%%'
const fallbackRouteParams = new Map([['slug', key]])
const state = getDynamicHTMLPostponedState(
const state = await getDynamicHTMLPostponedState(
{ [key]: key },
fallbackRouteParams
fallbackRouteParams,
sealResumeDataCache(createMutableResumeDataCache())
)

const value = 'hello'
Expand All @@ -38,6 +63,9 @@ describe('getDynamicHTMLPostponedState', () => {
expect(parsed).toEqual({
type: DynamicState.HTML,
data: { [value]: value },
immutableResumeDataCache: sealResumeDataCache(
createMutableResumeDataCache()
),
})

// The replacements have been replaced.
Expand All @@ -46,15 +74,17 @@ describe('getDynamicHTMLPostponedState', () => {
})

describe('getDynamicDataPostponedState', () => {
it('serializes a data postponed state with fallback params', () => {
const state = getDynamicDataPostponedState()
expect(state).toMatchInlineSnapshot(`"null"`)
it('serializes a data postponed state with fallback params', async () => {
const state = await getDynamicDataPostponedState(
sealResumeDataCache(createMutableResumeDataCache())
)
expect(state).toMatchInlineSnapshot(`"4:nullnull"`)
})
})

describe('parsePostponedState', () => {
it('parses a HTML postponed state with fallback params', () => {
const state = `39[["slug","%%drp:slug:e9615126684e5%%"]]{"t":2,"d":{"nextSegmentId":2,"rootFormatContext":{"insertionMode":0,"selectedValue":null,"tagScope":0},"progressiveChunkSize":12800,"resumableState":{"idPrefix":"","nextFormID":0,"streamingFormat":0,"instructions":0,"hasBody":true,"hasHtml":true,"unknownResources":{},"dnsResources":{},"connectResources":{"default":{},"anonymous":{},"credentials":{}},"imageResources":{},"styleResources":{},"scriptResources":{"/_next/static/chunks/webpack-6b2534a6458c6fe5.js":null,"/_next/static/chunks/f5e865f6-5e04edf75402c5e9.js":null,"/_next/static/chunks/9440-26a4cfbb73347735.js":null,"/_next/static/chunks/main-app-315ef55d588dbeeb.js":null,"/_next/static/chunks/8630-8e01a4bea783c651.js":null,"/_next/static/chunks/app/layout-1b900e1a3caf3737.js":null},"moduleUnknownResources":{},"moduleScriptResources":{"/_next/static/chunks/webpack-6b2534a6458c6fe5.js":null}},"replayNodes":[["oR",0,[["Context.Provider",0,[["ServerInsertedHTMLProvider",0,[["Context.Provider",0,[["n7",0,[["nU",0,[["nF",0,[["n9",0,[["Fragment",0,[["Context.Provider",2,[["Context.Provider",0,[["Context.Provider",0,[["Context.Provider",0,[["Context.Provider",0,[["Context.Provider",0,[["nY",0,[["nX",0,[["Fragment","c",[["Fragment",0,[["html",1,[["body",0,[["main",3,[["j",0,[["Fragment",0,[["Context.Provider","validation",[["i",2,[["Fragment",0,[["E",0,[["R",0,[["h",0,[["Fragment",0,[["O",0,[["Fragment",0,[["s",0,[["c",0,[["s",0,[["c",0,[["v",0,[["Context.Provider",0,[["Fragment","c",[["j",1,[["Fragment",0,[["Context.Provider","slug|%%drp:slug:e9615126684e5%%|d",[["i",2,[["Fragment",0,[["E",0,[["R",0,[["h",0,[["Fragment",0,[["O",0,[["Fragment",0,[["s",0,[["Fragment",0,[["s",0,[["c",0,[["v",0,[["Context.Provider",0,[["Fragment","c",[["j",1,[["Fragment",0,[["Context.Provider","__PAGE__",[["i",2,[["Fragment",0,[["E",0,[["R",0,[["h",0,[["Fragment",0,[["O",0,[["Suspense",0,[["s",0,[["Fragment",0,[["s",0,[["c",0,[["v",0,[["Context.Provider",0,[["Fragment","c",[["Fragment",0,[],{"1":1}]],null]],null]],null]],null]],null]],null]],null]],null,["Suspense Fallback",0,[],null],0]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],"replaySlots":null}}`
const state = `2589:39[["slug","%%drp:slug:e9615126684e5%%"]]{"t":2,"d":{"nextSegmentId":2,"rootFormatContext":{"insertionMode":0,"selectedValue":null,"tagScope":0},"progressiveChunkSize":12800,"resumableState":{"idPrefix":"","nextFormID":0,"streamingFormat":0,"instructions":0,"hasBody":true,"hasHtml":true,"unknownResources":{},"dnsResources":{},"connectResources":{"default":{},"anonymous":{},"credentials":{}},"imageResources":{},"styleResources":{},"scriptResources":{"/_next/static/chunks/webpack-6b2534a6458c6fe5.js":null,"/_next/static/chunks/f5e865f6-5e04edf75402c5e9.js":null,"/_next/static/chunks/9440-26a4cfbb73347735.js":null,"/_next/static/chunks/main-app-315ef55d588dbeeb.js":null,"/_next/static/chunks/8630-8e01a4bea783c651.js":null,"/_next/static/chunks/app/layout-1b900e1a3caf3737.js":null},"moduleUnknownResources":{},"moduleScriptResources":{"/_next/static/chunks/webpack-6b2534a6458c6fe5.js":null}},"replayNodes":[["oR",0,[["Context.Provider",0,[["ServerInsertedHTMLProvider",0,[["Context.Provider",0,[["n7",0,[["nU",0,[["nF",0,[["n9",0,[["Fragment",0,[["Context.Provider",2,[["Context.Provider",0,[["Context.Provider",0,[["Context.Provider",0,[["Context.Provider",0,[["Context.Provider",0,[["nY",0,[["nX",0,[["Fragment","c",[["Fragment",0,[["html",1,[["body",0,[["main",3,[["j",0,[["Fragment",0,[["Context.Provider","validation",[["i",2,[["Fragment",0,[["E",0,[["R",0,[["h",0,[["Fragment",0,[["O",0,[["Fragment",0,[["s",0,[["c",0,[["s",0,[["c",0,[["v",0,[["Context.Provider",0,[["Fragment","c",[["j",1,[["Fragment",0,[["Context.Provider","slug|%%drp:slug:e9615126684e5%%|d",[["i",2,[["Fragment",0,[["E",0,[["R",0,[["h",0,[["Fragment",0,[["O",0,[["Fragment",0,[["s",0,[["Fragment",0,[["s",0,[["c",0,[["v",0,[["Context.Provider",0,[["Fragment","c",[["j",1,[["Fragment",0,[["Context.Provider","__PAGE__",[["i",2,[["Fragment",0,[["E",0,[["R",0,[["h",0,[["Fragment",0,[["O",0,[["Suspense",0,[["s",0,[["Fragment",0,[["s",0,[["c",0,[["v",0,[["Context.Provider",0,[["Fragment","c",[["Fragment",0,[],{"1":1}]],null]],null]],null]],null]],null]],null]],null]],null,["Suspense Fallback",0,[],null],0]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],null]],"replaySlots":null}}null`
const params = {
slug: Math.random().toString(16).slice(3),
}
Expand All @@ -64,31 +94,40 @@ describe('parsePostponedState', () => {
expect(parsed).toEqual({
type: DynamicState.HTML,
data: expect.any(Object),
immutableResumeDataCache: sealResumeDataCache(
createMutableResumeDataCache()
),
})

// Ensure that the replacement worked and removed all the placeholders.
expect(JSON.stringify(parsed)).not.toMatch(/%%drp:slug:e9615126684e5%%/)
})

it('parses a HTML postponed state without fallback params', () => {
const state = `{}`
const state = `2:{}null`
const params = {}
const parsed = parsePostponedState(state, params)

// Ensure that it parsed it correctly.
expect(parsed).toEqual({
type: DynamicState.HTML,
data: expect.any(Object),
immutableResumeDataCache: sealResumeDataCache(
createMutableResumeDataCache()
),
})
})

it('parses a data postponed state', () => {
const state = 'null'
const state = '4:nullnull'
const parsed = parsePostponedState(state, undefined)

// Ensure that it parsed it correctly.
expect(parsed).toEqual({
type: DynamicState.DATA,
immutableResumeDataCache: sealResumeDataCache(
createMutableResumeDataCache()
),
})
})
})
Loading

0 comments on commit 4070640

Please sign in to comment.