Skip to content

Commit

Permalink
Make cacheLife profiles configurable (#71232)
Browse files Browse the repository at this point in the history
This should really be a global config but we don't have an easy way to
get the config from a module. Might need a loader. So this just plumbs
it through a bunch of contexts.

I'm also generate the enum type based on the config.
  • Loading branch information
sebmarkbage authored Oct 14, 2024
1 parent 300bfe5 commit fc2a7d2
Show file tree
Hide file tree
Showing 26 changed files with 263 additions and 25 deletions.
2 changes: 2 additions & 0 deletions packages/next/cache.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export {
revalidatePath,
} from 'next/dist/server/web/spec-extension/revalidate'
export { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store'
export { cacheLife as unstable_cacheLife } from 'next/dist/server/use-cache/cache-life'
export { cacheTag as unstable_cacheTag } from 'next/dist/server/use-cache/cache-tag'
7 changes: 5 additions & 2 deletions packages/next/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ const cacheExports = {
unstable_noStore:
require('next/dist/server/web/spec-extension/unstable-no-store')
.unstable_noStore,
unstable_cacheLife: require('next/dist/server/use-cache/cache-life'),
unstable_cacheTag: require('next/dist/server/use-cache/cache-tag'),
unstable_cacheLife: require('next/dist/server/use-cache/cache-life')
.cacheLife,
unstable_cacheTag: require('next/dist/server/use-cache/cache-tag').cacheTag,
}

// https://nodejs.org/api/esm.html#commonjs-namespaces
Expand All @@ -21,3 +22,5 @@ exports.unstable_cache = cacheExports.unstable_cache
exports.revalidatePath = cacheExports.revalidatePath
exports.revalidateTag = cacheExports.revalidateTag
exports.unstable_noStore = cacheExports.unstable_noStore
exports.unstable_cacheLife = cacheExports.unstable_cacheLife
exports.unstable_cacheTag = cacheExports.unstable_cacheTag
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/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1931,6 +1931,7 @@ export default async function getBaseWebpackConfig(
isEdgeServer,
pageExtensions: config.pageExtensions,
typedRoutes: enableTypedRoutes,
cacheLifeConfig: config.experimental.cacheLife,
originalRewrites,
originalRedirects,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('next-types-plugin', () => {
isEdgeServer: false,
pageExtensions: ['tsx', 'ts', 'jsx', 'js'],
typedRoutes: false,
cacheLifeConfig: undefined,
originalRewrites: undefined,
originalRedirects: undefined,
})
Expand Down Expand Up @@ -40,6 +41,7 @@ describe('next-types-plugin', () => {
isEdgeServer: false,
pageExtensions: ['tsx', 'ts', 'jsx', 'js'],
typedRoutes: false,
cacheLifeConfig: undefined,
originalRewrites: undefined,
originalRedirects: undefined,
})
Expand All @@ -60,6 +62,7 @@ describe('next-types-plugin', () => {
isEdgeServer: false,
pageExtensions: ['tsx', 'ts', 'jsx', 'js'],
typedRoutes: false,
cacheLifeConfig: undefined,
originalRewrites: undefined,
originalRedirects: undefined,
})
Expand Down
49 changes: 49 additions & 0 deletions packages/next/src/build/webpack/plugins/next-types-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { getPageFromPath } from '../../../entries'
import type { PageExtensions } from '../../../page-extensions-type'
import { devPageFiles } from './shared'
import { getProxiedPluginState } from '../../../build-context'
import type { CacheLife } from '../../../../server/use-cache/cache-life'

const PLUGIN_NAME = 'NextTypesPlugin'

Expand All @@ -34,6 +35,7 @@ interface Options {
isEdgeServer: boolean
pageExtensions: PageExtensions
typedRoutes: boolean
cacheLifeConfig: undefined | { [profile: string]: CacheLife }
originalRewrites: Rewrites | undefined
originalRedirects: Redirect[] | undefined
}
Expand Down Expand Up @@ -525,6 +527,40 @@ declare module 'next/form' {
`
}

function createCustomCacheLifeDefinitions(cacheLife: {
[profile: string]: CacheLife
}) {
const profiles = Object.keys(cacheLife)
if (!profiles.includes('default')) {
profiles.push('default')
}
for (let i = 0; i < profiles.length; i++) {
profiles[i] = JSON.stringify(profiles[i])
}

// TODO: Annotate each option with their expanded values for IDE support.
const profilesEnum = profiles.join(' | ')

// Redefine the cacheLife() accepted arguments.
return `// Type definitions for Next.js cacheLife configs
declare module 'next/cache' {
export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache'
export {
revalidateTag,
revalidatePath,
} from 'next/dist/server/web/spec-extension/revalidate'
export { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store'
import type { CacheLife } from 'next/dist/server/use-cache/cache-life'
export function unstable_cacheLife(profile: ${profilesEnum} | CacheLife): void
export { cacheTag as unstable_cacheTag } from 'next/dist/server/use-cache/cache-tag'
}
`
}

const appTypesBasePath = path.join('types', 'app')

export class NextTypesPlugin {
Expand All @@ -536,6 +572,7 @@ export class NextTypesPlugin {
pageExtensions: string[]
pagesDir: string
typedRoutes: boolean
cacheLifeConfig: undefined | { [profile: string]: CacheLife }
distDirAbsolutePath: string

constructor(options: Options) {
Expand All @@ -547,6 +584,7 @@ export class NextTypesPlugin {
this.pageExtensions = options.pageExtensions
this.pagesDir = path.join(this.appDir, '..', 'pages')
this.typedRoutes = options.typedRoutes
this.cacheLifeConfig = options.cacheLifeConfig
this.distDirAbsolutePath = path.join(this.dir, this.distDir)
if (this.typedRoutes && !redirectsRewritesTypesProcessed) {
redirectsRewritesTypesProcessed = true
Expand Down Expand Up @@ -780,6 +818,17 @@ export class NextTypesPlugin {
) as unknown as webpack.sources.RawSource
}

if (this.cacheLifeConfig) {
const cacheLifeAssetPath = path.join(
assetDirRelative,
'types/cache-life.d.ts'
)

assets[cacheLifeAssetPath] = new sources.RawSource(
createCustomCacheLifeDefinitions(this.cacheLifeConfig)
) as unknown as webpack.sources.RawSource
}

callback()
}
)
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 @@ -40,6 +40,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 @@ -79,6 +84,7 @@ export async function exportAppRoute(
waitUntil: afterRunner.context.waitUntil,
onClose: afterRunner.context.onClose,
onAfterTaskError: afterRunner.context.onTaskError,
cacheLifeProfiles,
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 @@ -581,6 +581,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 @@ -2513,6 +2514,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
21 changes: 21 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { SizeLimit } from '../types'
import type { ExpireTime } from './lib/revalidate'
import type { SupportedTestRunners } from '../cli/next-test'
import type { ExperimentalPPRConfig } from './lib/experimental/ppr'
import { INFINITE_CACHE } from '../lib/constants'

export type NextConfigComplete = Required<NextConfig> & {
images: Required<ImageConfigComplete>
Expand Down Expand Up @@ -258,6 +259,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 +1028,13 @@ export const defaultConfig: NextConfig = {
modularizeImports: undefined,
outputFileTracingRoot: process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT || '',
experimental: {
cacheLife: {
default: {
stale: undefined, // defaults to staleTimes.static
revalidate: 900,
expire: INFINITE_CACHE,
},
},
multiZoneDraftMode: false,
appNavFailHandling: Boolean(process.env.NEXT_PRIVATE_FLYING_SHUTTLE),
flyingShuttle: Boolean(process.env.NEXT_PRIVATE_FLYING_SHUTTLE)
Expand Down
29 changes: 29 additions & 0 deletions packages/next/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,35 @@ function assignDefaults(
}
}

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

const userProvidedModularizeImports = result.modularizeImports
// Unfortunately these packages end up re-exporting 10600 modules, for example: https://unpkg.com/browse/@mui/icons-material@5.11.16/esm/index.js.
// Leveraging modularizeImports tremendously reduces compile times for these.
Expand Down
Loading

0 comments on commit fc2a7d2

Please sign in to comment.