0c756bee15
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)
179 lines
4.9 KiB
TypeScript
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,
|
|
}
|
|
}
|
|
}
|
|
}
|