Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Update cache handling for app #43659

Merged
merged 3 commits into from Dec 3, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/next/client/components/app-router-headers.ts
@@ -1,6 +1,7 @@
export const RSC = 'RSC' as const
export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const
export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const
export const FETCH_CACHE_HEADER = 'x-vercel-sc-headers' as const
export const RSC_VARY_HEADER =
`${RSC}, ${NEXT_ROUTER_STATE_TREE}, ${NEXT_ROUTER_PREFETCH}` as const

Expand Down
Expand Up @@ -7,6 +7,9 @@ export interface StaticGenerationStore {
fetchRevalidate?: number
isStaticGeneration?: boolean
forceStatic?: boolean
incrementalCache?: import('../../server/lib/incremental-cache').IncrementalCache
pendingRevalidates?: Promise<any>[]
isRevalidate?: boolean
}

export let staticGenerationAsyncStorage:
Expand Down
29 changes: 28 additions & 1 deletion packages/next/export/worker.ts
Expand Up @@ -12,7 +12,7 @@ import '../server/node-polyfill-fetch'
import { loadRequireHook } from '../build/webpack/require-hook'

import { extname, join, dirname, sep } from 'path'
import { promises } from 'fs'
import fs, { promises } from 'fs'
import AmpHtmlValidator from 'next/dist/compiled/amphtml-validator'
import { loadComponents } from '../server/load-components'
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
Expand All @@ -32,6 +32,7 @@ import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
import { REDIRECT_ERROR_CODE } from '../client/components/redirect'
import { DYNAMIC_ERROR_CODE } from '../client/components/hooks-server-context'
import { NOT_FOUND_ERROR_CODE } from '../client/components/not-found'
import { IncrementalCache } from '../server/lib/incremental-cache'

loadRequireHook()

Expand Down Expand Up @@ -98,6 +99,7 @@ interface RenderOpts {
domainLocales?: DomainLocale[]
trailingSlash?: boolean
supportsDynamicHTML?: boolean
incrementalCache?: import('../server/lib/incremental-cache').IncrementalCache
}

