From 086e98b7d3b57c496649501d24634d1b1f02a66e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 16 Jun 2025 15:43:01 -0500 Subject: [PATCH 1/3] fix: mark the shared cache controls as external --- packages/next/src/server/lib/incremental-cache/index.ts | 2 +- ...-controls.test.ts => shared-cache-controls.external.test.ts} | 2 +- ...ared-cache-controls.ts => shared-cache-controls.external.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/next/src/server/lib/incremental-cache/{shared-cache-controls.test.ts => shared-cache-controls.external.test.ts} (97%) rename packages/next/src/server/lib/incremental-cache/{shared-cache-controls.ts => shared-cache-controls.external.ts} (100%) diff --git a/packages/next/src/server/lib/incremental-cache/index.ts b/packages/next/src/server/lib/incremental-cache/index.ts index 4bf18ed0d221..2794b4c0bf19 100644 --- a/packages/next/src/server/lib/incremental-cache/index.ts +++ b/packages/next/src/server/lib/incremental-cache/index.ts @@ -24,7 +24,7 @@ import { PRERENDER_REVALIDATE_HEADER, } from '../../../lib/constants' import { toRoute } from '../to-route' -import { SharedCacheControls } from './shared-cache-controls' +import { SharedCacheControls } from './shared-cache-controls.external' import { getPrerenderResumeDataCache, getRenderResumeDataCache, diff --git a/packages/next/src/server/lib/incremental-cache/shared-cache-controls.test.ts b/packages/next/src/server/lib/incremental-cache/shared-cache-controls.external.test.ts similarity index 97% rename from packages/next/src/server/lib/incremental-cache/shared-cache-controls.test.ts rename to packages/next/src/server/lib/incremental-cache/shared-cache-controls.external.test.ts index e6935165c61c..02c428cbf111 100644 --- a/packages/next/src/server/lib/incremental-cache/shared-cache-controls.test.ts +++ b/packages/next/src/server/lib/incremental-cache/shared-cache-controls.external.test.ts @@ -3,7 +3,7 @@ import type { PrerenderManifestRoute, } from '../../../build' import { RenderingMode } from '../../../build/rendering-mode' -import { SharedCacheControls } from './shared-cache-controls' +import { SharedCacheControls } from './shared-cache-controls.external' describe('SharedCacheControls', () => { let sharedCacheControls: SharedCacheControls diff --git a/packages/next/src/server/lib/incremental-cache/shared-cache-controls.ts b/packages/next/src/server/lib/incremental-cache/shared-cache-controls.external.ts similarity index 100% rename from packages/next/src/server/lib/incremental-cache/shared-cache-controls.ts rename to packages/next/src/server/lib/incremental-cache/shared-cache-controls.external.ts From 7f5f84498ebb9ab249937b7d553d2634359f9c17 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 17 Jun 2025 11:44:55 -0600 Subject: [PATCH 2/3] refactor: moved default cache handler to external --- .../next-edge-app-route-loader/index.ts | 2 +- .../loaders/next-edge-ssr-loader/index.ts | 2 +- .../lib/cache-handlers/default.external.ts | 162 +++++++++++++++++ .../src/server/lib/cache-handlers/default.ts | 164 +----------------- .../next/src/server/use-cache/handlers.ts | 2 +- 5 files changed, 167 insertions(+), 165 deletions(-) create mode 100644 packages/next/src/server/lib/cache-handlers/default.external.ts 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 59b68af893fc..88674f775ab0 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 @@ -37,7 +37,7 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction = if (!cacheHandlers.default) { cacheHandlers.default = require.resolve( - '../../../../server/lib/cache-handlers/default' + '../../../../server/lib/cache-handlers/default.external' ) } diff --git a/packages/next/src/server/lib/cache-handlers/default.external.ts b/packages/next/src/server/lib/cache-handlers/default.external.ts new file mode 100644 index 000000000000..5e6044207071 --- /dev/null +++ b/packages/next/src/server/lib/cache-handlers/default.external.ts @@ -0,0 +1,162 @@ +/** + * This is the default "use cache" handler it defaults to an in-memory store. + * In-memory caches are fragile and should not use stale-while-revalidate + * semantics on the caches because it's not worth warming up an entry that's + * likely going to get evicted before we get to use it anyway. However, we also + * don't want to reuse a stale entry for too long so stale entries should be + * considered expired/missing in such cache handlers. + */ + +import { LRUCache } from '../lru-cache' +import type { CacheEntry, CacheHandlerV2 } from './types' +import { + isStale, + tagsManifest, +} from '../incremental-cache/tags-manifest.external' + +type PrivateCacheEntry = { + entry: CacheEntry + + // For the default cache we store errored cache + // entries and allow them to be used up to 3 times + // after that we want to dispose it and try for fresh + + // If an entry is errored we return no entry + // three times so that we retry hitting origin (MISS) + // and then if it still fails to set after the third we + // return the errored content and use expiration of + // Math.min(30, entry.expiration) + isErrored: boolean + errorRetryCount: number + + // compute size on set since we need to read size + // of the ReadableStream for LRU evicting + size: number +} + +// LRU cache default to max 50 MB but in future track +const memoryCache = new LRUCache( + 50 * 1024 * 1024, + (entry) => entry.size +) +const pendingSets = new Map>() + +const debug = process.env.NEXT_PRIVATE_DEBUG_CACHE + ? console.debug.bind(console, 'DefaultCacheHandler:') + : undefined + +const DefaultCacheHandler: CacheHandlerV2 = { + async get(cacheKey) { + const pendingPromise = pendingSets.get(cacheKey) + + if (pendingPromise) { + debug?.('get', cacheKey, 'pending') + await pendingPromise + } + + const privateEntry = memoryCache.get(cacheKey) + + if (!privateEntry) { + debug?.('get', cacheKey, 'not found') + return undefined + } + + const entry = privateEntry.entry + if ( + performance.timeOrigin + performance.now() > + entry.timestamp + entry.revalidate * 1000 + ) { + // In-memory caches should expire after revalidate time because it is + // unlikely that a new entry will be able to be used before it is dropped + // from the cache. + debug?.('get', cacheKey, 'expired') + + return undefined + } + + if (isStale(entry.tags, entry.timestamp)) { + debug?.('get', cacheKey, 'had stale tag') + + return undefined + } + const [returnStream, newSaved] = entry.value.tee() + entry.value = newSaved + + debug?.('get', cacheKey, 'found', { + tags: entry.tags, + timestamp: entry.timestamp, + revalidate: entry.revalidate, + expire: entry.expire, + }) + + return { + ...entry, + value: returnStream, + } + }, + + async set(cacheKey, pendingEntry) { + debug?.('set', cacheKey, 'start') + + let resolvePending: () => void = () => {} + const pendingPromise = new Promise((resolve) => { + resolvePending = resolve + }) + pendingSets.set(cacheKey, pendingPromise) + + const entry = await pendingEntry + + let size = 0 + + try { + const [value, clonedValue] = entry.value.tee() + entry.value = value + const reader = clonedValue.getReader() + + for (let chunk; !(chunk = await reader.read()).done; ) { + size += Buffer.from(chunk.value).byteLength + } + + memoryCache.set(cacheKey, { + entry, + isErrored: false, + errorRetryCount: 0, + size, + }) + + debug?.('set', cacheKey, 'done') + } catch (err) { + // TODO: store partial buffer with error after we retry 3 times + debug?.('set', cacheKey, 'failed', err) + } finally { + resolvePending() + pendingSets.delete(cacheKey) + } + }, + + async refreshTags() { + // Nothing to do for an in-memory cache handler. + }, + + async getExpiration(...tags) { + const expiration = Math.max( + ...tags.map((tag) => tagsManifest.get(tag) ?? 0) + ) + + debug?.('getExpiration', { tags, expiration }) + + return expiration + }, + + async expireTags(...tags) { + const timestamp = Math.round(performance.timeOrigin + performance.now()) + debug?.('expireTags', { tags, timestamp }) + + for (const tag of tags) { + // TODO: update file-system-cache? + tagsManifest.set(tag, timestamp) + } + }, +} + +export default DefaultCacheHandler diff --git a/packages/next/src/server/lib/cache-handlers/default.ts b/packages/next/src/server/lib/cache-handlers/default.ts index 5e6044207071..f6b93f85e5dc 100644 --- a/packages/next/src/server/lib/cache-handlers/default.ts +++ b/packages/next/src/server/lib/cache-handlers/default.ts @@ -1,162 +1,2 @@ -/** - * This is the default "use cache" handler it defaults to an in-memory store. - * In-memory caches are fragile and should not use stale-while-revalidate - * semantics on the caches because it's not worth warming up an entry that's - * likely going to get evicted before we get to use it anyway. However, we also - * don't want to reuse a stale entry for too long so stale entries should be - * considered expired/missing in such cache handlers. - */ - -import { LRUCache } from '../lru-cache' -import type { CacheEntry, CacheHandlerV2 } from './types' -import { - isStale, - tagsManifest, -} from '../incremental-cache/tags-manifest.external' - -type PrivateCacheEntry = { - entry: CacheEntry - - // For the default cache we store errored cache - // entries and allow them to be used up to 3 times - // after that we want to dispose it and try for fresh - - // If an entry is errored we return no entry - // three times so that we retry hitting origin (MISS) - // and then if it still fails to set after the third we - // return the errored content and use expiration of - // Math.min(30, entry.expiration) - isErrored: boolean - errorRetryCount: number - - // compute size on set since we need to read size - // of the ReadableStream for LRU evicting - size: number -} - -// LRU cache default to max 50 MB but in future track -const memoryCache = new LRUCache( - 50 * 1024 * 1024, - (entry) => entry.size -) -const pendingSets = new Map>() - -const debug = process.env.NEXT_PRIVATE_DEBUG_CACHE - ? console.debug.bind(console, 'DefaultCacheHandler:') - : undefined - -const DefaultCacheHandler: CacheHandlerV2 = { - async get(cacheKey) { - const pendingPromise = pendingSets.get(cacheKey) - - if (pendingPromise) { - debug?.('get', cacheKey, 'pending') - await pendingPromise - } - - const privateEntry = memoryCache.get(cacheKey) - - if (!privateEntry) { - debug?.('get', cacheKey, 'not found') - return undefined - } - - const entry = privateEntry.entry - if ( - performance.timeOrigin + performance.now() > - entry.timestamp + entry.revalidate * 1000 - ) { - // In-memory caches should expire after revalidate time because it is - // unlikely that a new entry will be able to be used before it is dropped - // from the cache. - debug?.('get', cacheKey, 'expired') - - return undefined - } - - if (isStale(entry.tags, entry.timestamp)) { - debug?.('get', cacheKey, 'had stale tag') - - return undefined - } - const [returnStream, newSaved] = entry.value.tee() - entry.value = newSaved - - debug?.('get', cacheKey, 'found', { - tags: entry.tags, - timestamp: entry.timestamp, - revalidate: entry.revalidate, - expire: entry.expire, - }) - - return { - ...entry, - value: returnStream, - } - }, - - async set(cacheKey, pendingEntry) { - debug?.('set', cacheKey, 'start') - - let resolvePending: () => void = () => {} - const pendingPromise = new Promise((resolve) => { - resolvePending = resolve - }) - pendingSets.set(cacheKey, pendingPromise) - - const entry = await pendingEntry - - let size = 0 - - try { - const [value, clonedValue] = entry.value.tee() - entry.value = value - const reader = clonedValue.getReader() - - for (let chunk; !(chunk = await reader.read()).done; ) { - size += Buffer.from(chunk.value).byteLength - } - - memoryCache.set(cacheKey, { - entry, - isErrored: false, - errorRetryCount: 0, - size, - }) - - debug?.('set', cacheKey, 'done') - } catch (err) { - // TODO: store partial buffer with error after we retry 3 times - debug?.('set', cacheKey, 'failed', err) - } finally { - resolvePending() - pendingSets.delete(cacheKey) - } - }, - - async refreshTags() { - // Nothing to do for an in-memory cache handler. - }, - - async getExpiration(...tags) { - const expiration = Math.max( - ...tags.map((tag) => tagsManifest.get(tag) ?? 0) - ) - - debug?.('getExpiration', { tags, expiration }) - - return expiration - }, - - async expireTags(...tags) { - const timestamp = Math.round(performance.timeOrigin + performance.now()) - debug?.('expireTags', { tags, timestamp }) - - for (const tag of tags) { - // TODO: update file-system-cache? - tagsManifest.set(tag, timestamp) - } - }, -} - -export default DefaultCacheHandler +export { default } from './default.external' +export * from './default.external' diff --git a/packages/next/src/server/use-cache/handlers.ts b/packages/next/src/server/use-cache/handlers.ts index 8423a66b97b2..de98066909d4 100644 --- a/packages/next/src/server/use-cache/handlers.ts +++ b/packages/next/src/server/use-cache/handlers.ts @@ -1,4 +1,4 @@ -import DefaultCacheHandler from '../lib/cache-handlers/default' +import DefaultCacheHandler from '../lib/cache-handlers/default.external' import type { CacheHandlerCompat } from '../lib/cache-handlers/types' const debug = process.env.NEXT_PRIVATE_DEBUG_CACHE From 073a89883e04a66365cdb4bba7c88c2281c69e17 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 18 Jun 2025 10:38:12 -0600 Subject: [PATCH 3/3] refactor: remove the index file so we can not break git-blame --- packages/next/src/server/lib/cache-handlers/default.ts | 2 -- test/e2e/app-dir/use-cache-custom-handler/handler.js | 2 +- test/e2e/app-dir/use-cache-custom-handler/legacy-handler.js | 2 +- .../app-dir/use-cache-unknown-cache-kind/instrumentation.ts | 2 +- .../use-cache-unknown-cache-kind.test.ts | 4 +++- test/e2e/app-dir/use-cache/next.config.js | 4 +++- test/integration/telemetry/next.config.use-cache | 2 +- test/production/custom-server/cache-handler.js | 2 +- 8 files changed, 11 insertions(+), 9 deletions(-) delete mode 100644 packages/next/src/server/lib/cache-handlers/default.ts diff --git a/packages/next/src/server/lib/cache-handlers/default.ts b/packages/next/src/server/lib/cache-handlers/default.ts deleted file mode 100644 index f6b93f85e5dc..000000000000 --- a/packages/next/src/server/lib/cache-handlers/default.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './default.external' -export * from './default.external' diff --git a/test/e2e/app-dir/use-cache-custom-handler/handler.js b/test/e2e/app-dir/use-cache-custom-handler/handler.js index 8bb773174a3b..c0198d7387fd 100644 --- a/test/e2e/app-dir/use-cache-custom-handler/handler.js +++ b/test/e2e/app-dir/use-cache-custom-handler/handler.js @@ -1,7 +1,7 @@ // @ts-check const defaultCacheHandler = - require('next/dist/server/lib/cache-handlers/default').default + require('next/dist/server/lib/cache-handlers/default.external').default /** * @type {import('next/dist/server/lib/cache-handlers/types').CacheHandlerV2} diff --git a/test/e2e/app-dir/use-cache-custom-handler/legacy-handler.js b/test/e2e/app-dir/use-cache-custom-handler/legacy-handler.js index 3244b7d68611..6fe8f06b4b29 100644 --- a/test/e2e/app-dir/use-cache-custom-handler/legacy-handler.js +++ b/test/e2e/app-dir/use-cache-custom-handler/legacy-handler.js @@ -1,7 +1,7 @@ // @ts-check const defaultCacheHandler = - require('next/dist/server/lib/cache-handlers/default').default + require('next/dist/server/lib/cache-handlers/default.external').default /** * @type {import('next/dist/server/lib/cache-handlers/types').CacheHandler} diff --git a/test/e2e/app-dir/use-cache-unknown-cache-kind/instrumentation.ts b/test/e2e/app-dir/use-cache-unknown-cache-kind/instrumentation.ts index af56a26e9563..c0f1900b22e5 100644 --- a/test/e2e/app-dir/use-cache-unknown-cache-kind/instrumentation.ts +++ b/test/e2e/app-dir/use-cache-unknown-cache-kind/instrumentation.ts @@ -1,4 +1,4 @@ -import DefaultCacheHandler from 'next/dist/server/lib/cache-handlers/default' +import DefaultCacheHandler from 'next/dist/server/lib/cache-handlers/default.external' export function register() { globalThis[Symbol.for('@next/cache-handlers')] = { diff --git a/test/e2e/app-dir/use-cache-unknown-cache-kind/use-cache-unknown-cache-kind.test.ts b/test/e2e/app-dir/use-cache-unknown-cache-kind/use-cache-unknown-cache-kind.test.ts index d7b328d43bc8..43f3262ce0e7 100644 --- a/test/e2e/app-dir/use-cache-unknown-cache-kind/use-cache-unknown-cache-kind.test.ts +++ b/test/e2e/app-dir/use-cache-unknown-cache-kind/use-cache-unknown-cache-kind.test.ts @@ -16,7 +16,9 @@ const nextConfigWithCacheHandler: NextConfig = { experimental: { dynamicIO: true, cacheHandlers: { - custom: require.resolve('next/dist/server/lib/cache-handlers/default'), + custom: require.resolve( + 'next/dist/server/lib/cache-handlers/default.external' + ), }, }, } diff --git a/test/e2e/app-dir/use-cache/next.config.js b/test/e2e/app-dir/use-cache/next.config.js index da890801cdfe..3a2f883b8245 100644 --- a/test/e2e/app-dir/use-cache/next.config.js +++ b/test/e2e/app-dir/use-cache/next.config.js @@ -13,7 +13,9 @@ const nextConfig = { }, }, cacheHandlers: { - custom: require.resolve('next/dist/server/lib/cache-handlers/default'), + custom: require.resolve( + 'next/dist/server/lib/cache-handlers/default.external' + ), }, }, cacheHandler: require.resolve('./incremental-cache-handler'), diff --git a/test/integration/telemetry/next.config.use-cache b/test/integration/telemetry/next.config.use-cache index c4367991973f..e52062d48ab7 100644 --- a/test/integration/telemetry/next.config.use-cache +++ b/test/integration/telemetry/next.config.use-cache @@ -2,7 +2,7 @@ module.exports = { experimental: { dynamicIO: true, cacheHandlers: { - custom: require.resolve('next/dist/server/lib/cache-handlers/default'), + custom: require.resolve('next/dist/server/lib/cache-handlers/default.external'), }, }, } diff --git a/test/production/custom-server/cache-handler.js b/test/production/custom-server/cache-handler.js index e8711977ecf0..283beaa1f3ab 100644 --- a/test/production/custom-server/cache-handler.js +++ b/test/production/custom-server/cache-handler.js @@ -3,7 +3,7 @@ const { requestIdStorage } = require('./als') const defaultCacheHandler = - require('next/dist/server/lib/cache-handlers/default').default + require('next/dist/server/lib/cache-handlers/default.external').default /** * @type {import('next/dist/server/lib/cache-handlers/types').CacheHandlerV2}