Handle unstable_cache in pages (#49624)

Ensures this method works in pages the same as in app as discussed. 

x-ref: [slack
thread](https://vercel.slack.com/archives/C042LHPJ1NX/p1683242040712889)
x-ref: [slack
thread](https://vercel.slack.com/archives/C042LHPJ1NX/p1683724596461329)
This commit is contained in:
JJ Kasper 2023-05-10 13:59:48 -07:00 committed by GitHub
parent 2f3a503d8b
commit ce746ef5c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 240 additions and 62 deletions

View file

@ -1051,7 +1051,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
// be passed down for edge functions and the fetch disk
// cache can be leveraged locally
if (
!(globalThis as any).__incrementalCache &&
!(this.serverOptions as any).webServerConfig &&
!getRequestMeta(req, '_nextIncrementalCache')
) {
let protocol: 'http:' | 'https:' = 'https:'
@ -1071,6 +1071,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
| 'https',
})
addRequestMeta(req, '_nextIncrementalCache', incrementalCache)
;(globalThis as any).__incrementalCache = incrementalCache
}
try {

View file

@ -45,30 +45,33 @@ export default class FetchCache implements CacheHandler {
console.log('no cache endpoint available')
}
if (ctx.maxMemoryCacheSize && !memoryCache) {
if (this.debug) {
console.log('using memory store for fetch cache')
if (ctx.maxMemoryCacheSize) {
if (!memoryCache) {
if (this.debug) {
console.log('using memory store for fetch cache')
}
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
} else if (value.kind === 'ROUTE') {
return value.body.length
}
// rough estimate of size of cache value
return (
value.html.length + (JSON.stringify(value.pageData)?.length || 0)
)
},
})
}
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
} else if (value.kind === 'ROUTE') {
return value.body.length
}
// rough estimate of size of cache value
return (
value.html.length + (JSON.stringify(value.pageData)?.length || 0)
)
},
})
} else {
if (this.debug) {
console.log('not using memory store for fetch cache')

View file

@ -10,7 +10,10 @@ import {
} from '../../response-cache'
import { encode } from '../../../shared/lib/bloom-filter/base64-arraybuffer'
import { encodeText } from '../../stream-utils/encode-decode'
import { CACHE_ONE_YEAR } from '../../../lib/constants'
import {
CACHE_ONE_YEAR,
PRERENDER_REVALIDATE_HEADER,
} from '../../../lib/constants'
function toRoute(pathname: string): string {
return pathname.replace(/\/$/, '').replace(/\/index$/, '') || '/'
@ -70,6 +73,7 @@ export class IncrementalCache {
minimalMode?: boolean
fetchCacheKeyPrefix?: string
revalidatedTags?: string[]
isOnDemandRevalidate?: boolean
constructor({
fs,
@ -102,13 +106,22 @@ export class IncrementalCache {
fetchCacheKeyPrefix?: string
CurCacheHandler?: typeof CacheHandler
}) {
const debug = !!process.env.NEXT_PRIVATE_DEBUG_CACHE
if (!CurCacheHandler) {
if (fs && serverDistDir) {
if (debug) {
console.log('using filesystem cache handler')
}
CurCacheHandler = FileSystemCache
}
if (minimalMode && fetchCache) {
if (debug) {
console.log('using fetch cache handler')
}
CurCacheHandler = FetchCache
}
} else if (debug) {
console.log('using custom cache handler', CurCacheHandler.name)
}
if (process.env.__NEXT_TEST_MAX_ISR_CACHE) {
@ -124,6 +137,13 @@ export class IncrementalCache {
this.fetchCacheKeyPrefix = fetchCacheKeyPrefix
let revalidatedTags: string[] = []
if (
requestHeaders[PRERENDER_REVALIDATE_HEADER] ===
this.prerenderManifest?.preview?.previewModeId
) {
this.isOnDemandRevalidate = true
}
if (
minimalMode &&
typeof requestHeaders['x-next-revalidated-tags'] === 'string' &&
@ -298,7 +318,7 @@ export class IncrementalCache {
async get(
pathname: string,
fetchCache?: boolean,
revalidate?: number,
revalidate?: number | false,
fetchUrl?: string,
fetchIdx?: number
): Promise<IncrementalCacheEntry | null> {

View file

@ -301,7 +301,8 @@ export function patchFetch({
const fetchIdx = staticGenerationStore.nextFetchId ?? 1
staticGenerationStore.nextFetchId = fetchIdx + 1
const normalizedRevalidate = !revalidate ? CACHE_ONE_YEAR : revalidate
const normalizedRevalidate =
typeof revalidate !== 'number' ? CACHE_ONE_YEAR : revalidate
const doOriginalFetch = async (isStale?: boolean) => {
// add metadata to init without editing the original
@ -341,7 +342,7 @@ export function patchFetch({
},
revalidate: normalizedRevalidate,
},
normalizedRevalidate,
revalidate,
true,
fetchUrl,
fetchIdx
@ -365,7 +366,7 @@ export function patchFetch({
: await staticGenerationStore.incrementalCache.get(
cacheKey,
true,
normalizedRevalidate,
revalidate,
fetchUrl,
fetchIdx
)

View file

@ -2838,7 +2838,9 @@ export default class NextNodeServer extends BaseServer {
},
useCache: true,
onWarning: params.onWarning,
incrementalCache: getRequestMeta(params.req, '_nextIncrementalCache'),
incrementalCache:
(globalThis as any).__incrementalCache ||
getRequestMeta(params.req, '_nextIncrementalCache'),
})
params.res.statusCode = result.response.status

View file

@ -180,7 +180,7 @@ export async function adapter(
).IncrementalCache({
appDir: true,
fetchCache: true,
minimalMode: true,
minimalMode: process.env.NODE_ENV !== 'development',
fetchCacheKeyPrefix: process.env.__NEXT_FETCH_CACHE_KEY_PREFIX,
dev: process.env.NODE_ENV === 'development',
requestHeaders: params.request.headers as any,

View file

@ -63,7 +63,10 @@ export const getRuntimeContext = async (params: {
edgeFunctionEntry: params.edgeFunctionEntry,
distDir: params.distDir,
})
runtime.context.globalThis.__incrementalCache = params.incrementalCache
if (params.incrementalCache) {
runtime.context.globalThis.__incrementalCache = params.incrementalCache
}
for (const paramPath of params.paths) {
evaluateInContext(paramPath)

View file

@ -1,33 +1,40 @@
import {
StaticGenerationAsyncStorage,
StaticGenerationStore,
staticGenerationAsyncStorage as _staticGenerationAsyncStorage,
StaticGenerationAsyncStorage,
} from '../../../client/components/static-generation-async-storage'
import { CACHE_ONE_YEAR } from '../../../lib/constants'
import { addImplicitTags } from '../../lib/patch-fetch'
type Callback = (...args: any[]) => Promise<any>
export function unstable_cache<T extends Callback>(
cb: T,
keyParts: string[],
keyParts?: string[],
options: {
revalidate: number | false
revalidate?: number | false
tags?: string[]
}
} = {}
): T {
const joinedKey = cb.toString() + '-' + keyParts.join(', ')
const staticGenerationAsyncStorage = (
fetch as any
).__nextGetStaticStore?.() as undefined | StaticGenerationAsyncStorage
const joinedKey =
keyParts && keyParts.length > 0 ? keyParts.join(',') : cb.toString()
const staticGenerationAsyncStorage: StaticGenerationAsyncStorage =
(fetch as any).__nextGetStaticStore?.() || _staticGenerationAsyncStorage
const store: undefined | StaticGenerationStore =
staticGenerationAsyncStorage?.getStore()
if (!store || !store.incrementalCache) {
const incrementalCache:
| import('../../lib/incremental-cache').IncrementalCache
| undefined =
store?.incrementalCache || (globalThis as any).__incrementalCache
if (!incrementalCache) {
throw new Error(
`Invariant: static generation store missing in unstable_cache ${joinedKey}`
`Invariant: incrementalCache missing in unstable_cache ${joinedKey}`
)
}
if (options.revalidate === 0) {
throw new Error(
`Invariant revalidate: 0 can not be passed to unstable_cache(), must be "false" or "> 0" ${joinedKey}`
@ -39,21 +46,21 @@ export function unstable_cache<T extends Callback>(
// cache callback so that we only cache the specific values returned
// from the callback instead of also caching any fetches done inside
// of the callback as well
return staticGenerationAsyncStorage?.run(
return staticGenerationAsyncStorage.run(
{
...store,
fetchCache: 'only-no-store',
isStaticGeneration: !!store?.isStaticGeneration,
pathname: store?.pathname || '/',
},
async () => {
const cacheKey = await store.incrementalCache?.fetchCacheKey(joinedKey)
const cacheKey = await incrementalCache?.fetchCacheKey(joinedKey)
const cacheEntry =
cacheKey &&
!store.isOnDemandRevalidate &&
(await store.incrementalCache?.get(
cacheKey,
true,
options.revalidate as number
))
!(
store?.isOnDemandRevalidate || incrementalCache.isOnDemandRevalidate
) &&
(await incrementalCache?.get(cacheKey, true, options.revalidate))
const tags = options.tags || []
const implicitTags = addImplicitTags(store)
@ -67,8 +74,8 @@ export function unstable_cache<T extends Callback>(
const invokeCallback = async () => {
const result = await cb(...args)
if (cacheKey && store.incrementalCache) {
await store.incrementalCache.set(
if (cacheKey && incrementalCache) {
await incrementalCache.set(
cacheKey,
{
kind: 'FETCH',
@ -79,7 +86,10 @@ export function unstable_cache<T extends Callback>(
status: 200,
tags,
},
revalidate: options.revalidate as number,
revalidate:
typeof options.revalidate !== 'number'
? CACHE_ONE_YEAR
: options.revalidate,
},
options.revalidate,
true
@ -108,14 +118,18 @@ export function unstable_cache<T extends Callback>(
const currentTags = cacheEntry.value.data.tags
if (isStale) {
if (!store.pendingRevalidates) {
store.pendingRevalidates = []
}
store.pendingRevalidates.push(
invokeCallback().catch((err) =>
console.error(`revalidating cache with key: ${joinedKey}`, err)
if (!store) {
return invokeCallback()
} else {
if (!store.pendingRevalidates) {
store.pendingRevalidates = []
}
store.pendingRevalidates.push(
invokeCallback().catch((err) =>
console.error(`revalidating cache with key: ${joinedKey}`, err)
)
)
)
}
} else if (tags && !tags.every((tag) => currentTags?.includes(tag))) {
if (!cacheEntry.value.data.tags) {
cacheEntry.value.data.tags = []
@ -126,7 +140,7 @@ export function unstable_cache<T extends Callback>(
cacheEntry.value.data.tags.push(tag)
}
}
store.incrementalCache?.set(
incrementalCache?.set(
cacheKey,
cacheEntry.value,
options.revalidate,

View file

@ -14,6 +14,7 @@ createNextDescribe(
files: __dirname,
env: {
NEXT_DEBUG_BUILD: '1',
NEXT_PRIVATE_DEBUG_CACHE: '1',
...(process.env.CUSTOM_CACHE_HANDLER
? {
CUSTOM_CACHE_HANDLER: process.env.CUSTOM_CACHE_HANDLER,
@ -34,6 +35,46 @@ createNextDescribe(
}
})
it.each([
{ pathname: '/unstable-cache-node' },
{ pathname: '/unstable-cache-edge' },
{ pathname: '/api/unstable-cache-node' },
{ pathname: '/api/unstable-cache-edge' },
])('unstable-cache should work in pages$pathname', async ({ pathname }) => {
let res = await next.fetch(pathname)
expect(res.status).toBe(200)
const isApi = pathname.startsWith('/api')
let prevData
if (isApi) {
prevData = await res.json()
} else {
const initialHtml = await res.text()
const initial$ = isApi ? undefined : cheerio.load(initialHtml)
prevData = JSON.parse(initial$('#props').text())
}
expect(prevData.data.random).toBeTruthy()
await check(async () => {
res = await next.fetch(pathname)
expect(res.status).toBe(200)
let curData
if (isApi) {
curData = await res.json()
} else {
const curHtml = await res.text()
const cur$ = cheerio.load(curHtml)
curData = JSON.parse(cur$('#props').text())
}
expect(curData.data.random).toBeTruthy()
expect(curData.data.random).toBe(prevData.data.random)
return 'success'
}, 'success')
})
it('should not have cache tags header for non-minimal mode', async () => {
for (const path of [
'/ssr-forced',

View file

@ -0,0 +1,25 @@
import { unstable_cache } from 'next/cache'
export const config = {
runtime: 'edge',
}
export default async function handler(req) {
const data = await unstable_cache(async () => {
return {
random: Math.random(),
}
})()
return new Response(
JSON.stringify({
now: Date.now(),
data,
}),
{
headers: {
'content-type': 'application/json',
},
}
)
}

View file

@ -0,0 +1,14 @@
import { unstable_cache } from 'next/cache'
export default async function handler(req, res) {
const data = await unstable_cache(async () => {
return {
random: Math.random(),
}
})()
res.json({
now: Date.now(),
data,
})
}

View file

@ -0,0 +1,29 @@
import { unstable_cache } from 'next/cache'
export const config = {
runtime: 'experimental-edge',
}
export async function getServerSideProps() {
const data = await unstable_cache(async () => {
return {
random: Math.random(),
}
})()
return {
props: {
now: Date.now(),
data,
},
}
}
export default function Page(props) {
return (
<>
<p>/unstable-cache-edge</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}

View file

@ -0,0 +1,25 @@
import { unstable_cache } from 'next/cache'
export async function getServerSideProps() {
const data = await unstable_cache(async () => {
return {
random: Math.random(),
}
})()
return {
props: {
now: Date.now(),
data,
},
}
}
export default function Page(props) {
return (
<>
<p>/unstable-cache-node</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}