// expose AsyncLocalStorage on globalThis for react usage
Expand Down Expand Up @@ -310,6 +312,31 @@ export default async function exportPage({
// and bail when dynamic dependencies are detected
// only fully static paths are fully generated here
if (isAppDir) {
curRenderOpts.incrementalCache = new IncrementalCache({
dev: false,
requestHeaders: {},
flushToDisk: true,
maxMemoryCacheSize: 50 * 1024 * 1024,
getPrerenderManifest: () => ({
version: 3,
routes: {},
dynamicRoutes: {},
preview: {
previewModeEncryptionKey: '',
previewModeId: '',
previewModeSigningKey: '',
},
notFoundRoutes: [],
}),
fs: {
readFile: (f) => fs.promises.readFile(f, 'utf8'),
readFileSync: (f) => fs.readFileSync(f, 'utf8'),
writeFile: (f, d) => fs.promises.writeFile(f, d, 'utf8'),
mkdir: (dir) => fs.promises.mkdir(dir, { recursive: true }),
stat: (f) => fs.promises.stat(f),
},
serverDistDir: join(distDir, 'server'),
})
const { renderToHTMLOrFlight } =
require('../server/app-render') as typeof import('../server/app-render')

Expand Down
131 changes: 122 additions & 9 deletions packages/next/server/app-render.tsx
Expand Up @@ -80,6 +80,7 @@ function preloadComponent(Component: any, props: any) {
return Component
}

const CACHE_ONE_YEAR = 31536000
const INTERNAL_HEADERS_INSTANCE = Symbol('internal for headers readonly')

function readonlyHeadersError() {
Expand Down Expand Up @@ -176,6 +177,8 @@ export type RenderOptsPartial = {
assetPrefix?: string
fontLoaderManifest?: FontLoaderManifest
isBot?: boolean
incrementalCache?: import('./lib/incremental-cache').IncrementalCache
isRevalidate?: boolean
}

export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
Expand Down Expand Up @@ -245,12 +248,115 @@ function patchFetch(ComponentMod: any) {
const originFetch = globalThis.fetch
globalThis.fetch = async (input, init) => {
const staticGenerationStore =
'getStore' in staticGenerationAsyncStorage
('getStore' in staticGenerationAsyncStorage
? staticGenerationAsyncStorage.getStore()
: staticGenerationAsyncStorage
: staticGenerationAsyncStorage) || {}

const {
isStaticGeneration,
fetchRevalidate,
pathname,
incrementalCache,
isRevalidate,
} = (staticGenerationStore || {}) as StaticGenerationStore

let revalidate: number | undefined | boolean

if (typeof init?.next?.revalidate === 'number') {
revalidate = init.next.revalidate
}
if (init?.next?.revalidate === false) {
revalidate = CACHE_ONE_YEAR
}

if (
!staticGenerationStore.fetchRevalidate ||
(typeof revalidate === 'number' &&
revalidate < staticGenerationStore.fetchRevalidate)
) {
staticGenerationStore.fetchRevalidate = revalidate
}

let cacheKey: string | undefined

const doOriginalFetch = async () => {
return originFetch(input, init).then(async (res) => {
if (
incrementalCache &&
cacheKey &&
typeof revalidate === 'number' &&
revalidate > 0
) {
const clonedRes = res.clone()

let base64Body = ''

if (process.env.NEXT_RUNTIME === 'edge') {
let string = ''
new Uint8Array(await clonedRes.arrayBuffer()).forEach((byte) => {
string += String.fromCharCode(byte)
})
base64Body = btoa(string)
} else {
base64Body = Buffer.from(await clonedRes.arrayBuffer()).toString(
'base64'
)
}

await incrementalCache.set(
cacheKey,
{
kind: 'FETCH',
isStale: false,
age: 0,
data: {
headers: Object.fromEntries(clonedRes.headers.entries()),
body: base64Body,
},
revalidate,
},
revalidate,
true
)
}
return res
})
}

const { isStaticGeneration, fetchRevalidate, pathname } =
staticGenerationStore || {}
if (incrementalCache && typeof revalidate === 'number' && revalidate > 0) {
cacheKey = await incrementalCache?.fetchCacheKey(input.toString(), init)
const entry = await incrementalCache.get(cacheKey, true)

if (entry?.value && entry.value.kind === 'FETCH') {
// when stale and is revalidating we wait for fresh data
// so the revalidated entry has the updated data
if (!isRevalidate || !entry.isStale) {
if (entry.isStale) {
if (!staticGenerationStore.pendingRevalidates) {
staticGenerationStore.pendingRevalidates = []
}
staticGenerationStore.pendingRevalidates.push(
doOriginalFetch().catch(console.error)
)
}

const resData = entry.value.data
let decodedBody = ''

// TODO: handle non-text response bodies
if (process.env.NEXT_RUNTIME === 'edge') {
decodedBody = atob(resData.body)
} else {
decodedBody = Buffer.from(resData.body, 'base64').toString()
}

return new Response(decodedBody, {
headers: resData.headers,
status: resData.status,
})
}
}
}

if (staticGenerationStore && isStaticGeneration) {
if (init && typeof init === 'object') {
Expand Down Expand Up @@ -292,7 +398,7 @@ function patchFetch(ComponentMod: any) {
if (hasNextConfig) delete init.next
}
}
return originFetch(input, init)
return doOriginalFetch()
}
}

Expand Down Expand Up @@ -771,9 +877,7 @@ export async function renderToHTMLOrFlight(
supportsDynamicHTML,
} = renderOpts

if (process.env.NODE_ENV === 'production') {
patchFetch(ComponentMod)
}
patchFetch(ComponentMod)
const generateStaticHTML = supportsDynamicHTML !== true

const staticGenerationAsyncStorage = ComponentMod.staticGenerationAsyncStorage
Expand Down Expand Up @@ -1111,7 +1215,7 @@ export async function renderToHTMLOrFlight(
// otherwise
if (layoutOrPageMod.dynamic === 'force-static') {
staticGenerationStore.forceStatic = true
} else {
} else if (layoutOrPageMod.dynamic !== 'error') {
staticGenerationStore.forceStatic = false
}
}
Expand Down Expand Up @@ -1726,6 +1830,10 @@ export async function renderToHTMLOrFlight(
}
const renderResult = new RenderResult(await bodyResult())

if (staticGenerationStore.pendingRevalidates) {
await Promise.all(staticGenerationStore.pendingRevalidates)
}

if (isStaticGeneration) {
const htmlResult = await streamToBufferedResult(renderResult)

Expand All @@ -1744,6 +1852,9 @@ export async function renderToHTMLOrFlight(
await generateFlight()
)

if (staticGenerationStore?.forceStatic === false) {
staticGenerationStore.fetchRevalidate = 0
}
;(renderOpts as any).pageData = filteredFlightData
;(renderOpts as any).revalidate =
typeof staticGenerationStore?.fetchRevalidate === 'undefined'
Expand All @@ -1760,6 +1871,8 @@ export async function renderToHTMLOrFlight(
isStaticGeneration,
inUse: true,
pathname,
incrementalCache: renderOpts.incrementalCache,
isRevalidate: renderOpts.isRevalidate,
}

const tryGetPreviewData =
Expand Down
33 changes: 30 additions & 3 deletions packages/next/server/base-server.ts
Expand Up @@ -78,6 +78,7 @@ import {
RSC,
RSC_VARY_HEADER,
FLIGHT_PARAMETERS,
FETCH_CACHE_HEADER,
} from '../client/components/app-router-headers'

export type FindComponentsResult = {
Expand Down Expand Up @@ -310,6 +311,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
res: BaseNextResponse
): void

protected abstract getIncrementalCache(options: {
requestHeaders: Record<string, undefined | string | string[]>
}): import('./lib/incremental-cache').IncrementalCache

protected abstract getResponseCache(options: {
dev: boolean
}): ResponseCacheBase
Expand Down Expand Up @@ -1283,11 +1288,22 @@ export default abstract class Server<ServerOptions extends Options = Options> {
ssgCacheKey =
ssgCacheKey === '/index' && pathname === '/' ? '/' : ssgCacheKey
}
const incrementalCache = this.getIncrementalCache({
requestHeaders: Object.assign({}, req.headers),
})
if (
this.nextConfig.experimental.fetchCache &&
(opts.runtime !== 'experimental-edge' ||
(this.serverOptions as any).webServerConfig)
) {
delete req.headers[FETCH_CACHE_HEADER]
}
let isRevalidate = false

const doRender: () => Promise<ResponseCacheEntry | null> = async () => {
let pageData: any
let body: RenderResult | null
let sprRevalidate: number | false
let isrRevalidate: number | false
let isNotFound: boolean | undefined
let isRedirect: boolean | undefined

Expand All @@ -1312,6 +1328,12 @@ export default abstract class Server<ServerOptions extends Options = Options> {
const renderOpts: RenderOpts = {
...components,
...opts,
...(isAppPath && this.nextConfig.experimental.fetchCache
? {
incrementalCache,
isRevalidate: this.minimalMode || isRevalidate,
}
: {}),
isDataReq,
resolvedUrl,
locale,
Expand Down Expand Up @@ -1346,7 +1368,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
body = renderResult
// TODO: change this to a different passing mechanism
pageData = (renderOpts as any).pageData
sprRevalidate = (renderOpts as any).revalidate
isrRevalidate = (renderOpts as any).revalidate
isNotFound = (renderOpts as any).isNotFound
isRedirect = (renderOpts as any).isRedirect

Expand All @@ -1361,7 +1383,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
}
value = { kind: 'PAGE', html: body, pageData }
}
return { revalidate: sprRevalidate, value }
return { revalidate: isrRevalidate, value }
}

const cacheEntry = await this.responseCache.get(
Expand All @@ -1371,6 +1393,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
const isDynamicPathname = isDynamicRoute(pathname)
const didRespond = hasResolved || res.sent

if (hadCache) {
isRevalidate = true
}

if (!staticPaths) {
;({ staticPaths, fallbackMode } = hasStaticPaths
? await this.getStaticPaths({ pathname })
Expand Down Expand Up @@ -1486,6 +1512,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
}
},
{
incrementalCache,
isManualRevalidate,
isPrefetch: req.headers.purpose === 'prefetch',
}
Expand Down
3 changes: 3 additions & 0 deletions packages/next/server/config-schema.ts
Expand Up @@ -277,6 +277,9 @@ const configSchema = {
fallbackNodePolyfills: {
type: 'boolean',
},
fetchCache: {
type: 'boolean',
},
forceSwcTransforms: {
type: 'boolean',
},
Expand Down
2 changes: 2 additions & 0 deletions packages/next/server/config-shared.ts
Expand Up @@ -79,6 +79,7 @@ export interface NextJsWebpackConfig {
}

export interface ExperimentalConfig {
fetchCache?: boolean
allowMiddlewareResponseBody?: boolean
skipMiddlewareUrlNormalize?: boolean
skipTrailingSlashRedirect?: boolean
Expand Down Expand Up @@ -565,6 +566,7 @@ export const defaultConfig: NextConfig = {
swcMinify: true,
output: !!process.env.NEXT_PRIVATE_STANDALONE ? 'standalone' : undefined,
experimental: {
fetchCache: false,
middlewarePrefetch: 'flexible',
optimisticClientCache: true,
runtime: undefined,
Expand Down