Skip to content

Commit

Permalink
Ensure global cache handlers are used properly (vercel#74626)
Browse files Browse the repository at this point in the history
Currently any cache handlers configured under the global symbol are
overridden by the default built-in cache handle unexpectedly. The
default built-in cache handler should only be used if no user configured
one is available and no global symbol provided one is available.

This also fixes `expireTags` not being called on configured cache
handlers at all due to that part not being wired up.

Regression tests for the above are also added to ensure this is behaving
as expected.
  • Loading branch information
ijjk authored Jan 8, 2025
1 parent 94bda49 commit 8ffa6c7
Show file tree
Hide file tree
Showing 20 changed files with 288 additions and 75 deletions.
44 changes: 13 additions & 31 deletions packages/next/src/build/static-paths/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@ import type { ParamValue, Params } from '../../server/request/params'
import type { AppPageModule } from '../../server/route-modules/app-page/module'
import type { AppSegment } from '../segment-config/app/app-segments'
import type { StaticPathsResult } from './types'
import type { CacheHandler } from '../../server/lib/incremental-cache'

import path from 'path'
import { AfterRunner } from '../../server/after/run-with-after'
import { createWorkStore } from '../../server/async-storage/work-store'
import { FallbackMode } from '../../lib/fallback'
import { formatDynamicImportPath } from '../../lib/format-dynamic-import-path'
import { getRouteMatcher } from '../../shared/lib/router/utils/route-matcher'
import {
getRouteRegex,
type RouteRegex,
} from '../../shared/lib/router/utils/route-regex'
import { IncrementalCache } from '../../server/lib/incremental-cache'
import { interopDefault } from '../../lib/interop-default'
import { nodeFs } from '../../server/lib/node-fs-methods'
import type { IncrementalCache } from '../../server/lib/incremental-cache'
import { normalizePathname, encodeParam } from './utils'
import * as ciEnvironment from '../../server/ci-info'
import escapePathDelimiters from '../../shared/lib/router/utils/escape-path-delimiters'
import { createIncrementalCache } from '../../export/helpers/create-incremental-cache'
import type { NextConfigComplete } from '../../server/config-shared'

/**
* Compares two parameters to see if they're equal.
Expand Down Expand Up @@ -263,6 +260,7 @@ export async function buildAppStaticPaths({
cacheHandler,
cacheLifeProfiles,
requestHeaders,
cacheHandlers,
maxMemoryCacheSize,
fetchCacheKeyPrefix,
nextConfigOutput,
Expand All @@ -280,6 +278,7 @@ export async function buildAppStaticPaths({
isrFlushToDisk?: boolean
fetchCacheKeyPrefix?: string
cacheHandler?: string
cacheHandlers?: NextConfigComplete['experimental']['cacheHandlers']
cacheLifeProfiles?: {
[profile: string]: import('../../server/use-cache/cache-life').CacheLife
}
Expand All @@ -302,33 +301,16 @@ export async function buildAppStaticPaths({

ComponentMod.patchFetch()

let CurCacheHandler: typeof CacheHandler | undefined
if (cacheHandler) {
CurCacheHandler = interopDefault(
await import(formatDynamicImportPath(dir, cacheHandler)).then(
(mod) => mod.default || mod
)
)
}

const incrementalCache = new IncrementalCache({
fs: nodeFs,
dev: true,
const incrementalCache = await createIncrementalCache({
dir,
distDir,
dynamicIO,
flushToDisk: isrFlushToDisk,
serverDistDir: path.join(distDir, 'server'),
fetchCacheKeyPrefix,
maxMemoryCacheSize,
getPrerenderManifest: () => ({
version: -1 as any, // letting us know this doesn't conform to spec
routes: {},
dynamicRoutes: {},
notFoundRoutes: [],
preview: null as any, // `preview` is special case read in next-dev-server
}),
CurCacheHandler,
cacheHandler,
cacheHandlers,
requestHeaders,
minimalMode: ciEnvironment.hasNextSupport,
fetchCacheKeyPrefix,
flushToDisk: isrFlushToDisk,
cacheMaxMemorySize: maxMemoryCacheSize,
})

const regex = getRouteRegex(page)
Expand Down
14 changes: 12 additions & 2 deletions packages/next/src/build/templates/edge-ssr-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,24 @@ import type { NextConfigComplete } from '../../server/config-shared'
import { PAGE_TYPES } from '../../lib/page-types'
import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
import { createServerModuleMap } from '../../server/app-render/action-utils'
import {
cacheHandlerGlobal,
cacheHandlersSymbol,
} from '../../server/use-cache/constants'

declare const incrementalCacheHandler: any
// OPTIONAL_IMPORT:incrementalCacheHandler

const cacheHandlers = {}

if (!(globalThis as any).__nextCacheHandlers) {
;(globalThis as any).__nextCacheHandlers = cacheHandlers
if (!cacheHandlerGlobal.__nextCacheHandlers) {
cacheHandlerGlobal.__nextCacheHandlers = cacheHandlers

if (!cacheHandlerGlobal.__nextCacheHandlers.default) {
cacheHandlerGlobal.__nextCacheHandlers.default =
cacheHandlerGlobal[cacheHandlersSymbol]?.DefaultCache ||
cacheHandlerGlobal.__nextCacheHandlers.__nextDefault
}
}

const Document: DocumentType = null!
Expand Down
14 changes: 12 additions & 2 deletions packages/next/src/build/templates/edge-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import type { RequestData } from '../../server/web/types'
import type { BuildManifest } from '../../server/get-page-files'
import type { NextConfigComplete } from '../../server/config-shared'
import type { PAGE_TYPES } from '../../lib/page-types'
import {
cacheHandlerGlobal,
cacheHandlersSymbol,
} from '../../server/use-cache/constants'

// injected by the loader afterwards.
declare const pagesType: PAGE_TYPES
Expand All @@ -42,8 +46,14 @@ declare const user500RouteModuleOptions: any

const cacheHandlers = {}

if (!(globalThis as any).__nextCacheHandlers) {
;(globalThis as any).__nextCacheHandlers = cacheHandlers
if (!cacheHandlerGlobal.__nextCacheHandlers) {
cacheHandlerGlobal.__nextCacheHandlers = cacheHandlers

if (!cacheHandlerGlobal.__nextCacheHandlers.default) {
cacheHandlerGlobal.__nextCacheHandlers.default =
cacheHandlerGlobal[cacheHandlersSymbol]?.DefaultCache ||
cacheHandlerGlobal.__nextCacheHandlers.__nextDefault
}
}

const pageMod = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction<EdgeAppRouteLoaderQue
const cacheHandlers = JSON.parse(cacheHandlersStringified || '{}')

if (!cacheHandlers.default) {
cacheHandlers.default = require.resolve(
cacheHandlers.__nextDefault = require.resolve(
'../../../../server/lib/cache-handlers/default'
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
const cacheHandlers = JSON.parse(cacheHandlersStringified || '{}')

if (!cacheHandlers.default) {
cacheHandlers.default = require.resolve(
cacheHandlers.__nextDefault = require.resolve(
'../../../../server/lib/cache-handlers/default'
)
}
Expand Down
14 changes: 11 additions & 3 deletions packages/next/src/export/helpers/create-incremental-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { hasNextSupport } from '../../server/ci-info'
import { nodeFs } from '../../server/lib/node-fs-methods'
import { interopDefault } from '../../lib/interop-default'
import { formatDynamicImportPath } from '../../lib/format-dynamic-import-path'
import { cacheHandlerGlobal } from '../../server/use-cache/constants'
import DefaultCacheHandler from '../../server/lib/cache-handlers/default'

export async function createIncrementalCache({
cacheHandler,
Expand All @@ -14,6 +16,7 @@ export async function createIncrementalCache({
dir,
flushToDisk,
cacheHandlers,
requestHeaders,
}: {
dynamicIO: boolean
cacheHandler?: string
Expand All @@ -22,6 +25,7 @@ export async function createIncrementalCache({
distDir: string
dir: string
flushToDisk?: boolean
requestHeaders?: Record<string, string | string[] | undefined>
cacheHandlers?: Record<string, string | undefined>
}) {
// Custom cache handler overrides.
Expand All @@ -34,8 +38,8 @@ export async function createIncrementalCache({
)
}

if (!(globalThis as any).__nextCacheHandlers && cacheHandlers) {
;(globalThis as any).__nextCacheHandlers = {}
if (!cacheHandlerGlobal.__nextCacheHandlers && cacheHandlers) {
cacheHandlerGlobal.__nextCacheHandlers = {}

for (const key of Object.keys(cacheHandlers)) {
if (cacheHandlers[key]) {
Expand All @@ -46,11 +50,15 @@ export async function createIncrementalCache({
)
}
}

if (!cacheHandlers.default) {
cacheHandlerGlobal.__nextCacheHandlers.default = DefaultCacheHandler
}
}

const incrementalCache = new IncrementalCache({
dev: false,
requestHeaders: {},
requestHeaders: requestHeaders || {},
flushToDisk,
dynamicIO,
maxMemoryCacheSize: cacheMaxMemorySize,
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1359,7 +1359,7 @@ export default abstract class Server<
) || []

for (const handler of Object.values(_globalThis.__nextCacheHandlers)) {
if (typeof handler.receiveExpiredTags === 'function') {
if (typeof handler?.receiveExpiredTags === 'function') {
await handler.receiveExpiredTags(...expiredTags)
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ export default class DevServer extends Server {
isAppPath,
requestHeaders,
cacheHandler: this.nextConfig.cacheHandler,
cacheHandlers: this.nextConfig.experimental.cacheHandlers,
cacheLifeProfiles: this.nextConfig.experimental.cacheLife,
fetchCacheKeyPrefix: this.nextConfig.experimental.fetchCacheKeyPrefix,
isrFlushToDisk: this.nextConfig.experimental.isrFlushToDisk,
Expand Down
17 changes: 17 additions & 0 deletions packages/next/src/server/dev/static-paths-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { InvariantError } from '../../shared/lib/invariant-error'
import { collectRootParamKeys } from '../../build/segment-config/app/collect-root-param-keys'
import { buildAppStaticPaths } from '../../build/static-paths/app'
import { buildPagesStaticPaths } from '../../build/static-paths/pages'
import { createIncrementalCache } from '../../export/helpers/create-incremental-cache'

type RuntimeConfig = {
pprConfig: ExperimentalPPRConfig | undefined
Expand Down Expand Up @@ -45,6 +46,7 @@ export async function loadStaticPaths({
maxMemoryCacheSize,
requestHeaders,
cacheHandler,
cacheHandlers,
cacheLifeProfiles,
nextConfigOutput,
buildId,
Expand All @@ -65,6 +67,7 @@ export async function loadStaticPaths({
maxMemoryCacheSize?: number
requestHeaders: IncrementalCache['requestHeaders']
cacheHandler?: string
cacheHandlers?: NextConfigComplete['experimental']['cacheHandlers']
cacheLifeProfiles?: {
[profile: string]: import('../../server/use-cache/cache-life').CacheLife
}
Expand All @@ -73,6 +76,20 @@ export async function loadStaticPaths({
authInterrupts: boolean
sriEnabled: boolean
}): Promise<Partial<StaticPathsResult>> {
// this needs to be initialized before loadComponents otherwise
// "use cache" could be missing it's cache handlers
await createIncrementalCache({
dir,
distDir,
dynamicIO: false,
cacheHandler,
cacheHandlers,
requestHeaders,
fetchCacheKeyPrefix,
flushToDisk: isrFlushToDisk,
cacheMaxMemorySize: maxMemoryCacheSize,
})

// update work memory runtime-config
require('../../shared/lib/runtime-config.external').setConfig(config)
setHttpClientAndAgentOptions({
Expand Down
7 changes: 5 additions & 2 deletions packages/next/src/server/lib/cache-handlers/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const DefaultCacheHandler: CacheHandler = {
}
},

async unstable_expireTags(...tags) {
async expireTags(...tags) {
for (const tag of tags) {
if (!tagsManifest.items[tag]) {
tagsManifest.items[tag] = {}
Expand All @@ -113,8 +113,11 @@ const DefaultCacheHandler: CacheHandler = {
}
},

// This is only meant to invalidate in memory tags
// not meant to be propagated like expireTags would
// in multi-instance scenario
async receiveExpiredTags(...tags): Promise<void> {
return this.unstable_expireTags(...tags)
return this.expireTags(...tags)
},
}

Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/server/lib/cache-handlers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ export interface CacheHandler {

set(cacheKey: string, entry: Promise<CacheEntry>): Promise<void>

// This is called when unstable_expireTags('') is called
// This is called when expireTags('') is called
// and should update tags manifest accordingly
unstable_expireTags(...tags: string[]): Promise<void>
expireTags(...tags: string[]): Promise<void>

// This is called when an action request sends
// NEXT_CACHE_REVALIDATED_TAGS_HEADER and tells
Expand Down
17 changes: 16 additions & 1 deletion packages/next/src/server/lib/incremental-cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,22 @@ export class IncrementalCache implements IncrementalCacheType {
}

async revalidateTag(tags: string | string[]): Promise<void> {
return this.cacheHandler?.revalidateTag?.(tags)
const _globalThis: typeof globalThis & {
__nextCacheHandlers?: Record<
string,
import('../cache-handlers/types').CacheHandler
>
} = globalThis

return Promise.all([
// call expireTags on all configured cache handlers
Object.values(_globalThis.__nextCacheHandlers || {}).map(
(cacheHandler) =>
typeof cacheHandler.expireTags === 'function' &&
cacheHandler.expireTags(...(Array.isArray(tags) ? tags : [tags]))
),
this.cacheHandler?.revalidateTag?.(tags),
]).then(() => {})
}

// x-ref: https://github.com/facebook/react/blob/2655c9354d8e1c54ba888444220f63e836925caa/packages/react/src/ReactFetch.js#L23
Expand Down
9 changes: 6 additions & 3 deletions packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import { InvariantError } from '../shared/lib/invariant-error'
import { AwaiterOnce } from './after/awaiter'
import { AsyncCallbackSet } from './lib/async-callback-set'
import DefaultCacheHandler from './lib/cache-handlers/default'
import { cacheHandlerGlobal, cacheHandlersSymbol } from './use-cache/constants'

export * from './base-server'

Expand Down Expand Up @@ -379,8 +380,8 @@ export default class NextNodeServer extends BaseServer<
protected async loadCustomCacheHandlers() {
const { cacheHandlers } = this.nextConfig.experimental

if (!(globalThis as any).__nextCacheHandlers && cacheHandlers) {
;(globalThis as any).__nextCacheHandlers = {}
if (!cacheHandlerGlobal.__nextCacheHandlers && cacheHandlers) {
cacheHandlerGlobal.__nextCacheHandlers = {}

for (const key of Object.keys(cacheHandlers)) {
if (cacheHandlers[key]) {
Expand All @@ -393,7 +394,9 @@ export default class NextNodeServer extends BaseServer<
}

if (!cacheHandlers.default) {
;(globalThis as any).__nextCacheHandlers.default = DefaultCacheHandler
cacheHandlerGlobal.__nextCacheHandlers.default =
cacheHandlerGlobal[cacheHandlersSymbol]?.DefaultCache ||
DefaultCacheHandler
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions packages/next/src/server/use-cache/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { CacheHandler } from '../lib/cache-handlers/types'

// If the expire time is less than .
export const DYNAMIC_EXPIRE = 300

export const cacheHandlersSymbol = Symbol.for('@next/cache-handlers')
export const cacheHandlerGlobal: typeof globalThis & {
[cacheHandlersSymbol]?: {
RemoteCache?: CacheHandler
DefaultCache?: CacheHandler
}
__nextCacheHandlers?: Record<string, CacheHandler>
} = globalThis
Loading

0 comments on commit 8ffa6c7

Please sign in to comment.