rsnext/packages/next/server/lib/incremental-cache/file-system-cache.ts
JJ Kasper 0c756bee15
Update cache handling for app (#43659)
This updates the app directory caching. 

x-ref: [slack thread
](https://vercel.slack.com/archives/C042LHPJ1NX/p1669231119199339)

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ]
[e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
2022-12-02 23:32:49 -08:00

179 lines
4.9 KiB
TypeScript

import LRUCache from 'next/dist/compiled/lru-cache'
import path from '../../../shared/lib/isomorphic/path'
import type { CacheHandler, CacheHandlerContext, CacheHandlerValue } from './'
let memoryCache: LRUCache<string, CacheHandlerValue> | undefined
export default class FileSystemCache implements CacheHandler {
private fs: CacheHandlerContext['fs']
private flushToDisk?: CacheHandlerContext['flushToDisk']
private serverDistDir: CacheHandlerContext['serverDistDir']
private appDir: boolean
constructor(ctx: CacheHandlerContext) {
this.fs = ctx.fs
this.flushToDisk = ctx.flushToDisk
this.serverDistDir = ctx.serverDistDir
this.appDir = !!ctx._appDir
if (ctx.maxMemoryCacheSize && !memoryCache) {
memoryCache = new LRUCache({
max: ctx.maxMemoryCacheSize,
length({ value }) {
if (!value) {
return 25
} else if (value.kind === 'REDIRECT') {
return JSON.stringify(value.props).length
} else if (value.kind === 'IMAGE') {
throw new Error('invariant image should not be incremental-cache')
} else if (value.kind === 'FETCH') {
return JSON.stringify(value.data || '').length
}
// rough estimate of size of cache value
return (
value.html.length + (JSON.stringify(value.pageData)?.length || 0)
)
},
})
}
}
public async get(key: string, fetchCache?: boolean) {
let data = memoryCache?.get(key)
// let's check the disk for seed data
if (!data) {
try {
const { filePath, isAppPath } = await this.getFsPath({
pathname: fetchCache ? key : `${key}.html`,
fetchCache,
})
const fileData = await this.fs.readFile(filePath)
const { mtime } = await this.fs.stat(filePath)
if (fetchCache) {
const lastModified = mtime.getTime()
data = {
lastModified,
value: JSON.parse(fileData),
}
} else {
const pageData = isAppPath
? await this.fs.readFile(
(
await this.getFsPath({ pathname: `${key}.rsc`, appDir: true })
).filePath
)
: JSON.parse(
await this.fs.readFile(
await (
await this.getFsPath({
pathname: `${key}.json`,
appDir: false,
})
).filePath
)
)
data = {
lastModified: mtime.getTime(),
value: {
kind: 'PAGE',
html: fileData,
pageData,
},
}
}
if (data) {
memoryCache?.set(key, data)
}
} catch (_) {
// unable to get data from disk
}
}
return data || null
}
public async set(key: string, data: CacheHandlerValue['value']) {
memoryCache?.set(key, {
value: data,
lastModified: Date.now(),
})
if (!this.flushToDisk) return
if (data?.kind === 'PAGE') {
const isAppPath = typeof data.pageData === 'string'
const { filePath: htmlPath } = await this.getFsPath({
pathname: `${key}.html`,
appDir: isAppPath,
})
await this.fs.mkdir(path.dirname(htmlPath))
await this.fs.writeFile(htmlPath, data.html)
await this.fs.writeFile(
(
await this.getFsPath({
pathname: `${key}.${isAppPath ? 'rsc' : 'json'}`,
appDir: isAppPath,
})
).filePath,
isAppPath ? data.pageData : JSON.stringify(data.pageData)
)
} else if (data?.kind === 'FETCH') {
const { filePath } = await this.getFsPath({
pathname: key,
fetchCache: true,
})
await this.fs.mkdir(path.dirname(filePath))
await this.fs.writeFile(filePath, JSON.stringify(data))
}
}
private async getFsPath({
pathname,
appDir,
fetchCache,
}: {
pathname: string
appDir?: boolean
fetchCache?: boolean
}): Promise<{
filePath: string
isAppPath: boolean
}> {
if (fetchCache) {
// we store in .next/cache/fetch-cache so it can be persisted
// across deploys
return {
filePath: path.join(
this.serverDistDir,
'..',
'cache',
'fetch-cache',
pathname
),
isAppPath: false,
}
}
let isAppPath = false
let filePath = path.join(this.serverDistDir, 'pages', pathname)
if (!this.appDir || appDir === false)
return {
filePath,
isAppPath,
}
try {
await this.fs.readFile(filePath)
return {
filePath,
isAppPath,
}
} catch (err) {
return {
filePath: path.join(this.serverDistDir, 'app', pathname),
isAppPath: true,
}
}
}
}