diff --git a/packages/next/src/server/lib/lru-cache.ts b/packages/next/src/server/lib/lru-cache.ts index 59b76b5d0b4c..59c97293c0e3 100644 --- a/packages/next/src/server/lib/lru-cache.ts +++ b/packages/next/src/server/lib/lru-cache.ts @@ -49,12 +49,14 @@ export class LRUCache { private readonly tail: SentinelNode private totalSize: number = 0 private readonly maxSize: number - private readonly calculateSize: ((value: T) => number) | undefined + private readonly calculateSize: + | ((value: T, key: string) => number) + | undefined private readonly onEvict: ((key: string, value: T) => void) | undefined constructor( maxSize: number, - calculateSize?: (value: T) => number, + calculateSize?: (value: T, key: string) => number, onEvict?: (key: string, value: T) => void ) { this.maxSize = maxSize @@ -124,7 +126,7 @@ export class LRUCache { * - O(k) where k is the number of items evicted (can be O(N) for variable sizes) */ public set(key: string, value: T): boolean { - const size = this.calculateSize?.(value) ?? 1 + const size = this.calculateSize?.(value, key) ?? 1 if (size <= 0) { throw new Error( `LRUCache: calculateSize returned ${size}, but size must be > 0. ` + diff --git a/packages/next/src/server/lib/router-utils/filesystem.test.ts b/packages/next/src/server/lib/router-utils/filesystem.test.ts new file mode 100644 index 000000000000..39e6a5433a9c --- /dev/null +++ b/packages/next/src/server/lib/router-utils/filesystem.test.ts @@ -0,0 +1,73 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises' +import { tmpdir } from 'os' +import { join } from 'path' +import { defaultConfig } from '../../config-shared' +import { setupFsCheck } from './filesystem' + +describe('filesystem cache', () => { + let dir: string + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), 'next-fs-check-')) + const distDir = join(dir, '.next') + const serverDir = join(distDir, 'server') + await mkdir(serverDir, { recursive: true }) + + await Promise.all([ + writeFile(join(distDir, 'BUILD_ID'), 'build-id'), + writeFile( + join(distDir, 'routes-manifest.json'), + JSON.stringify({ + redirects: [], + rewrites: { beforeFiles: [], afterFiles: [], fallback: [] }, + headers: [], + onMatchHeaders: [], + dynamicRoutes: [], + dataRoutes: [], + }) + ), + writeFile( + join(distDir, 'prerender-manifest.json'), + JSON.stringify({ + preview: { + previewModeId: 'id', + previewModeSigningKey: 'signing-key', + previewModeEncryptionKey: 'encryption-key', + }, + }) + ), + writeFile(join(serverDir, 'pages-manifest.json'), '{}'), + writeFile( + join(serverDir, 'functions-config-manifest.json'), + JSON.stringify({ functions: {} }) + ), + ]) + }) + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }) + }) + + it('includes URL keys when limiting the production filesystem cache', async () => { + const fsChecker = await setupFsCheck({ + dir, + dev: false, + config: { ...defaultConfig, output: 'standalone' }, + }) + + fsChecker.pageFiles.add('/first') + expect(await fsChecker.getItem('/first')).toMatchObject({ + itemPath: '/first', + type: 'pageFile', + }) + fsChecker.pageFiles.delete('/first') + + // The retained URL keys together exceed the cache's 1 MiB size limit. + const longSegment = 'x'.repeat(1024) + for (let index = 0; index < 1100; index++) { + await fsChecker.getItem(`/${longSegment}/${index}`) + } + + expect(await fsChecker.getItem('/first')).toBeNull() + }) +}) diff --git a/packages/next/src/server/lib/router-utils/filesystem.ts b/packages/next/src/server/lib/router-utils/filesystem.ts index 6909eb2c7738..eef4c50ef047 100644 --- a/packages/next/src/server/lib/router-utils/filesystem.ts +++ b/packages/next/src/server/lib/router-utils/filesystem.ts @@ -113,12 +113,14 @@ export async function setupFsCheck(opts: { config: NextConfigRuntime }) { const getItemsLru = !opts.dev - ? new LRUCache(1024 * 1024, function length(value) { + ? new LRUCache(1024 * 1024, function length(value, key) { + const keyLength = key.length if (!value) { // Null entries (negative cache) still need a non-zero size for LRU eviction - return 1 + return keyLength || 1 } return ( + keyLength + (value.fsPath || '').length + value.itemPath.length + value.type.length