Skip to content

Commit 7792966

Browse files
committed
feat: support segment cache
1 parent c83e75d commit 7792966

File tree

3 files changed

+92
-40
lines changed

3 files changed

+92
-40
lines changed

src/build/content/prerendered.ts

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ const buildPagesCacheValue = async (
6969
revalidate: initialRevalidateSeconds,
7070
})
7171

72+
const RSC_SEGMENTS_DIR_SUFFIX = '.segments'
73+
const RSC_SEGMENT_SUFFIX = '.segment.rsc'
74+
7275
const buildAppCacheValue = async (
7376
path: string,
7477
shouldUseAppPageKind: boolean,
@@ -78,6 +81,29 @@ const buildAppCacheValue = async (
7881

7982
// supporting both old and new cache kind for App Router pages - https://github.com/vercel/next.js/pull/65988
8083
if (shouldUseAppPageKind) {
84+
// segments are normalized and outputted separately for each segment, we denormalize it here and stitch
85+
// fully constructed segmentData to avoid data fetch waterfalls later in cache handler at runtime
86+
// https://github.com/vercel/next.js/blob/def2c6ba75dff754767379afb44c26c30bd3d96b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts#L185
87+
// let segmentData: NetlifyCachedAppPageValue['segmentData']
88+
if (meta.segmentPaths) {
89+
const segmentsDir = path + RSC_SEGMENTS_DIR_SUFFIX
90+
const segmentData: NetlifyCachedAppPageValue['segmentData'] = {}
91+
92+
console.log('segment stitching', {
93+
segmentPaths: meta.segmentPaths,
94+
path,
95+
})
96+
await Promise.all(
97+
meta.segmentPaths.map(async (segmentPath: string) => {
98+
const segmentDataFilePath = segmentsDir + segmentPath + RSC_SEGMENT_SUFFIX
99+
100+
segmentData[segmentPath] = await readFile(segmentDataFilePath, 'base64')
101+
}),
102+
)
103+
104+
meta.segmentData = segmentData
105+
}
106+
81107
return {
82108
kind: 'APP_PAGE',
83109
html,
@@ -168,45 +194,52 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
168194
: Date.now()
169195
const key = routeToFilePath(route)
170196
let value: NetlifyIncrementalCacheValue
171-
switch (true) {
172-
// Parallel route default layout has no prerendered page
173-
case meta.dataRoute?.endsWith('/default.rsc') &&
174-
!existsSync(join(ctx.publishDir, 'server/app', `${key}.html`)):
175-
return
176-
case meta.dataRoute?.endsWith('.json'):
177-
if (manifest.notFoundRoutes.includes(route)) {
178-
// if pages router returns 'notFound: true', build won't produce html and json files
197+
try {
198+
switch (true) {
199+
// Parallel route default layout has no prerendered page
200+
case meta.dataRoute?.endsWith('/default.rsc') &&
201+
!existsSync(join(ctx.publishDir, 'server/app', `${key}.html`)):
179202
return
180-
}
181-
value = await buildPagesCacheValue(
182-
join(ctx.publishDir, 'server/pages', key),
183-
meta.initialRevalidateSeconds,
184-
shouldUseEnumKind,
185-
)
186-
break
187-
case meta.dataRoute?.endsWith('.rsc'):
188-
value = await buildAppCacheValue(
189-
join(ctx.publishDir, 'server/app', key),
190-
shouldUseAppPageKind,
191-
)
192-
break
193-
case meta.dataRoute === null:
194-
value = await buildRouteCacheValue(
195-
join(ctx.publishDir, 'server/app', key),
196-
meta.initialRevalidateSeconds,
197-
shouldUseEnumKind,
198-
)
199-
break
200-
default:
201-
throw new Error(`Unrecognized content: ${route}`)
202-
}
203+
case meta.dataRoute?.endsWith('.json'):
204+
if (manifest.notFoundRoutes.includes(route)) {
205+
// if pages router returns 'notFound: true', build won't produce html and json files
206+
return
207+
}
203208

204-
// Netlify Forms are not support and require a workaround
205-
if (value.kind === 'PAGE' || value.kind === 'PAGES' || value.kind === 'APP_PAGE') {
206-
verifyNetlifyForms(ctx, value.html)
207-
}
209+
value = await buildPagesCacheValue(
210+
join(ctx.publishDir, 'server/pages', key),
211+
meta.initialRevalidateSeconds,
212+
shouldUseEnumKind,
213+
)
214+
break
215+
case meta.dataRoute?.endsWith('.rsc'):
216+
value = await buildAppCacheValue(
217+
join(ctx.publishDir, 'server/app', key),
218+
shouldUseAppPageKind,
219+
)
220+
break
208221

209-
await writeCacheEntry(key, value, lastModified, ctx)
222+
case meta.dataRoute === null:
223+
value = await buildRouteCacheValue(
224+
join(ctx.publishDir, 'server/app', key),
225+
meta.initialRevalidateSeconds,
226+
shouldUseEnumKind,
227+
)
228+
break
229+
default:
230+
throw new Error(`Unrecognized content: ${route}`)
231+
}
232+
233+
// Netlify Forms are not support and require a workaround
234+
if (value.kind === 'PAGE' || value.kind === 'PAGES' || value.kind === 'APP_PAGE') {
235+
verifyNetlifyForms(ctx, value.html)
236+
}
237+
238+
await writeCacheEntry(key, value, lastModified, ctx)
239+
} catch (error) {
240+
console.error({ key, route })
241+
throw error
242+
}
210243
}),
211244
),
212245
...ctx.getFallbacks(manifest).map(async (route) => {

src/run/handlers/cache.cts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
364364
requestContext.isCacheableAppPage = true
365365
}
366366

367-
const { revalidate, rscData, ...restOfPageValue } = blob.value
367+
const { revalidate, rscData, segmentData, ...restOfPageValue } = blob.value
368368

369369
span.addEvent(blob.value?.kind, { lastModified: blob.lastModified, revalidate, ttl })
370370

@@ -375,6 +375,14 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
375375
value: {
376376
...restOfPageValue,
377377
rscData: rscData ? Buffer.from(rscData, 'base64') : undefined,
378+
segmentData: segmentData
379+
? new Map(
380+
Object.entries(segmentData).map(([segmentPath, base64EncodedSegment]) => [
381+
segmentPath,
382+
Buffer.from(base64EncodedSegment, 'base64'),
383+
]),
384+
)
385+
: undefined,
378386
},
379387
}
380388
}
@@ -416,6 +424,14 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
416424
revalidate: context.revalidate ?? context.cacheControl?.revalidate,
417425
cacheControl: context.cacheControl,
418426
rscData: data.rscData?.toString('base64'),
427+
segmentData: data.segmentData
428+
? Object.fromEntries(
429+
[...data.segmentData.entries()].map(([segmentPath, base64EncodedSegment]) => [
430+
segmentPath,
431+
base64EncodedSegment.toString('base64'),
432+
]),
433+
)
434+
: undefined,
419435
}
420436
}
421437

