2022-01-19 13:36:06 +01:00
|
|
|
import type { CacheFs } from '../shared/lib/utils'
|
|
|
|
|
2020-06-28 22:58:43 +02:00
|
|
|
import LRUCache from 'next/dist/compiled/lru-cache'
|
2022-04-21 11:07:03 +02:00
|
|
|
import path from '../shared/lib/isomorphic/path'
|
2021-06-30 13:44:40 +02:00
|
|
|
import { PrerenderManifest } from '../build'
|
2022-04-30 13:19:27 +02:00
|
|
|
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
|
2022-02-09 00:46:59 +01:00
|
|
|
import { IncrementalCacheValue, IncrementalCacheEntry } from './response-cache'
|
2020-06-28 22:58:43 +02:00
|
|
|
|
|
|
|
function toRoute(pathname: string): string {
|
|
|
|
return pathname.replace(/\/$/, '').replace(/\/index$/, '') || '/'
|
|
|
|
}
|
|
|
|
|
|
|
|
export class IncrementalCache {
|
|
|
|
incrementalOptions: {
|
|
|
|
flushToDisk?: boolean
|
|
|
|
pagesDir?: string
|
|
|
|
distDir?: string
|
|
|
|
dev?: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
prerenderManifest: PrerenderManifest
|
2021-07-21 19:39:38 +02:00
|
|
|
cache?: LRUCache<string, IncrementalCacheEntry>
|
2020-11-11 03:09:45 +01:00
|
|
|
locales?: string[]
|
2021-12-17 23:56:26 +01:00
|
|
|
fs: CacheFs
|
2020-06-28 22:58:43 +02:00
|
|
|
|
|
|
|
constructor({
|
2021-12-17 23:56:26 +01:00
|
|
|
fs,
|
2020-06-28 22:58:43 +02:00
|
|
|
max,
|
|
|
|
dev,
|
|
|
|
distDir,
|
|
|
|
pagesDir,
|
|
|
|
flushToDisk,
|
2020-11-11 03:09:45 +01:00
|
|
|
locales,
|
2022-01-26 07:22:11 +01:00
|
|
|
getPrerenderManifest,
|
2020-06-28 22:58:43 +02:00
|
|
|
}: {
|
2021-12-17 23:56:26 +01:00
|
|
|
fs: CacheFs
|
2020-06-28 22:58:43 +02:00
|
|
|
dev: boolean
|
|
|
|
max?: number
|
|
|
|
distDir: string
|
|
|
|
pagesDir: string
|
|
|
|
flushToDisk?: boolean
|
2020-11-11 03:09:45 +01:00
|
|
|
locales?: string[]
|
2022-01-26 07:22:11 +01:00
|
|
|
getPrerenderManifest: () => PrerenderManifest
|
2020-06-28 22:58:43 +02:00
|
|
|
}) {
|
2021-12-17 23:56:26 +01:00
|
|
|
this.fs = fs
|
2020-06-28 22:58:43 +02:00
|
|
|
this.incrementalOptions = {
|
|
|
|
dev,
|
|
|
|
distDir,
|
|
|
|
pagesDir,
|
|
|
|
flushToDisk:
|
|
|
|
!dev && (typeof flushToDisk !== 'undefined' ? flushToDisk : true),
|
|
|
|
}
|
2020-11-11 03:09:45 +01:00
|
|
|
this.locales = locales
|
2022-01-26 07:22:11 +01:00
|
|
|
this.prerenderManifest = getPrerenderManifest()
|
2020-06-28 22:58:43 +02:00
|
|
|
|
2021-07-20 19:01:42 +02:00
|
|
|
if (process.env.__NEXT_TEST_MAX_ISR_CACHE) {
|
|
|
|
// Allow cache size to be overridden for testing purposes
|
|
|
|
max = parseInt(process.env.__NEXT_TEST_MAX_ISR_CACHE, 10)
|
|
|
|
}
|
|
|
|
|
2021-07-21 19:39:38 +02:00
|
|
|
if (max) {
|
|
|
|
this.cache = new LRUCache({
|
|
|
|
max,
|
|
|
|
length({ value }) {
|
2022-02-09 00:46:59 +01:00
|
|
|
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')
|
|
|
|
}
|
2021-07-21 19:39:38 +02:00
|
|
|
// rough estimate of size of cache value
|
|
|
|
return value.html.length + JSON.stringify(value.pageData).length
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2020-06-28 22:58:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private getSeedPath(pathname: string, ext: string): string {
|
|
|
|
return path.join(this.incrementalOptions.pagesDir!, `${pathname}.${ext}`)
|
|
|
|
}
|
|
|
|
|
2021-07-20 19:01:42 +02:00
|
|
|
private calculateRevalidate(
|
|
|
|
pathname: string,
|
|
|
|
fromTime: number
|
|
|
|
): number | false {
|
2020-11-12 19:50:32 +01:00
|
|
|
pathname = toRoute(pathname)
|
|
|
|
|
2020-06-28 22:58:43 +02:00
|
|
|
// in development we don't have a prerender-manifest
|
|
|
|
// and default to always revalidating to allow easier debugging
|
2021-07-20 19:01:42 +02:00
|
|
|
if (this.incrementalOptions.dev) return new Date().getTime() - 1000
|
2020-06-28 22:58:43 +02:00
|
|
|
|
|
|
|
const { initialRevalidateSeconds } = this.prerenderManifest.routes[
|
|
|
|
pathname
|
|
|
|
] || {
|
|
|
|
initialRevalidateSeconds: 1,
|
|
|
|
}
|
|
|
|
const revalidateAfter =
|
|
|
|
typeof initialRevalidateSeconds === 'number'
|
2021-07-20 19:01:42 +02:00
|
|
|
? initialRevalidateSeconds * 1000 + fromTime
|
2020-06-28 22:58:43 +02:00
|
|
|
: initialRevalidateSeconds
|
|
|
|
|
|
|
|
return revalidateAfter
|
|
|
|
}
|
|
|
|
|
|
|
|
getFallback(page: string): Promise<string> {
|
|
|
|
page = normalizePagePath(page)
|
2021-12-17 23:56:26 +01:00
|
|
|
return this.fs.readFile(this.getSeedPath(page, 'html'))
|
2020-06-28 22:58:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// get data from cache if available
|
2021-07-06 17:45:49 +02:00
|
|
|
async get(pathname: string): Promise<IncrementalCacheEntry | null> {
|
|
|
|
if (this.incrementalOptions.dev) return null
|
2020-06-28 22:58:43 +02:00
|
|
|
pathname = normalizePagePath(pathname)
|
|
|
|
|
2021-07-21 19:39:38 +02:00
|
|
|
let data = this.cache && this.cache.get(pathname)
|
2020-06-28 22:58:43 +02:00
|
|
|
|
|
|
|
// let's check the disk for seed data
|
|
|
|
if (!data) {
|
2020-10-15 23:55:38 +02:00
|
|
|
if (this.prerenderManifest.notFoundRoutes.includes(pathname)) {
|
2021-08-14 15:11:40 +02:00
|
|
|
const now = Date.now()
|
|
|
|
const revalidateAfter = this.calculateRevalidate(pathname, now)
|
|
|
|
data = {
|
|
|
|
value: null,
|
|
|
|
revalidateAfter: revalidateAfter !== false ? now : false,
|
|
|
|
}
|
2020-10-15 23:55:38 +02:00
|
|
|
}
|
|
|
|
|
2020-06-28 22:58:43 +02:00
|
|
|
try {
|
2021-07-20 19:01:42 +02:00
|
|
|
const htmlPath = this.getSeedPath(pathname, 'html')
|
2021-12-17 23:56:26 +01:00
|
|
|
const jsonPath = this.getSeedPath(pathname, 'json')
|
|
|
|
const html = await this.fs.readFile(htmlPath)
|
|
|
|
const pageData = JSON.parse(await this.fs.readFile(jsonPath))
|
|
|
|
const { mtime } = await this.fs.stat(htmlPath)
|
2020-06-28 22:58:43 +02:00
|
|
|
|
|
|
|
data = {
|
2021-07-20 19:01:42 +02:00
|
|
|
revalidateAfter: this.calculateRevalidate(pathname, mtime.getTime()),
|
2021-07-06 17:45:49 +02:00
|
|
|
value: {
|
|
|
|
kind: 'PAGE',
|
|
|
|
html,
|
|
|
|
pageData,
|
|
|
|
},
|
2020-06-28 22:58:43 +02:00
|
|
|
}
|
2021-07-21 19:39:38 +02:00
|
|
|
if (this.cache) {
|
|
|
|
this.cache.set(pathname, data)
|
|
|
|
}
|
2020-06-28 22:58:43 +02:00
|
|
|
} catch (_) {
|
|
|
|
// unable to get data from disk
|
|
|
|
}
|
|
|
|
}
|
2021-07-06 17:45:49 +02:00
|
|
|
if (!data) {
|
|
|
|
return null
|
|
|
|
}
|
2020-06-28 22:58:43 +02:00
|
|
|
|
|
|
|
if (
|
|
|
|
data &&
|
|
|
|
data.revalidateAfter !== false &&
|
|
|
|
data.revalidateAfter < new Date().getTime()
|
|
|
|
) {
|
|
|
|
data.isStale = true
|
|
|
|
}
|
2021-01-13 17:15:11 +01:00
|
|
|
|
|
|
|
const manifestPath = toRoute(pathname)
|
|
|
|
const manifestEntry = this.prerenderManifest.routes[manifestPath]
|
2020-06-28 22:58:43 +02:00
|
|
|
|
|
|
|
if (data && manifestEntry) {
|
|
|
|
data.curRevalidate = manifestEntry.initialRevalidateSeconds
|
|
|
|
}
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
// populate the incremental cache with new data
|
|
|
|
async set(
|
|
|
|
pathname: string,
|
2021-07-06 17:45:49 +02:00
|
|
|
data: IncrementalCacheValue | null,
|
2020-06-28 22:58:43 +02:00
|
|
|
revalidateSeconds?: number | false
|
|
|
|
) {
|
|
|
|
if (this.incrementalOptions.dev) return
|
|
|
|
if (typeof revalidateSeconds !== 'undefined') {
|
|
|
|
// TODO: Update this to not mutate the manifest from the
|
|
|
|
// build.
|
|
|
|
this.prerenderManifest.routes[pathname] = {
|
|
|
|
dataRoute: path.posix.join(
|
|
|
|
'/_next/data',
|
|
|
|
`${normalizePagePath(pathname)}.json`
|
|
|
|
),
|
|
|
|
srcRoute: null, // FIXME: provide actual source route, however, when dynamically appending it doesn't really matter
|
|
|
|
initialRevalidateSeconds: revalidateSeconds,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pathname = normalizePagePath(pathname)
|
2021-07-21 19:39:38 +02:00
|
|
|
if (this.cache) {
|
|
|
|
this.cache.set(pathname, {
|
|
|
|
revalidateAfter: this.calculateRevalidate(
|
|
|
|
pathname,
|
|
|
|
new Date().getTime()
|
|
|
|
),
|
|
|
|
value: data,
|
|
|
|
})
|
|
|
|
}
|
2020-06-28 22:58:43 +02:00
|
|
|
|
|
|
|
// TODO: This option needs to cease to exist unless it stops mutating the
|
|
|
|
// `next build` output's manifest.
|
2021-07-06 17:45:49 +02:00
|
|
|
if (this.incrementalOptions.flushToDisk && data?.kind === 'PAGE') {
|
2020-06-28 22:58:43 +02:00
|
|
|
try {
|
2021-12-17 23:56:26 +01:00
|
|
|
const seedHtmlPath = this.getSeedPath(pathname, 'html')
|
|
|
|
const seedJsonPath = this.getSeedPath(pathname, 'json')
|
|
|
|
await this.fs.mkdir(path.dirname(seedHtmlPath))
|
|
|
|
await this.fs.writeFile(seedHtmlPath, data.html)
|
|
|
|
await this.fs.writeFile(seedJsonPath, JSON.stringify(data.pageData))
|
2020-06-28 22:58:43 +02:00
|
|
|
} catch (error) {
|
|
|
|
// failed to flush to disk
|
|
|
|
console.warn('Failed to update prerender files for', pathname, error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|