Skip to content

Commit

Permalink
Make cacheLife profiles configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Oct 13, 2024
1 parent e09d5b0 commit a60fc42
Show file tree
Hide file tree
Showing 16 changed files with 109 additions and 14 deletions.
10 changes: 10 additions & 0 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,7 @@ export async function buildAppStaticPaths({
segments,
isrFlushToDisk,
cacheHandler,
cacheLifeProfiles,
requestHeaders,
maxMemoryCacheSize,
fetchCacheKeyPrefix,
Expand All @@ -1235,6 +1236,9 @@ export async function buildAppStaticPaths({
isrFlushToDisk?: boolean
fetchCacheKeyPrefix?: string
cacheHandler?: string
cacheLifeProfiles?: {
[profile: string]: import('../server/use-cache/cache-life').CacheLife
}
maxMemoryCacheSize?: number
requestHeaders: IncrementalCache['requestHeaders']
nextConfigOutput: 'standalone' | 'export' | undefined
Expand Down Expand Up @@ -1305,6 +1309,7 @@ export async function buildAppStaticPaths({
fallbackRouteParams: null,
renderOpts: {
incrementalCache,
cacheLifeProfiles,
supportsDynamicResponse: true,
isRevalidate: false,
experimental: {
Expand Down Expand Up @@ -1489,6 +1494,7 @@ export async function isPageStatic({
maxMemoryCacheSize,
nextConfigOutput,
cacheHandler,
cacheLifeProfiles,
pprConfig,
isAppPPRFallbacksEnabled,
buildId,
Expand All @@ -1510,6 +1516,9 @@ export async function isPageStatic({
isrFlushToDisk?: boolean
maxMemoryCacheSize?: number
cacheHandler?: string
cacheLifeProfiles?: {
[profile: string]: import('../server/use-cache/cache-life').CacheLife
}
nextConfigOutput: 'standalone' | 'export' | undefined
pprConfig: ExperimentalPPRConfig | undefined
isAppPPRFallbacksEnabled: boolean | undefined
Expand Down Expand Up @@ -1632,6 +1641,7 @@ export async function isPageStatic({
isrFlushToDisk,
maxMemoryCacheSize,
cacheHandler,
cacheLifeProfiles,
ComponentMod,
nextConfigOutput,
isRoutePPREnabled,
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ async function exportAppImpl(
largePageDataBytes: nextConfig.experimental.largePageDataBytes,
serverActions: nextConfig.experimental.serverActions,
serverComponents: enabledDirectories.app,
cacheLifeProfiles: nextConfig.experimental.cacheLife,
nextFontManifest: require(
join(distDir, 'server', `${NEXT_FONT_MANIFEST}.json`)
),
Expand Down
6 changes: 6 additions & 0 deletions packages/next/src/export/routes/app-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export async function exportAppRoute(
page: string,
module: AppRouteRouteModule,
incrementalCache: IncrementalCache | undefined,
cacheLifeProfiles:
| undefined
| {
[profile: string]: import('../../server/use-cache/cache-life').CacheLife
},
htmlFilepath: string,
fileWriter: FileWriter,
experimental: Required<Pick<ExperimentalConfig, 'after' | 'dynamicIO'>>,
Expand Down Expand Up @@ -73,6 +78,7 @@ export async function exportAppRoute(
nextExport: true,
supportsDynamicResponse: false,
incrementalCache,
cacheLifeProfiles,
waitUntil: undefined,
onClose: undefined,
buildId,
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/export/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ async function exportPageImpl(
page,
components.routeModule as AppRouteRouteModule,
input.renderOpts.incrementalCache,
input.renderOpts.cacheLifeProfiles,
htmlFilepath,
fileWriter,
input.renderOpts.experimental,
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ export interface RenderOptsPartial {
nextFontManifest?: DeepReadonly<NextFontManifest>
isBot?: boolean
incrementalCache?: import('../lib/incremental-cache').IncrementalCache
cacheLifeProfiles?: {
[profile: string]: import('../use-cache/cache-life').CacheLife
}
setAppIsrStatus?: (key: string, value: boolean | null) => void
isRevalidate?: boolean
nextExport?: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { FallbackRouteParams } from '../request/fallback-params'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'
import type { AppSegmentConfig } from '../../build/segment-config/app/app-segment-config'
import type { AfterContext } from '../after/after-context'
import type { CacheLife } from '../use-cache/cache-life'

// Share the instance module in the next-shared layer
import { workAsyncStorage } from './work-async-storage-instance' with { 'turbopack-transition': 'next-shared' }
Expand All @@ -31,6 +32,8 @@ export interface WorkStore {
readonly fallbackRouteParams: FallbackRouteParams | null

readonly incrementalCache?: IncrementalCache
readonly cacheLifeProfiles?: { [profile: string]: CacheLife }

readonly isOnDemandRevalidate?: boolean
readonly isPrerendering?: boolean
readonly isRevalidate?: boolean
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/server/async-storage/with-work-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { FetchMetric } from '../base-http'
import type { RequestLifecycleOpts } from '../base-server'
import type { FallbackRouteParams } from '../request/fallback-params'
import type { AppSegmentConfig } from '../../build/segment-config/app/app-segment-config'
import type { CacheLife } from '../use-cache/cache-life'

import { AfterContext } from '../after/after-context'

Expand All @@ -26,6 +27,7 @@ export type WorkStoreContext = {
requestEndedState?: { ended?: boolean }
isPrefetchRequest?: boolean
renderOpts: {
cacheLifeProfiles?: { [profile: string]: CacheLife }
incrementalCache?: IncrementalCache
isOnDemandRevalidate?: boolean
fetchCache?: AppSegmentConfig['fetchCache']
Expand Down Expand Up @@ -108,6 +110,7 @@ export const withWorkStore: WithStore<WorkStore, WorkStoreContext> = <Result>(
// we fallback to a global incremental cache for edge-runtime locally
// so that it can access the fs cache without mocks
renderOpts.incrementalCache || (globalThis as any).__incrementalCache,
cacheLifeProfiles: renderOpts.cacheLifeProfiles,
isRevalidate: renderOpts.isRevalidate,
isPrerendering: renderOpts.nextExport,
fetchCache: renderOpts.fetchCache,
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ export default abstract class Server<
domainLocales: this.nextConfig.i18n?.domains,
distDir: this.distDir,
serverComponents: this.enabledDirectories.app,
cacheLifeProfiles: this.nextConfig.experimental.cacheLife,
enableTainting: this.nextConfig.experimental.taint,
crossOrigin: this.nextConfig.crossOrigin
? this.nextConfig.crossOrigin
Expand Down Expand Up @@ -2511,6 +2512,7 @@ export default abstract class Server<
},
supportsDynamicResponse,
incrementalCache,
cacheLifeProfiles: this.nextConfig.experimental?.cacheLife,
isRevalidate: isSSG,
waitUntil: this.getWaitUntil(),
onClose: res.onClose.bind(res),
Expand Down
9 changes: 9 additions & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,15 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
static: z.number().optional(),
})
.optional(),
cacheLife: z
.record(
z.object({
stale: z.number().optional(),
revalidate: z.number().optional(),
expire: z.number().optional(),
})
)
.optional(),
clientRouterFilter: z.boolean().optional(),
clientRouterFilterRedirects: z.boolean().optional(),
clientRouterFilterAllowedRate: z.number().optional(),
Expand Down
19 changes: 19 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,19 @@ export interface ExperimentalConfig {
dynamic?: number
static?: number
}
cacheLife?: {
[profile: string]: {
// How long the client can cache a value without checking with the server.
stale?: number
// How frequently you want the cache to refresh on the server.
// Stale values may be served while revalidating.
revalidate?: number
// In the worst case scenario, where you haven't had traffic in a while,
// how stale can a value be until you prefer deopting to dynamic.
// Must be longer than revalidate.
expire?: number
}
}
// decimal for percent for possible false positives
// e.g. 0.01 for 10% potential false matches lower
// percent increases size of the filter
Expand Down Expand Up @@ -1014,6 +1027,12 @@ export const defaultConfig: NextConfig = {
modularizeImports: undefined,
outputFileTracingRoot: process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT || '',
experimental: {
cacheLife: {
default: {
revalidate: 900,
expire: Infinity,
},
},
multiZoneDraftMode: false,
appNavFailHandling: Boolean(process.env.NEXT_PRIVATE_FLYING_SHUTTLE),
flyingShuttle: Boolean(process.env.NEXT_PRIVATE_FLYING_SHUTTLE)
Expand Down
12 changes: 12 additions & 0 deletions packages/next/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,18 @@ function assignDefaults(
}
}

if (result.experimental?.cacheLife) {
const defaultCacheLifeProfile = result.experimental.cacheLife['default']
if (!defaultCacheLifeProfile) {
throw new Error('No default cacheLife profile.')
} else {
if (defaultCacheLifeProfile.stale === undefined) {
const staticStaleTime = result.experimental.staleTimes?.static
defaultCacheLifeProfile.stale = staticStaleTime
}
}
}

const userProvidedModularizeImports = result.modularizeImports
// Unfortunately these packages end up re-exporting 10600 modules, for example: https://unpkg.com/browse/@mui/[email protected]/esm/index.js.
// Leveraging modularizeImports tremendously reduces compile times for these.
Expand Down
5 changes: 5 additions & 0 deletions packages/next/src/server/dev/static-paths-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export async function loadStaticPaths({
maxMemoryCacheSize,
requestHeaders,
cacheHandler,
cacheLifeProfiles,
nextConfigOutput,
isAppPPRFallbacksEnabled,
buildId,
Expand All @@ -64,6 +65,9 @@ export async function loadStaticPaths({
maxMemoryCacheSize?: number
requestHeaders: IncrementalCache['requestHeaders']
cacheHandler?: string
cacheLifeProfiles?: {
[profile: string]: import('../../server/use-cache/cache-life').CacheLife
}
nextConfigOutput: 'standalone' | 'export' | undefined
isAppPPRFallbacksEnabled: boolean | undefined
buildId: string
Expand Down Expand Up @@ -97,6 +101,7 @@ export async function loadStaticPaths({
distDir,
requestHeaders,
cacheHandler,
cacheLifeProfiles,
isrFlushToDisk,
fetchCacheKeyPrefix,
maxMemoryCacheSize,
Expand Down
28 changes: 16 additions & 12 deletions packages/next/src/server/use-cache/cache-life.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { workAsyncStorage } from '../app-render/work-async-storage.external'
import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external'

export type CacheLife = {
Expand All @@ -15,19 +16,9 @@ export type CacheLife = {
// Cache-Control: max-age=[stale],s-max-age=[revalidate],stale-while-revalidate=[expire-revalidate],stale-if-error=[expire-revalidate]
// Except that stale-while-revalidate/stale-if-error only applies to shared caches - not private caches.

const cacheLifeProfileMap: Map<string, CacheLife> = new Map()

// The default revalidates relatively frequently but doesn't expire to ensure it's always
// able to serve fast results but by default doesn't hang.

export const defaultCacheLife = {
stale: Number(process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME),
revalidate: 15 * 60, // Note: This is a new take on the defaults.
expire: Infinity,
}

cacheLifeProfileMap.set('default', defaultCacheLife)

type CacheLifeProfiles = 'default' // TODO: Generate from the config

function validateCacheLife(profile: CacheLife) {
Expand Down Expand Up @@ -99,9 +90,22 @@ export function cacheLife(profile: CacheLifeProfiles | CacheLife): void {
}

if (typeof profile === 'string') {
const configuredProfile = cacheLifeProfileMap.get(profile)
const workStore = workAsyncStorage.getStore()
if (!workStore) {
throw new Error(
'cacheLife() can only be called during App Router rendering at the moment.'
)
}
if (!workStore.cacheLifeProfiles) {
throw new Error(
'cacheLifeProfiles should always be provided. This is a bug in Next.js.'
)
}

// TODO: This should be globally available and not require an AsyncLocalStorage.
const configuredProfile = workStore.cacheLifeProfiles[profile]
if (configuredProfile === undefined) {
if (cacheLifeProfileMap.has(profile.trim())) {
if (workStore.cacheLifeProfiles[profile.trim()]) {
throw new Error(
`Unknown cacheLife profile "${profile}" is not configured in next.config.js\n` +
`Did you mean "${profile.trim()}" without the spaces?`
Expand Down
17 changes: 16 additions & 1 deletion packages/next/src/server/use-cache/use-cache-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
getClientReferenceManifestSingleton,
getServerModuleMap,
} from '../app-render/encryption-utils'
import { defaultCacheLife } from './cache-life'
import type { CacheScopeStore } from '../async-storage/cache-scope.external'

const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'
Expand Down Expand Up @@ -172,6 +171,22 @@ function generateCacheEntryWithCacheContext(
encodedArguments: FormData | string,
fn: any
) {
if (!workStore.cacheLifeProfiles) {
throw new Error(
'cacheLifeProfiles should always be provided. This is a bug in Next.js.'
)
}
const defaultCacheLife = workStore.cacheLifeProfiles['default']
if (
!defaultCacheLife ||
defaultCacheLife.revalidate === undefined ||
defaultCacheLife.expire === undefined ||
defaultCacheLife.stale === undefined
) {
throw new Error(
'A default cacheLife profile must always be provided. This is a bug in Next.js.'
)
}
// Initialize the Store for this Cache entry.
const cacheStore: UseCacheStore = {
type: 'cache',
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/web/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ export async function adapter(
page: '/', // Fake Work
fallbackRouteParams: null,
renderOpts: {
cacheLifeProfiles:
params.request.nextConfig?.experimental?.cacheLife,
experimental: {
after: isAfterEnabled,
isRoutePPREnabled: false,
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/web/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface RequestData {
basePath?: string
i18n?: I18NConfig | null
trailingSlash?: boolean
experimental?: Pick<ExperimentalConfig, 'after'>
experimental?: Pick<ExperimentalConfig, 'after' | 'cacheLife'>
}
page?: {
name?: string
Expand Down

0 comments on commit a60fc42

Please sign in to comment.