Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

refactor: update fallbacks to use response cache #68603

Merged
merged 2 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 49 additions & 34 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ import { getRevalidateReason } from './instrumentation/utils'
import { RouteKind } from './route-kind'
import type { RouteModule } from './route-modules/route-module'
import { FallbackMode, parseFallbackField } from '../lib/fallback'
import { toResponseCacheEntry } from './response-cache/utils'

export type FindComponentsResult = {
components: LoadComponentsReturnType
Expand Down Expand Up @@ -378,7 +379,6 @@ export default abstract class Server<
req: ServerRequest,
parsedUrl: NextUrlWithParsedQuery
): void
protected abstract getFallback(page: string): Promise<string>
protected abstract hasPage(pathname: string): Promise<boolean>

protected abstract sendRenderResult(
Expand Down Expand Up @@ -2318,7 +2318,7 @@ export default abstract class Server<
} catch {}

// use existing incrementalCache instance if available
const incrementalCache =
const incrementalCache: import('./lib/incremental-cache').IncrementalCache =
(globalThis as any).__incrementalCache ||
(await this.getIncrementalCache({
requestHeaders: Object.assign({}, req.headers),
Expand Down Expand Up @@ -2709,11 +2709,11 @@ export default abstract class Server<
}
}

const responseGenerator: ResponseGenerator = async (
const responseGenerator: ResponseGenerator = async ({
hasResolved,
previousCacheEntry,
isRevalidating
): Promise<ResponseCacheEntry | null> => {
isRevalidating,
}): Promise<ResponseCacheEntry | null> => {
const isProduction = !this.renderOpts.dev
const didRespond = hasResolved || res.sent

Expand Down Expand Up @@ -2823,37 +2823,51 @@ export default abstract class Server<
throw new NoFallbackError()
}

if (!isNextDataRequest) {
// Production already emitted the fallback as static HTML.
if (isProduction) {
const html = await this.getFallback(
locale ? `/${locale}${pathname}` : pathname
)
if (!isNextDataRequest && !isRSCRequest) {
const key = isProduction
? isAppPath
? pathname
: locale
? `/${locale}${pathname}`
: pathname
: null

const fallbackResponse = await this.responseCache.get(
key,
async ({ previousCacheEntry: previousFallbackCacheEntry }) => {
// 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
// the previous fallback cache entry. This preserves the previous
// behavior.
if (isProduction) {
if (!previousFallbackCacheEntry) {
return null
}

return {
value: {
kind: CachedRouteKind.PAGES,
html: RenderResult.fromStatic(html),
pageData: {},
status: undefined,
headers: undefined,
},
}
}
// We need to generate the fallback on-demand for development.
else {
query.__nextFallback = 'true'

// We pass `undefined` as there cannot be a postponed state in
// development.
const result = await doRender({ postponed: undefined })
if (!result) {
return null
return toResponseCacheEntry(previousFallbackCacheEntry)
}

query.__nextFallback = 'true'

// We pass `undefined` as rendering a fallback isn't resumed here.
return doRender({ postponed: undefined })
},
{
routeKind: isAppPath ? RouteKind.APP_PAGE : RouteKind.PAGES,
incrementalCache,
isRoutePPREnabled,
isFallback: true,
}
// Prevent caching this result
delete result.revalidate
return result
}
)

if (!fallbackResponse) return null

// Remove the revalidate from the response to prevent it from being
// used in the surrounding cache.
delete fallbackResponse.revalidate

return fallbackResponse
}
}

Expand Down Expand Up @@ -2910,6 +2924,7 @@ export default abstract class Server<
isOnDemandRevalidate,
isPrefetch: req.headers.purpose === 'prefetch',
isRoutePPREnabled,
isFallback: false,
}
)

Expand Down
84 changes: 48 additions & 36 deletions packages/next/src/server/lib/incremental-cache/file-system-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export default class FileSystemCache implements CacheHandler {

public async get(...args: Parameters<CacheHandler['get']>) {
const [key, ctx] = args
const { tags, softTags, kind, isRoutePPREnabled } = ctx
const { tags, softTags, kind, isRoutePPREnabled, isFallback } = ctx

let data = memoryCache?.get(key)

Expand Down Expand Up @@ -252,24 +252,27 @@ export default class FileSystemCache implements CacheHandler {
}
} else if (kind === IncrementalCacheKind.APP_PAGE) {
let meta: RouteMetadata | undefined
let rscData: Buffer | undefined

// We try to load the metadata file, but if it fails, we don't
// error.
try {
meta = JSON.parse(
await this.fs.readFile(
filePath.replace(/\.html$/, NEXT_META_SUFFIX),
'utf8'
// error. We also don't load it if this is a fallback.
if (!isFallback) {
try {
meta = JSON.parse(
await this.fs.readFile(
filePath.replace(/\.html$/, NEXT_META_SUFFIX),
'utf8'
)
)
)
} catch {}
} catch {}

const rscData = await this.fs.readFile(
this.getFilePath(
`${key}${isRoutePPREnabled ? RSC_PREFETCH_SUFFIX : RSC_SUFFIX}`,
IncrementalCacheKind.APP_PAGE
rscData = await this.fs.readFile(
this.getFilePath(
`${key}${isRoutePPREnabled ? RSC_PREFETCH_SUFFIX : RSC_SUFFIX}`,
IncrementalCacheKind.APP_PAGE
)
)
)
}

data = {
lastModified: mtime.getTime(),
Expand All @@ -284,16 +287,19 @@ export default class FileSystemCache implements CacheHandler {
}
} else if (kind === IncrementalCacheKind.PAGES) {
let meta: RouteMetadata | undefined
let pageData: string | object = {}

const pageData = JSON.parse(
await this.fs.readFile(
this.getFilePath(
`${key}${NEXT_DATA_SUFFIX}`,
IncrementalCacheKind.PAGES
),
'utf8'
if (!isFallback) {
pageData = JSON.parse(
await this.fs.readFile(
this.getFilePath(
`${key}${NEXT_DATA_SUFFIX}`,
IncrementalCacheKind.PAGES
),
'utf8'
)
)
)
}

data = {
lastModified: mtime.getTime(),
Expand Down Expand Up @@ -376,6 +382,7 @@ export default class FileSystemCache implements CacheHandler {

public async set(...args: Parameters<CacheHandler['set']>) {
const [key, data, ctx] = args
const { isFallback } = ctx
memoryCache?.set(key, {
value: data,
lastModified: Date.now(),
Expand Down Expand Up @@ -417,25 +424,30 @@ export default class FileSystemCache implements CacheHandler {
await this.fs.mkdir(path.dirname(htmlPath))
await this.fs.writeFile(htmlPath, data.html)

await this.fs.writeFile(
this.getFilePath(
`${key}${
// Fallbacks don't generate a data file.
if (!isFallback) {
await this.fs.writeFile(
this.getFilePath(
`${key}${
isAppPath
? ctx.isRoutePPREnabled
? RSC_PREFETCH_SUFFIX
: RSC_SUFFIX
: NEXT_DATA_SUFFIX
}`,
isAppPath
? ctx.isRoutePPREnabled
? RSC_PREFETCH_SUFFIX
: RSC_SUFFIX
: NEXT_DATA_SUFFIX
}`,
isAppPath ? IncrementalCacheKind.APP_PAGE : IncrementalCacheKind.PAGES
),
isAppPath ? data.rscData : JSON.stringify(data.pageData)
)
? IncrementalCacheKind.APP_PAGE
: IncrementalCacheKind.PAGES
),
isAppPath ? data.rscData : JSON.stringify(data.pageData)
)
}

if (data.headers || data.status || (isAppPath && data.postponed)) {
if (data?.kind === CachedRouteKind.APP_PAGE) {
const meta: RouteMetadata = {
headers: data.headers,
status: data.status,
postponed: isAppPath ? data.postponed : undefined,
postponed: data.postponed,
}

await this.fs.writeFile(
Expand Down
12 changes: 8 additions & 4 deletions packages/next/src/server/lib/incremental-cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,16 +190,17 @@ export class IncrementalCache implements IncrementalCacheType {
private calculateRevalidate(
pathname: string,
fromTime: number,
dev?: boolean
dev: boolean,
isFallback: boolean | undefined
): Revalidate {
// in development we don't have a prerender-manifest
// and default to always revalidating to allow easier debugging
if (dev) return new Date().getTime() - 1000

// if an entry isn't present in routes we fallback to a default
// of revalidating after 1 second.
// of revalidating after 1 second unless it's a fallback request.
const initialRevalidateSeconds =
this.revalidateTimings.get(toRoute(pathname)) ?? 1
this.revalidateTimings.get(toRoute(pathname)) ?? (isFallback ? false : 1)

const revalidateAfter =
typeof initialRevalidateSeconds === 'number'
Expand Down Expand Up @@ -384,6 +385,7 @@ export class IncrementalCache implements IncrementalCacheType {
tags?: string[]
softTags?: string[]
isRoutePPREnabled?: boolean
isFallback: boolean | undefined
}
): Promise<IncrementalCacheEntry | null> {
// we don't leverage the prerender cache in dev mode
Expand Down Expand Up @@ -446,7 +448,8 @@ export class IncrementalCache implements IncrementalCacheType {
revalidateAfter = this.calculateRevalidate(
cacheKey,
cacheData?.lastModified || Date.now(),
this.dev && ctx.kind !== IncrementalCacheKind.FETCH
this.dev ? ctx.kind !== IncrementalCacheKind.FETCH : false,
ctx.isFallback
)
isStale =
revalidateAfter !== false && revalidateAfter < Date.now()
Expand Down Expand Up @@ -494,6 +497,7 @@ export class IncrementalCache implements IncrementalCacheType {
fetchIdx?: number
tags?: string[]
isRoutePPREnabled?: boolean
isFallback?: boolean
}
) {
if (this.disableForTestmode || (this.dev && !ctx.fetchCache)) return
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/lib/patch-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ export function createPatchedFetcher(
fetchIdx,
tags,
softTags: implicitTags,
isFallback: false,
})

if (entry) {
Expand Down
10 changes: 1 addition & 9 deletions packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -797,15 +797,6 @@ export default class NextNodeServer extends BaseServer<
) as NextFontManifest
}

protected getFallback(page: string): Promise<string> {
page = normalizePagePath(page)
const cacheFs = this.getCacheFilesystem()
return cacheFs.readFile(
join(this.serverDistDir, 'pages', `${page}.html`),
'utf8'
)
}

protected handleNextImageRequest: NodeRouteHandler = async (
req,
res,
Expand Down Expand Up @@ -887,6 +878,7 @@ export default class NextNodeServer extends BaseServer<
{
routeKind: RouteKind.IMAGE,
incrementalCache: imageOptimizerCache,
isFallback: false,
}
)

Expand Down
Loading
Loading