From 8ffa6c74b1f3fe357ce25bb455a565c6327dbd1e Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 7 Jan 2025 22:51:30 -0800 Subject: [PATCH] Ensure global cache handlers are used properly (#74626) 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. --- packages/next/src/build/static-paths/app.ts | 44 +++---- .../next/src/build/templates/edge-ssr-app.ts | 14 ++- packages/next/src/build/templates/edge-ssr.ts | 14 ++- .../next-edge-app-route-loader/index.ts | 2 +- .../loaders/next-edge-ssr-loader/index.ts | 2 +- .../helpers/create-incremental-cache.ts | 14 ++- packages/next/src/server/base-server.ts | 2 +- .../next/src/server/dev/next-dev-server.ts | 1 + .../src/server/dev/static-paths-worker.ts | 17 +++ .../src/server/lib/cache-handlers/default.ts | 7 +- .../src/server/lib/cache-handlers/types.ts | 4 +- .../src/server/lib/incremental-cache/index.ts | 17 ++- packages/next/src/server/next-server.ts | 9 +- .../next/src/server/use-cache/constants.ts | 13 +++ .../src/server/use-cache/use-cache-wrapper.ts | 29 +---- .../dynamic-io-cache-handlers/app/layout.tsx | 8 ++ .../dynamic-io-cache-handlers/app/page.tsx | 36 ++++++ .../app/revalidate-tag/route.ts | 10 ++ .../dynamic-io-cache-handlers.test.ts | 109 ++++++++++++++++++ .../dynamic-io-cache-handlers/next.config.js | 11 ++ 20 files changed, 288 insertions(+), 75 deletions(-) create mode 100644 packages/next/src/server/use-cache/constants.ts create mode 100644 test/production/app-dir/dynamic-io-cache-handlers/app/layout.tsx create mode 100644 test/production/app-dir/dynamic-io-cache-handlers/app/page.tsx create mode 100644 test/production/app-dir/dynamic-io-cache-handlers/app/revalidate-tag/route.ts create mode 100644 test/production/app-dir/dynamic-io-cache-handlers/dynamic-io-cache-handlers.test.ts create mode 100644 test/production/app-dir/dynamic-io-cache-handlers/next.config.js diff --git a/packages/next/src/build/static-paths/app.ts b/packages/next/src/build/static-paths/app.ts index ca8128b4fc51e..125f87ff1d71b 100644 --- a/packages/next/src/build/static-paths/app.ts +++ b/packages/next/src/build/static-paths/app.ts @@ -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. @@ -263,6 +260,7 @@ export async function buildAppStaticPaths({ cacheHandler, cacheLifeProfiles, requestHeaders, + cacheHandlers, maxMemoryCacheSize, fetchCacheKeyPrefix, nextConfigOutput, @@ -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 } @@ -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) diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts index c30af2f3cbec7..3b4f34167dc2e 100644 --- a/packages/next/src/build/templates/edge-ssr-app.ts +++ b/packages/next/src/build/templates/edge-ssr-app.ts @@ -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! diff --git a/packages/next/src/build/templates/edge-ssr.ts b/packages/next/src/build/templates/edge-ssr.ts index 903a6cbef8da6..7b36da0fcc5fe 100644 --- a/packages/next/src/build/templates/edge-ssr.ts +++ b/packages/next/src/build/templates/edge-ssr.ts @@ -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 @@ -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 = { diff --git a/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts b/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts index 8c589541480ad..8cc84be52a3cc 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts @@ -36,7 +36,7 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction = const cacheHandlers = JSON.parse(cacheHandlersStringified || '{}') if (!cacheHandlers.default) { - cacheHandlers.default = require.resolve( + cacheHandlers.__nextDefault = require.resolve( '../../../../server/lib/cache-handlers/default' ) } diff --git a/packages/next/src/export/helpers/create-incremental-cache.ts b/packages/next/src/export/helpers/create-incremental-cache.ts index 8e670de43122d..63c68da6cf4bb 100644 --- a/packages/next/src/export/helpers/create-incremental-cache.ts +++ b/packages/next/src/export/helpers/create-incremental-cache.ts @@ -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, @@ -14,6 +16,7 @@ export async function createIncrementalCache({ dir, flushToDisk, cacheHandlers, + requestHeaders, }: { dynamicIO: boolean cacheHandler?: string @@ -22,6 +25,7 @@ export async function createIncrementalCache({ distDir: string dir: string flushToDisk?: boolean + requestHeaders?: Record cacheHandlers?: Record }) { // Custom cache handler overrides. @@ -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]) { @@ -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, diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index d31c636a603d3..0b804b8aefbce 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -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) } } diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index fa7c36f4076d6..f910777d7ed87 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -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, diff --git a/packages/next/src/server/dev/static-paths-worker.ts b/packages/next/src/server/dev/static-paths-worker.ts index 85f6d8d4d9211..b1b5f3f6f33fd 100644 --- a/packages/next/src/server/dev/static-paths-worker.ts +++ b/packages/next/src/server/dev/static-paths-worker.ts @@ -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 @@ -45,6 +46,7 @@ export async function loadStaticPaths({ maxMemoryCacheSize, requestHeaders, cacheHandler, + cacheHandlers, cacheLifeProfiles, nextConfigOutput, buildId, @@ -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 } @@ -73,6 +76,20 @@ export async function loadStaticPaths({ authInterrupts: boolean sriEnabled: boolean }): Promise> { + // 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({ diff --git a/packages/next/src/server/lib/cache-handlers/default.ts b/packages/next/src/server/lib/cache-handlers/default.ts index e78c0f1a5f997..5c8f6c3cf8065 100644 --- a/packages/next/src/server/lib/cache-handlers/default.ts +++ b/packages/next/src/server/lib/cache-handlers/default.ts @@ -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] = {} @@ -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 { - return this.unstable_expireTags(...tags) + return this.expireTags(...tags) }, } diff --git a/packages/next/src/server/lib/cache-handlers/types.ts b/packages/next/src/server/lib/cache-handlers/types.ts index 1b1c651413bda..48ba359f2bc27 100644 --- a/packages/next/src/server/lib/cache-handlers/types.ts +++ b/packages/next/src/server/lib/cache-handlers/types.ts @@ -44,9 +44,9 @@ export interface CacheHandler { set(cacheKey: string, entry: Promise): Promise - // 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 + expireTags(...tags: string[]): Promise // This is called when an action request sends // NEXT_CACHE_REVALIDATED_TAGS_HEADER and tells diff --git a/packages/next/src/server/lib/incremental-cache/index.ts b/packages/next/src/server/lib/incremental-cache/index.ts index f79930a3ef822..04e2bea59a130 100644 --- a/packages/next/src/server/lib/incremental-cache/index.ts +++ b/packages/next/src/server/lib/incremental-cache/index.ts @@ -249,7 +249,22 @@ export class IncrementalCache implements IncrementalCacheType { } async revalidateTag(tags: string | string[]): Promise { - 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 diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 791a3934d960b..e25b66f80c93e 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -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' @@ -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]) { @@ -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 } } } diff --git a/packages/next/src/server/use-cache/constants.ts b/packages/next/src/server/use-cache/constants.ts new file mode 100644 index 0000000000000..aa216f29017d1 --- /dev/null +++ b/packages/next/src/server/use-cache/constants.ts @@ -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 +} = globalThis diff --git a/packages/next/src/server/use-cache/use-cache-wrapper.ts b/packages/next/src/server/use-cache/use-cache-wrapper.ts index e500bbb0c90aa..6768e222a08d2 100644 --- a/packages/next/src/server/use-cache/use-cache-wrapper.ts +++ b/packages/next/src/server/use-cache/use-cache-wrapper.ts @@ -33,39 +33,16 @@ import { getClientReferenceManifestForRsc, getServerModuleMap, } from '../app-render/encryption-utils' -import DefaultCacheHandler from '../lib/cache-handlers/default' import type { CacheHandler, CacheEntry } from '../lib/cache-handlers/types' import type { CacheSignal } from '../app-render/cache-signal' import { decryptActionBoundArgs } from '../app-render/encryption' import { InvariantError } from '../../shared/lib/invariant-error' import { getDigestForWellKnownError } from '../app-render/create-error-handler' +import { cacheHandlerGlobal, DYNAMIC_EXPIRE } from './constants' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' -// If the expire time is less than . -const DYNAMIC_EXPIRE = 300 - -const cacheHandlersSymbol = Symbol.for('@next/cache-handlers') -const _globalThis: typeof globalThis & { - [cacheHandlersSymbol]?: { - RemoteCache?: CacheHandler - DefaultCache?: CacheHandler - } - __nextCacheHandlers?: Record -} = globalThis - -const cacheHandlerMap: Map = new Map([ - [ - 'default', - _globalThis[cacheHandlersSymbol]?.DefaultCache || DefaultCacheHandler, - ], - [ - 'remote', - // in dev remote maps to default handler - // and is meant to be overridden in prod - _globalThis[cacheHandlersSymbol]?.RemoteCache || DefaultCacheHandler, - ], -]) +const cacheHandlerMap: Map = new Map() function generateCacheEntry( workStore: WorkStore, @@ -455,7 +432,7 @@ export function cache( fn: any ) { for (const [key, value] of Object.entries( - _globalThis.__nextCacheHandlers || {} + cacheHandlerGlobal.__nextCacheHandlers || {} )) { cacheHandlerMap.set(key, value as CacheHandler) } diff --git a/test/production/app-dir/dynamic-io-cache-handlers/app/layout.tsx b/test/production/app-dir/dynamic-io-cache-handlers/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/production/app-dir/dynamic-io-cache-handlers/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/production/app-dir/dynamic-io-cache-handlers/app/page.tsx b/test/production/app-dir/dynamic-io-cache-handlers/app/page.tsx new file mode 100644 index 0000000000000..ebbb4eb578350 --- /dev/null +++ b/test/production/app-dir/dynamic-io-cache-handlers/app/page.tsx @@ -0,0 +1,36 @@ +import React, { Suspense } from 'react' + +async function Random({ cached }: { cached?: boolean }) { + const data = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random' + ).then((res) => res.text()) + + return ( + <> +

now: {Date.now()}

+

+ {cached ? 'cached ' : ''}random: {data} +

+ + ) +} + +async function CachedRandom() { + 'use cache' + + return +} + +export default function Page() { + return ( + <> +

index page

+ Loading...

}> + +
+ Loading...

}> + +
+ + ) +} diff --git a/test/production/app-dir/dynamic-io-cache-handlers/app/revalidate-tag/route.ts b/test/production/app-dir/dynamic-io-cache-handlers/app/revalidate-tag/route.ts new file mode 100644 index 0000000000000..399578325a3d0 --- /dev/null +++ b/test/production/app-dir/dynamic-io-cache-handlers/app/revalidate-tag/route.ts @@ -0,0 +1,10 @@ +import { revalidateTag } from 'next/cache' +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(req: NextRequest) { + console.log(req.url.toString()) + + revalidateTag(req.nextUrl.searchParams.get('tag') || '') + + return NextResponse.json({ success: true }) +} diff --git a/test/production/app-dir/dynamic-io-cache-handlers/dynamic-io-cache-handlers.test.ts b/test/production/app-dir/dynamic-io-cache-handlers/dynamic-io-cache-handlers.test.ts new file mode 100644 index 0000000000000..7c7ba0b67740f --- /dev/null +++ b/test/production/app-dir/dynamic-io-cache-handlers/dynamic-io-cache-handlers.test.ts @@ -0,0 +1,109 @@ +import path from 'path' +import { createNext, FileRef, NextInstance } from 'e2e-utils' +import { + fetchViaHTTP, + findPort, + initNextServerScript, + killApp, + retry, +} from 'next-test-utils' + +describe('dynamic-io-cache-handlers', () => { + let appPort: number + let server: any + let output = '' + let next: NextInstance + + if (process.env.__NEXT_EXPERIMENTAL_PPR) { + return it('should skip', () => {}) + } + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(__dirname), + skipStart: true, + }) + await next.build() + + const standaloneServer = '.next/standalone/server.js' + await next.patchFile( + standaloneServer, + ` + globalThis[Symbol.for('@next/cache-handlers')] = { + DefaultCache: { + get(cacheKey, softTags) { + console.log('symbol get', cacheKey, softTags) + }, + + set(cacheKey, entry) { + console.log('symbol set', cacheKey) + }, + + expireTags(...tags) { + console.log('symbol expireTags', tags) + }, + + receiveExpiredTags(...tags) { + console.log('symbol receiveExpiredTags', tags) + } + } + } + ${await next.readFile(standaloneServer)}` + ) + + appPort = await findPort() + + require('console').error( + 'starting standalone mode', + path.join(next.testDir, standaloneServer) + ) + + server = await initNextServerScript( + path.join(next.testDir, standaloneServer), + /- Local:/, + { + ...process.env, + PORT: `${appPort}`, + }, + undefined, + { + cwd: next.testDir, + shouldRejectOnError: true, + onStdout(data) { + output += data + require('console').log(data) + }, + onStderr(data) { + output += data + require('console').log(data) + }, + } + ) + }) + afterAll(async () => { + await next.destroy() + await killApp(server) + }) + + it('should use global symbol for default cache handler', async () => { + const res = await fetchViaHTTP(appPort, '/') + expect(res.status).toBe(200) + + await retry(() => { + expect(output).toContain('symbol receiveExpiredTags') + expect(output).toContain('symbol get') + expect(output).toContain('symbol set') + }) + }) + + it('should call expireTags on global default cache handler', async () => { + const res = await fetchViaHTTP(appPort, '/revalidate-tag', { tag: 'tag1' }) + expect(res.status).toBe(200) + + await retry(() => { + expect(output).toContain('symbol receiveExpiredTags') + expect(output).toContain('symbol expireTags') + expect(output).toContain('tag1') + }) + }) +}) diff --git a/test/production/app-dir/dynamic-io-cache-handlers/next.config.js b/test/production/app-dir/dynamic-io-cache-handlers/next.config.js new file mode 100644 index 0000000000000..1dcb709cd9481 --- /dev/null +++ b/test/production/app-dir/dynamic-io-cache-handlers/next.config.js @@ -0,0 +1,11 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + output: 'standalone', + experimental: { + dynamicIO: true, + }, +} + +module.exports = nextConfig