mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
96e9a8e246
When Cache Components is enabled, `next dev` treats a `'use cache'` value as a miss and renders as if the cache were empty whenever the read does not resolve right away. Two development configurations triggered that even for values that were already cached, so warm reloads streamed slowly instead of serving the cached value: `cacheMaxMemorySize: 0` replaced the built-in default handler with a no-op stub, so nothing was cached at all, and custom cache handlers with a slow or remote `get` did not return in time. For the size-0 case, development now uses a real in-memory handler instead of the no-op stub, and the `'use cache'` wrapper forces a dynamic cache life (`revalidate: 0`, a 5-minute `expire`) for it, the same treatment private caches already receive, so every read serves the stale entry and re-warms a fresh one in the background. This also fixes the dev private handler, which was sized from `cacheMaxMemorySize` and so degraded to the no-op stub whenever `cacheMaxMemorySize: 0` was set. Custom cache handlers keep their configured cache life, since their backing owns it. Instead we put a fast built-in in-memory front handler in front of the configured one through the new `TieredCacheHandler`, which serves warm reads from the front, writes through to both tiers, and reconciles the front against the backing in the background, evicting the front entry when the backing no longer has it (the handler interface has no per-key delete, so it overwrites the entry with an already-expired copy). These dev-only handlers are kept out of the registered handler set and merged in only where tag operations iterate, so `revalidateTag` still reaches them. Everything is gated on `process.env.__NEXT_DEV_SERVER`, so production is unchanged: `cacheMaxMemorySize: 0` still caches nothing, private entries are still never persisted, and configured handlers are used directly. New development test suites cover the size-0 and custom-handler behavior.
85 lines
2.2 KiB
JavaScript
85 lines
2.2 KiB
JavaScript
// @ts-check
|
|
|
|
const { setTimeout } = require('timers/promises')
|
|
|
|
/**
|
|
* A persistent cache handler that simulates a remote cache: its `get` resolves
|
|
* on a macro task (after a delay) rather than in a microtask. Without a
|
|
* built-in in-memory front, a warm read would therefore still be pending at a
|
|
* staged render boundary in dev and be reported as a cold-cache miss. The
|
|
* tiered handler fronts this so warm reads resolve in a microtask instead.
|
|
*/
|
|
|
|
/** @type {Map<string, import('next/dist/server/lib/cache-handlers/types').CacheEntry>} */
|
|
const store = new Map()
|
|
|
|
// Let a route handler purge this backing store out-of-band, so a test can
|
|
// verify the tiered front handler stops serving its cached entry afterwards.
|
|
/** @type {any} */
|
|
const globalScope = globalThis
|
|
globalScope.__purgeUseCacheBacking = () => {
|
|
store.clear()
|
|
}
|
|
|
|
/** @type {Map<string, Promise<void>>} */
|
|
const pendingSets = new Map()
|
|
|
|
/**
|
|
* @type {import('next/dist/server/lib/cache-handlers/types').CacheHandler}
|
|
*/
|
|
const cacheHandler = {
|
|
async get(cacheKey) {
|
|
const pendingPromise = pendingSets.get(cacheKey)
|
|
if (pendingPromise) {
|
|
await pendingPromise
|
|
}
|
|
|
|
// Simulate the latency of fetching from a remote cache.
|
|
await setTimeout(200)
|
|
|
|
const entry = store.get(cacheKey)
|
|
if (!entry) {
|
|
return undefined
|
|
}
|
|
|
|
const [returnStream, savedStream] = entry.value.tee()
|
|
entry.value = savedStream
|
|
return { ...entry, value: returnStream }
|
|
},
|
|
|
|
async set(cacheKey, pendingEntry) {
|
|
/** @type {() => void} */
|
|
let resolvePending = () => {}
|
|
const pendingPromise = new Promise((resolve) => {
|
|
resolvePending = /** @type {() => void} */ (resolve)
|
|
})
|
|
pendingSets.set(cacheKey, pendingPromise)
|
|
|
|
try {
|
|
const entry = await pendingEntry
|
|
const [value, clonedValue] = entry.value.tee()
|
|
entry.value = value
|
|
|
|
// Consume the cloned stream so the entry is fully resolved before
|
|
// storing.
|
|
const reader = clonedValue.getReader()
|
|
while (!(await reader.read()).done) {}
|
|
|
|
store.set(cacheKey, entry)
|
|
} finally {
|
|
resolvePending()
|
|
pendingSets.delete(cacheKey)
|
|
}
|
|
},
|
|
|
|
async refreshTags() {},
|
|
|
|
async getExpiration() {
|
|
return 0
|
|
},
|
|
|
|
async updateTags() {},
|
|
}
|
|
|
|
module.exports = cacheHandler
|