Skip to content

Commit

Permalink
Handle unstable_cache in pages (vercel#49624)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
ijjk authored May 10, 2023
1 parent 2f3a503 commit ce746ef
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 62 deletions.
3 changes: 2 additions & 1 deletion packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:'
Expand All @@ -1071,6 +1071,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
| 'https',
})
addRequestMeta(req, '_nextIncrementalCache', incrementalCache)
;(globalThis as any).__incrementalCache = incrementalCache
}

try {
Expand Down
49 changes: 26 additions & 23 deletions packages/next/src/server/lib/incremental-cache/fetch-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
24 changes: 22 additions & 2 deletions packages/next/src/server/lib/incremental-cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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$/, '') || '/'
Expand Down Expand Up @@ -70,6 +73,7 @@ export class IncrementalCache {
minimalMode?: boolean
fetchCacheKeyPrefix?: string
revalidatedTags?: string[]
isOnDemandRevalidate?: boolean

constructor({
fs,
Expand Down Expand Up @@ -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) {
Expand All @@ -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' &&
Expand Down Expand Up @@ -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> {
Expand Down
7 changes: 4 additions & 3 deletions packages/next/src/server/lib/patch-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -341,7 +342,7 @@ export function patchFetch({
},
revalidate: normalizedRevalidate,
},
normalizedRevalidate,
revalidate,
true,
fetchUrl,
fetchIdx
Expand All @@ -365,7 +366,7 @@ export function patchFetch({
: await staticGenerationStore.incrementalCache.get(
cacheKey,
true,
normalizedRevalidate,
revalidate,
fetchUrl,
fetchIdx
)
Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/web/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion packages/next/src/server/web/sandbox/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
74 changes: 44 additions & 30 deletions packages/next/src/server/web/spec-extension/unstable-cache.ts
Original file line number Diff line number Diff line change
@@ -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}`
Expand All @@ -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)
Expand All @@ -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',
Expand All @@ -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
Expand Down Expand Up @@ -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 = []
Expand All @@ -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,
Expand Down
41 changes: 41 additions & 0 deletions test/e2e/app-dir/app-static/app-static.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand Down
Loading

0 comments on commit ce746ef

Please sign in to comment.