Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/next/src/server/lib/lru-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ export class LRUCache<T> {
private readonly tail: SentinelNode<T>
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
Expand Down Expand Up @@ -124,7 +126,7 @@ export class LRUCache<T> {
* - 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. ` +
Expand Down
73 changes: 73 additions & 0 deletions packages/next/src/server/lib/router-utils/filesystem.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
6 changes: 4 additions & 2 deletions packages/next/src/server/lib/router-utils/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,14 @@ export async function setupFsCheck(opts: {
config: NextConfigRuntime
}) {
const getItemsLru = !opts.dev
? new LRUCache<FsOutput | null>(1024 * 1024, function length(value) {
? new LRUCache<FsOutput | null>(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
Expand Down