src/shared/cache-types.cts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
IncrementalCachedPageValue,
1010
IncrementalCacheValue,
1111
} from 'next/dist/server/response-cache/types.js'
12+
import type { IncrementalCachedAppPageValue as IncrementalCachedAppPageValueForNewVersions } from 'next-with-cache-handler-v2/dist/server/response-cache/types.js'
1213

1314
export type { CacheHandlerContext } from 'next/dist/server/lib/incremental-cache/index.js'
1415

@@ -44,17 +45,19 @@ type IncrementalCachedAppPageValueForMultipleVersions = Omit<
4445
'kind'
4546
> & {
4647
kind: 'APP_PAGE'
47-
}
48+
} & Pick<IncrementalCachedAppPageValueForNewVersions, 'segmentData'>
4849

4950
/**
5051
* Used for storing in blobs and reading from blobs
5152
*/
5253
export type NetlifyCachedAppPageValue = Omit<
5354
IncrementalCachedAppPageValueForMultipleVersions,
54-
'rscData'
55+
'rscData' | 'segmentData'
5556
> & {
56-
// Next.js stores rscData as buffer, while we store it as base64 encoded string
57+
// Next.js stores rscData as Buffer, while we store it as base64 encoded string
5758
rscData: string | undefined
59+
// Next.js stores segmentData as Map<string, Buffer>, while we store it as Record<string, string>, where value is base64 encoded string
60+
segmentData: Record<string, string> | undefined
5861
revalidate?: Parameters<CacheHandler['set']>[2]['revalidate']
5962
cacheControl?: CacheControl
6063
}

0 commit comments

Comments
 (0)