Skip to content

Commit

Permalink
Update server to support segment prefetch requests
Browse files Browse the repository at this point in the history
This updates the server and incremental cache to handle segment
prefetch requests.

Segment requests must always be served from the cache. They must never
reach the application layer or invoke a lambda. If a matching segment
is not present in the cache, it's a 404.
  • Loading branch information
acdlite committed Oct 11, 2024
1 parent 1c6fb80 commit 360a1a5
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 0 deletions.
53 changes: 53 additions & 0 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2145,6 +2145,13 @@ export default abstract class Server<
const isDynamicRSCRequest =
isRoutePPREnabled && isRSCRequest && !isPrefetchRSCRequest

// Need to read this before it's stripped by stripFlightHeaders. We don't
// need to transfer it to the request meta because it's only read
// within this function; the static segment data should have already been
// generated, so we will always either return a static response or a 404.
const segmentPrefetchHeader =
req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER.toLowerCase()]

// we need to ensure the status code if /404 is visited directly
if (is404Page && !isNextDataRequest && !isRSCRequest) {
res.statusCode = 404
Expand Down Expand Up @@ -2755,6 +2762,7 @@ export default abstract class Server<
rscData: metadata.flightData,
postponed: metadata.postponed,
status: res.statusCode,
segmentData: undefined,
} satisfies CachedAppPageValue,
revalidate: metadata.revalidate,
isFallback: !!fallbackRouteParams,
Expand Down Expand Up @@ -3090,6 +3098,51 @@ export default abstract class Server<
}
)

if (
isRoutePPREnabled &&
isPrefetchRSCRequest &&
typeof segmentPrefetchHeader === 'string'
) {
if (cacheEntry?.value?.kind === CachedRouteKind.APP_PAGE) {
// This is a prefetch request for an individual segment's static data.
// Unless the segment is fully dynamic, the data should have already been
// loaded into the cache, when the page itself was generated. So we should
// always either return the cache entry. If no cache entry is available,
// it's a 404 — either the segment is fully dynamic, or an invalid segment
// path was requested.
if (cacheEntry.value.segmentData) {
const matchedSegment =
cacheEntry.value.segmentData[segmentPrefetchHeader]
if (matchedSegment !== undefined) {
return {
type: 'rsc',
body: RenderResult.fromStatic(matchedSegment),
// TODO: Eventually this should use revalidate time of the
// individual segment, not the whole page.
revalidate: cacheEntry.revalidate,
}
}
}
// If the segment is not found, return a 404. Since this is an RSC
// request, there's no reason to render a 404 page; just return an
// empty response.
res.statusCode = 404
return {
type: 'rsc',
body: RenderResult.fromStatic(''),
revalidate: cacheEntry.revalidate,
}
} else {
// Segment prefetches should never reach the application layer. If
// there's no cache entry for this page, it's a 404.
res.statusCode = 404
return {
type: 'rsc',
body: RenderResult.fromStatic(''),
}
}
}

if (isPreviewMode) {
res.setHeader(
'Cache-Control',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
NEXT_DATA_SUFFIX,
NEXT_META_SUFFIX,
RSC_PREFETCH_SUFFIX,
RSC_SEGMENT_SUFFIX,
RSC_SEGMENTS_DIR_SUFFIX,
RSC_SUFFIX,
} from '../../../lib/constants'

Expand Down Expand Up @@ -263,6 +265,38 @@ export default class FileSystemCache implements CacheHandler {
)
} catch {}

let maybeSegmentData: { [segmentPath: string]: string } | undefined
if (meta?.segmentPaths) {
// Collect all the segment data for this page.
// TODO: To optimize file system reads, we should consider creating
// separate cache entries for each segment, rather than storing them
// all on the page's entry. Though the behavior is
// identical regardless.
const segmentData: { [segmentPath: string]: string } = {}
maybeSegmentData = segmentData
const segmentsDir = key + RSC_SEGMENTS_DIR_SUFFIX
await Promise.all(
meta.segmentPaths.map(async (segmentPath: string) => {
const segmentDataFilePath = this.getFilePath(
segmentPath === '/'
? segmentsDir + '/_index' + RSC_SEGMENT_SUFFIX
: segmentsDir + segmentPath + RSC_SEGMENT_SUFFIX,
IncrementalCacheKind.APP_PAGE
)
try {
segmentData[segmentPath] = await this.fs.readFile(
segmentDataFilePath,
'utf8'
)
} catch {
// This shouldn't happen, but if for some reason we fail to
// load a segment from the filesystem, treat it the same as if
// the segment is dynamic and does not have a prefetch.
}
})
)
}

let rscData: Buffer | undefined
if (!isFallback) {
rscData = await this.fs.readFile(
Expand All @@ -282,6 +316,7 @@ export default class FileSystemCache implements CacheHandler {
postponed: meta?.postponed,
headers: meta?.headers,
status: meta?.status,
segmentData: maybeSegmentData,
},
}
} else if (kind === IncrementalCacheKind.PAGES) {
Expand Down Expand Up @@ -405,6 +440,7 @@ export default class FileSystemCache implements CacheHandler {
headers: data.headers,
status: data.status,
postponed: undefined,
segmentPaths: undefined,
}

await this.fs.writeFile(
Expand Down Expand Up @@ -447,6 +483,7 @@ export default class FileSystemCache implements CacheHandler {
headers: data.headers,
status: data.status,
postponed: data.postponed,
segmentPaths: undefined,
}

await this.fs.writeFile(
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/response-cache/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface CachedAppPageValue {
status: number | undefined
postponed: string | undefined
headers: OutgoingHttpHeaders | undefined
segmentData: { [segmentPath: string]: string } | undefined
}

export interface CachedPageValue {
Expand Down Expand Up @@ -117,6 +118,7 @@ export interface IncrementalCachedAppPageValue {
headers: OutgoingHttpHeaders | undefined
postponed: string | undefined
status: number | undefined
segmentData: { [segmentPath: string]: string } | undefined
}

export interface IncrementalCachedPageValue {
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/response-cache/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export async function fromResponseCacheEntry(
rscData: cacheEntry.value.rscData,
headers: cacheEntry.value.headers,
status: cacheEntry.value.status,
segmentData: cacheEntry.value.segmentData,
}
: cacheEntry.value,
}
Expand Down Expand Up @@ -70,6 +71,7 @@ export async function toResponseCacheEntry(
headers: response.value.headers,
status: response.value.status,
postponed: response.value.postponed,
segmentData: response.value.segmentData,
} satisfies CachedAppPageValue)
: response.value,
}
Expand Down

0 comments on commit 360a1a5

Please sign in to comment.