next
Version:
The React Framework
937 lines (936 loc) • 147 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "cache", {
enumerable: true,
get: function() {
return cache;
}
});
const _server = require("react-server-dom-webpack/server");
const _client = require("react-server-dom-webpack/client");
const _static = require("react-server-dom-webpack/static");
const _workasyncstorageexternal = require("../app-render/work-async-storage.external");
const _workunitasyncstorageexternal = require("../app-render/work-unit-async-storage.external");
const _dynamicrenderingutils = require("../dynamic-rendering-utils");
const _manifestssingleton = require("../app-render/manifests-singleton");
const _encryption = require("../app-render/encryption");
const _invarianterror = require("../../shared/lib/invariant-error");
const _createerrorhandler = require("../app-render/create-error-handler");
const _errortelemetryutils = require("../../lib/error-telemetry-utils");
const _stringhash = /*#__PURE__*/ _interop_require_default(require("next/dist/compiled/string-hash"));
const _constants = require("./constants");
const _constants1 = require("../../lib/constants");
const _handlers = require("./handlers");
const _clonecacheentry = require("./clone-cache-entry");
const _approuterheaders = require("../../client/components/app-router-headers");
const _requestcookies = require("../web/spec-extension/adapters/request-cookies");
const _headers = require("../web/spec-extension/adapters/headers");
const _usecacheerrors = require("./use-cache-errors");
const _dynamicrendering = require("../app-render/dynamic-rendering");
const _searchparams = require("../request/search-params");
const _lazyresult = require("../lib/lazy-result");
const _dynamicaccessasyncstorageexternal = require("../app-render/dynamic-access-async-storage.external");
const _stagedrendering = require("../app-render/staged-rendering");
const _log = /*#__PURE__*/ _interop_require_wildcard(require("../../build/output/log"));
const _runtimereactsexternal = require("../runtime-reacts.external");
const _promisewithresolvers = require("../../shared/lib/promise-with-resolvers");
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function(nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interop_require_wildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
return {
default: obj
};
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {
__proto__: null
};
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
for(var key in obj){
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
/**
* Encapsulates a pending cache invocation for deduping. Manages lazy stream
* tee-ing (via fork()) and metadata access for both intra-request and
* cross-request joiners.
*/ class SharedCacheEntry {
constructor(stream, pendingMetadata){
this.stream = stream;
this.pendingMetadata = pendingMetadata;
}
/**
* Tee the stream: returns a copy for the caller, replaces the internal stream
* with the remaining branch for future callers. Both the leader and joiners
* call this — everyone gets a fork.
*/ fork() {
const [forked, remaining] = this.stream.tee();
this.stream = remaining;
return forked;
}
}
function ignoreReject() {}
/**
* Manages the deferred promise for a shared cache result, tracks which maps
* it's registered in, and drives cleanup from resolve/reject.
*
* For 'cached' results, cleanup is lazy: entries stay in the maps until
* metadata/collection resolves, giving late-arriving invocations a chance to
* join while the leader streams. For 'prerender-dynamic' and errors, cleanup
* is immediate.
*/ class ResolvableSharedCacheResult {
registerIn(map, key) {
map.set(key, this.deferred.promise);
this.registrations.push({
map,
key
});
}
resolve(result) {
this.deferred.resolve(result);
if (result.type === 'cached') {
result.entry.pendingMetadata.finally(this.cleanup.bind(this));
} else {
this.cleanup();
}
}
reject(error) {
// The promise stored in the dedup maps has no consumer unless a concurrent
// invocation joined it, so we attach a noop catch handler to prevent the
// rejection from being reported as unhandled. The leader rethrows the
// error into the render, which is where it's surfaced.
this.deferred.promise.catch(ignoreReject);
this.deferred.reject(error);
this.cleanup();
}
cleanup() {
for (const { map, key } of this.registrations){
map.delete(key);
}
}
constructor(){
this.deferred = (0, _promisewithresolvers.createPromiseWithResolvers)();
this.registrations = [];
}
}
/**
* Module-scope map for cross-request deduplication. Keyed by `cacheHandlerKey`
* (specific key on warm path, coarse key on cold path). Entries live only for
* the duration of the leader's invocation.
*/ const crossRequestPendingCacheInvocations = new Map();
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge';
const debug = process.env.NEXT_PRIVATE_DEBUG_CACHE ? console.debug.bind(console, 'use-cache:') : undefined;
const filterStackFrame = process.env.NODE_ENV !== 'production' ? require('../lib/source-maps').filterStackFrameDEV : undefined;
const findSourceMapURL = process.env.NODE_ENV !== 'production' ? require('../lib/source-maps').findSourceMapURLDEV : undefined;
const nestedCacheZeroRevalidateErrorMessage = `A "use cache" with zero \`revalidate\` is nested inside another "use cache" ` + `that has no explicit \`cacheLife\`, which is not allowed during ` + `prerendering. Add \`cacheLife()\` to the outer "use cache" to choose ` + `whether it should be prerendered (with non-zero \`revalidate\`) or remain ` + `dynamic (with zero \`revalidate\`). Read more: ` + `https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife`;
const nestedCacheShortExpireErrorMessage = `A "use cache" with short \`expire\` (under 5 minutes) is nested inside ` + `another "use cache" that has no explicit \`cacheLife\`, which is not ` + `allowed during prerendering. Add \`cacheLife()\` to the outer "use cache" ` + `to choose whether it should be prerendered (with longer \`expire\`) or remain ` + `dynamic (with short \`expire\`). Read more: ` + `https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife`;
// Tracks which root params each cache function has historically read. Used to
// compute the specific cache key upfront on subsequent invocations. In-memory
// only — after server restart, the coarse-key redirect entry in the cache
// handler provides fallback.
const knownRootParamsByFunctionId = new Map();
function addKnownRootParamNames(id, names) {
const existing = knownRootParamsByFunctionId.get(id);
if (existing) {
for (const name of names){
existing.add(name);
}
return existing;
}
const created = new Set(names);
knownRootParamsByFunctionId.set(id, created);
return created;
}
function computeRootParamsCacheKeySuffix(rootParams, paramNames) {
if (paramNames.size === 0) {
return '';
}
return JSON.stringify([
...paramNames
].sort().map((paramName)=>[
paramName,
rootParams[paramName]
]));
}
// Next-internal cookies that must not vary the private cache key, since they're
// not part of the application's own cookie state. The instant-navigation cookie
// toggles while a navigation lock is held, so including it would force spurious
// misses.
const COOKIES_EXCLUDED_FROM_PRIVATE_CACHE_KEY = new Set([
_approuterheaders.NEXT_INSTANT_TEST_COOKIE
]);
// Request and transport headers that must not vary the private cache key. They
// either differ between otherwise-equivalent requests, which would cause
// spurious misses (a browser reload adds `cache-control`/`pragma` that an
// initial navigation doesn't, and `accept`/`sec-fetch-*` differ between an HTML
// navigation and an RSC or prefetch request for the same page), or are
// connection- and proxy-level rather than application data. The `cookie` header
// is excluded because cookies are keyed separately below (via the dedicated
// cookie path, which applies `COOKIES_EXCLUDED_FROM_PRIVATE_CACHE_KEY`);
// including the raw header would duplicate them and reintroduce the cookies
// that path excludes. Header names are lowercased by `HeadersAdapter`, so every
// entry here is lowercase.
const HEADERS_EXCLUDED_FROM_PRIVATE_CACHE_KEY = new Set([
'accept',
'accept-encoding',
'cache-control',
'connection',
'cookie',
'if-match',
'if-modified-since',
'if-none-match',
'if-range',
'if-unmodified-since',
'keep-alive',
'pragma',
'priority',
'purpose',
'range',
'sec-fetch-dest',
'sec-fetch-mode',
'sec-fetch-site',
'sec-fetch-user',
'sec-purpose',
'te',
'upgrade',
'upgrade-insecure-requests',
'x-forwarded-for',
'x-forwarded-host',
'x-forwarded-port',
'x-forwarded-proto'
]);
// TODO: This varies the dev private cache key by the request's cookies and
// headers (minus the transport and content-negotiation headers excluded above).
// It's a heuristic: it still over-keys (a cache that reads only one cookie or
// header varies by all of them) and the header denylist is necessarily
// incomplete. Follow up by tracking which cookies and headers a cache function
// actually reads (the same mechanism root params use via `readRootParamNames`)
// and keying by only those. Note that Next-internal flight headers such as
// `rsc` and `next-router-state-tree` are already stripped upstream in
// `getHeaders`, so they never appear here.
function computePrivateCacheKeyRequestSuffix(cookies, headers) {
const relevantCookies = cookies.getAll().filter((cookie)=>!COOKIES_EXCLUDED_FROM_PRIVATE_CACHE_KEY.has(cookie.name)).map((cookie)=>[
cookie.name,
cookie.value
]).sort(([nameA], [nameB])=>nameA < nameB ? -1 : nameA > nameB ? 1 : 0);
const relevantHeaders = [
...headers.entries()
].filter(([name])=>!HEADERS_EXCLUDED_FROM_PRIVATE_CACHE_KEY.has(name)).sort(([nameA], [nameB])=>nameA < nameB ? -1 : nameA > nameB ? 1 : 0);
if (relevantCookies.length === 0 && relevantHeaders.length === 0) {
return '';
}
return JSON.stringify({
cookies: relevantCookies,
headers: relevantHeaders
});
}
function saveToResumeDataCache(resumeDataCache, serializedCacheKey, pendingCacheResult) {
if (!(resumeDataCache == null ? void 0 : resumeDataCache.mutable)) {
return pendingCacheResult;
}
const split = clonePendingCacheResult(pendingCacheResult);
const savedCacheResult = getNthCacheResult(split, 0);
const rdcResult = getNthCacheResult(split, 1);
// The RDC is per-page and root params are fixed within a page, so we always
// use the coarse key (without root param suffix). Unlike the cache handler,
// the RDC doesn't need root-param-specific keys for isolation.
resumeDataCache.cache.set(serializedCacheKey, rdcResult);
debug == null ? void 0 : debug('Resume Data Cache entry saved', serializedCacheKey);
return savedCacheResult;
}
/**
* A joiner's RDC context may differ from the leader's:
*
* - Intra-request: the leader was nested inside another cache (no accessible
* RDC) while this joiner is top-level and has one.
* - Cross-request: the leader belongs to a different request entirely — this
* request's RDC has never seen the entry.
*
* In both cases the joiner must save to its own RDC so its final prerender can
* resume from the entry. Constructs a `CollectedCacheResult` from a forked
* stream branch of the shared entry and the awaited metadata.
*
* The `cache.has()` guard avoids redundant saves when the intra-request leader
* already saved to the same RDC. Without it, this would needlessly tee the
* stream and overwrite an equivalent RDC entry.
*/ function saveSharedCacheEntryToResumeDataCache(serializedCacheKey, sharedCacheEntry, resumeDataCache) {
if (!(resumeDataCache == null ? void 0 : resumeDataCache.mutable) || resumeDataCache.cache.has(serializedCacheKey)) {
return;
}
const rdcResult = sharedCacheEntry.pendingMetadata.then((metadata)=>({
entry: {
value: sharedCacheEntry.fork(),
tags: metadata.tags,
revalidate: metadata.revalidate,
expire: metadata.expire,
stale: metadata.stale,
timestamp: metadata.timestamp
},
readRootParamNames: metadata.readRootParamNames,
hasExplicitRevalidate: metadata.hasExplicitRevalidate,
hasExplicitExpire: metadata.hasExplicitExpire,
dynamicNestedCacheError: metadata.dynamicNestedCacheError
}));
resumeDataCache.cache.set(serializedCacheKey, rdcResult);
debug == null ? void 0 : debug('Resume Data Cache entry saved by joiner', serializedCacheKey);
}
function saveToCacheHandler(cacheHandler, workStore, id, cacheHandlerKeyBase, savedCacheResult, rootParams) {
// Write the entry to the cache handler. With root params, this is a redirect
// entry at the coarse key plus the actual entry at the specific key;
// otherwise just the entry at the coarse key. Both set calls are fired
// together and awaited in parallel.
const combinedSetPromise = savedCacheResult.then(async (collectedResult)=>{
const { entry: fullEntry, readRootParamNames } = collectedResult;
// Use the combined set (union of all historically observed reads) for both
// the specific key and the redirect entry's tags. The read path computes
// cacheHandlerKey from this same union (knownRootParamsByFunctionId), so
// the write path must use the identical set to land on the same specific
// key. If we used only the current invocation's reads, a function that
// conditionally reads different root params across invocations would
// scatter entries across different specific keys, making previous entries
// unreachable from the read path's union-based lookup.
const rootParamNames = readRootParamNames ? addKnownRootParamNames(id, readRootParamNames) : knownRootParamsByFunctionId.get(id);
const setPromises = [];
let coarseEntry = fullEntry;
if (rootParamNames && rootParamNames.size > 0 && rootParams) {
const specificKey = cacheHandlerKeyBase + computeRootParamsCacheKeySuffix(rootParams, rootParamNames);
setPromises.push(cacheHandler.set(specificKey, Promise.resolve(fullEntry)));
// The coarse key gets a redirect entry instead. On a cold server (empty
// knownRootParamsByFunctionId), its tags tell a reader which root params
// to include in the specific-key lookup.
const rootParamTags = [
...rootParamNames
].map((paramName)=>_constants1.NEXT_CACHE_ROOT_PARAM_TAG_ID + paramName);
coarseEntry = {
value: new ReadableStream({
start (controller) {
// Single byte so the entry has non-zero size in LRU caches.
controller.enqueue(new Uint8Array([
0
]));
controller.close();
}
}),
tags: [
...fullEntry.tags,
...rootParamTags
],
stale: fullEntry.stale,
timestamp: fullEntry.timestamp,
expire: fullEntry.expire,
revalidate: fullEntry.revalidate
};
}
setPromises.push(cacheHandler.set(cacheHandlerKeyBase, Promise.resolve(coarseEntry)));
await Promise.all(setPromises);
});
workStore.pendingRevalidateWrites ??= [];
workStore.pendingRevalidateWrites.push(combinedSetPromise);
// A cross-request joiner reads its recomputed specific key only after it has
// awaited this entry's metadata, so gate the metadata on the writes landing:
// that guarantees the entry is present when the joiner re-reads. A failed
// write shouldn't reject the metadata (the joiner just misses and
// regenerates), so settle either way; a collection failure still propagates
// through `savedCacheResult`.
return combinedSetPromise.then(()=>savedCacheResult, ()=>savedCacheResult);
}
function generateCacheEntry(workStore, cacheContext, clientReferenceManifest, encodedArguments, fn, timeoutError, deadlockError) {
// We need to run this inside a clean AsyncLocalStorage snapshot so that the cache
// generation cannot read anything from the context we're currently executing which
// might include request specific things like cookies() inside a React.cache().
// Note: It is important that we await at least once before this because it lets us
// pop out of any stack specific contexts as well - aka "Sync" Local Storage.
return workStore.runInCleanSnapshot(generateCacheEntryWithRestoredWorkStore, workStore, cacheContext, clientReferenceManifest, encodedArguments, fn, timeoutError, deadlockError);
}
function generateCacheEntryWithRestoredWorkStore(workStore, cacheContext, clientReferenceManifest, encodedArguments, fn, timeoutError, deadlockError) {
// Since we cleared the AsyncLocalStorage we need to restore the workStore.
// Note: We explicitly don't restore the RequestStore nor the PrerenderStore.
// We don't want any request specific information leaking an we don't want to create a
// bloated fake request mock for every cache call. So any feature that currently lives
// in RequestStore but should be available to Caches need to move to WorkStore.
// PrerenderStore is not needed inside the cache scope because the outer most one will
// be the one to report its result to the outer Prerender.
return _workasyncstorageexternal.workAsyncStorage.run(workStore, generateCacheEntryWithCacheContext, workStore, cacheContext, clientReferenceManifest, encodedArguments, fn, timeoutError, deadlockError);
}
function createUseCacheStore(workStore, cacheContext, defaultCacheLife) {
if (cacheContext.kind === 'private') {
const outerWorkUnitStore = cacheContext.outerWorkUnitStore;
return {
type: 'private-cache',
phase: 'render',
implicitTags: outerWorkUnitStore == null ? void 0 : outerWorkUnitStore.implicitTags,
revalidate: defaultCacheLife.revalidate,
expire: defaultCacheLife.expire,
stale: defaultCacheLife.stale,
explicitRevalidate: undefined,
explicitExpire: undefined,
explicitStale: undefined,
tags: null,
hmrRefreshHash: (0, _workunitasyncstorageexternal.getHmrRefreshHash)(outerWorkUnitStore),
isHmrRefresh: (0, _workunitasyncstorageexternal.isHmrRefresh)(outerWorkUnitStore),
serverComponentsHmrCache: (0, _workunitasyncstorageexternal.getServerComponentsHmrCache)(outerWorkUnitStore),
forceRevalidate: shouldForceRevalidate(workStore, outerWorkUnitStore),
draftMode: (0, _workunitasyncstorageexternal.getDraftModeProviderForCacheScope)(workStore, outerWorkUnitStore),
rootParams: outerWorkUnitStore.rootParams,
readRootParamNames: process.env.__NEXT_DEV_SERVER ? new Set() : undefined,
// Every private cache scope is its own work unit. Any cache keyed on
// headers() or cookies() needs to be invalidated. Otherwise some
// Next.js API semantics leak across render passes.
headers: _headers.HeadersAdapter.fresh(outerWorkUnitStore.headers),
cookies: _requestcookies.RequestCookiesAdapter.fresh(outerWorkUnitStore.cookies),
outerOwnerStack: cacheContext.outerOwnerStack
};
} else {
let useCacheOrRequestStore;
const outerWorkUnitStore = cacheContext.outerWorkUnitStore;
switch(outerWorkUnitStore.type){
case 'cache':
case 'private-cache':
case 'request':
useCacheOrRequestStore = outerWorkUnitStore;
break;
case 'prerender-runtime':
case 'prerender':
case 'prerender-ppr':
case 'prerender-legacy':
case 'unstable-cache':
case 'generate-static-params':
break;
default:
outerWorkUnitStore;
}
return {
type: 'cache',
phase: 'render',
implicitTags: outerWorkUnitStore.implicitTags,
revalidate: defaultCacheLife.revalidate,
expire: defaultCacheLife.expire,
stale: defaultCacheLife.stale,
explicitRevalidate: undefined,
explicitExpire: undefined,
explicitStale: undefined,
tags: null,
hmrRefreshHash: (0, _workunitasyncstorageexternal.getHmrRefreshHash)(outerWorkUnitStore),
isHmrRefresh: (useCacheOrRequestStore == null ? void 0 : useCacheOrRequestStore.isHmrRefresh) ?? false,
serverComponentsHmrCache: useCacheOrRequestStore == null ? void 0 : useCacheOrRequestStore.serverComponentsHmrCache,
forceRevalidate: shouldForceRevalidate(workStore, outerWorkUnitStore),
draftMode: (0, _workunitasyncstorageexternal.getDraftModeProviderForCacheScope)(workStore, outerWorkUnitStore),
rootParams: outerWorkUnitStore.rootParams,
readRootParamNames: new Set(),
outerOwnerStack: cacheContext.outerOwnerStack,
dynamicNestedCacheError: undefined
};
}
}
/**
* Captures the owner stack from the outer component tree before entering a
* cache boundary. When nested inside another cache scope, the parent's
* outerOwnerStack is concatenated so that the full component tree is preserved
* across multiple cache boundaries.
*/ function captureOuterOwnerStack(workUnitStore) {
var _getClientReact_captureOwnerStack, _getClientReact, _getServerReact_captureOwnerStack, _getServerReact;
const capturedOwnerStack = (((_getClientReact = (0, _runtimereactsexternal.getClientReact)()) == null ? void 0 : (_getClientReact_captureOwnerStack = _getClientReact.captureOwnerStack) == null ? void 0 : _getClientReact_captureOwnerStack.call(_getClientReact)) ?? ((_getServerReact = (0, _runtimereactsexternal.getServerReact)()) == null ? void 0 : (_getServerReact_captureOwnerStack = _getServerReact.captureOwnerStack) == null ? void 0 : _getServerReact_captureOwnerStack.call(_getServerReact))) || '';
let parentOuterOwnerStack;
switch(workUnitStore.type){
case 'cache':
case 'private-cache':
parentOuterOwnerStack = workUnitStore.outerOwnerStack;
break;
case 'unstable-cache':
case 'request':
case 'prerender':
case 'prerender-ppr':
case 'prerender-legacy':
case 'prerender-runtime':
case 'prerender-client':
case 'validation-client':
case 'generate-static-params':
break;
default:
workUnitStore;
}
return capturedOwnerStack + (parentOuterOwnerStack || '') || undefined;
}
// The maximum time we allow a `'use cache'` entry to fill. After this, we
// assume the fill is stalled — either on hanging input to the cached function,
// or on hanging I/O inside of it — and de-opt with an error.
//
// For prerender, the effective value is clamped to 90% of the configured
// `staticPageGenerationTimeout` so the cache-fill error surfaces before the
// build worker kills the page. In dev (`request`), the configured
// `experimental.useCacheTimeout` is used straight.
function getUseCacheFillTimeoutMs(workStore, workUnitStoreType) {
const { useCacheTimeout, staticPageGenerationTimeout } = workStore;
const effectiveTimeout = workUnitStoreType === 'request' ? useCacheTimeout : Math.min(useCacheTimeout, staticPageGenerationTimeout * 0.9);
return effectiveTimeout * 1000;
}
function generateCacheEntryWithCacheContext(workStore, cacheContext, clientReferenceManifest, encodedArguments, fn, timeoutError, deadlockError) {
const defaultCacheLife = workStore.cacheLifeProfiles.default;
// Initialize the Store for this Cache entry.
const cacheStore = createUseCacheStore(workStore, cacheContext, defaultCacheLife);
return _workunitasyncstorageexternal.workUnitAsyncStorage.run(cacheStore, ()=>_dynamicaccessasyncstorageexternal.dynamicAccessAsyncStorage.run({
abortController: new AbortController()
}, generateCacheEntryImpl, workStore, cacheContext, cacheStore, clientReferenceManifest, encodedArguments, fn, timeoutError, deadlockError));
}
function propagateCacheLifeAndTagsToRevalidateStore(revalidateStore, metadata) {
const outerTags = revalidateStore.tags ??= [];
for (const tag of metadata.tags){
if (!outerTags.includes(tag)) {
outerTags.push(tag);
}
}
if (revalidateStore.stale > metadata.stale) {
revalidateStore.stale = metadata.stale;
}
if (revalidateStore.revalidate > metadata.revalidate) {
revalidateStore.revalidate = metadata.revalidate;
}
if (revalidateStore.expire > metadata.expire) {
revalidateStore.expire = metadata.expire;
}
}
function propagateCacheStaleTimeToRequestStore(requestStore, metadata) {
if (requestStore.stale !== undefined && requestStore.stale > metadata.stale) {
requestStore.stale = metadata.stale;
}
}
function propagateCacheEntryMetadata(cacheContext, metadata) {
if (cacheContext.kind === 'private') {
switch(cacheContext.outerWorkUnitStore.type){
case 'prerender-runtime':
case 'private-cache':
propagateCacheLifeAndTagsToRevalidateStore(cacheContext.outerWorkUnitStore, metadata);
break;
case 'request':
propagateCacheStaleTimeToRequestStore(cacheContext.outerWorkUnitStore, metadata);
break;
case undefined:
break;
default:
cacheContext.outerWorkUnitStore;
}
} else {
switch(cacheContext.outerWorkUnitStore.type){
case 'cache':
if (metadata.readRootParamNames) {
for (const paramName of metadata.readRootParamNames){
cacheContext.outerWorkUnitStore.readRootParamNames.add(paramName);
}
}
// If this entry's cache life is dynamic, record this invocation as the
// origin to use as `cause` when the outer cache surfaces the
// nested-dynamic cache error. `??=` keeps the first occurrence so the
// cause points at the immediate dynamic child.
if (cacheContext.dynamicNestedCacheError !== undefined && (metadata.revalidate === 0 || metadata.expire < _constants.MIN_PRERENDERABLE_EXPIRE)) {
cacheContext.outerWorkUnitStore.dynamicNestedCacheError ??= cacheContext.dynamicNestedCacheError;
}
// fallthrough
case 'private-cache':
case 'prerender':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
propagateCacheLifeAndTagsToRevalidateStore(cacheContext.outerWorkUnitStore, metadata);
break;
case 'request':
propagateCacheStaleTimeToRequestStore(cacheContext.outerWorkUnitStore, metadata);
break;
case 'unstable-cache':
case 'generate-static-params':
break;
default:
cacheContext.outerWorkUnitStore;
}
}
}
/**
* Conditionally propagates cache life, tags, and root param names to the outer
* context. During prerenders (`prerender` / `prerender-runtime`) and dev
* cache-filling requests, propagation is deferred because the entry might be
* omitted from the final prerender due to short expire/stale times. If omitted,
* it should not affect the prerender. The final decision happens when the entry
* is read from the resume data cache in the final render phase — at that point
* `propagateCacheEntryMetadata` is called unconditionally (after the omission
* checks have already filtered out short-lived entries).
*
* Note: Root param names are only propagated when the outer context is a
* `cache` store (i.e. an enclosing `"use cache"` function), which is never
* deferred. For prerender contexts, root param names are tracked separately
* via `addKnownRootParamNames` in the resume data cache read path.
*/ function maybePropagateCacheEntryMetadata(cacheContext, metadata) {
const outerWorkUnitStore = cacheContext.outerWorkUnitStore;
switch(outerWorkUnitStore.type){
case 'prerender':
case 'prerender-runtime':
{
break;
}
case 'request':
{
if (process.env.NODE_ENV === 'development' && outerWorkUnitStore.cacheSignal) {
break;
}
// fallthrough
}
case 'private-cache':
case 'cache':
case 'unstable-cache':
case 'prerender-legacy':
case 'prerender-ppr':
{
propagateCacheEntryMetadata(cacheContext, metadata);
break;
}
case 'generate-static-params':
break;
default:
{
outerWorkUnitStore;
}
}
}
async function collectResult(savedStream, workStore, cacheContext, innerCacheStore, startTime, errors) {
// We create a buffered stream that collects all chunks until the end to
// ensure that RSC has finished rendering and therefore we have collected
// all tags. In the future the RSC API might allow for the equivalent of
// the allReady Promise that exists on SSR streams.
//
// If something errored or rejected anywhere in the render, we close
// the stream as errored. This lets a CacheHandler choose to save the
// partial result up until that point for future hits for a while to avoid
// unnecessary retries or not to retry. We use the end of the stream for
// this to avoid another complicated side-channel. A receiver has to consider
// that the stream might also error for other reasons anyway such as losing
// connection.
const buffer = [];
const reader = savedStream.getReader();
try {
for(let entry; !(entry = await reader.read()).done;){
buffer.push(entry.value);
}
} catch (error) {
errors.push(error);
}
let idx = 0;
const bufferStream = new ReadableStream({
pull (controller) {
if (workStore.invalidDynamicUsageError) {
controller.error(workStore.invalidDynamicUsageError);
} else if (idx < buffer.length) {
controller.enqueue(buffer[idx++]);
} else if (errors.length > 0) {
// TODO: Should we use AggregateError here?
controller.error(errors[0]);
} else {
controller.close();
}
}
});
const collectedTags = innerCacheStore.tags;
const isPrivateCacheInDev = Boolean(process.env.__NEXT_DEV_SERVER && cacheContext.kind === 'private');
// In development, force a dynamic cache life (`revalidate: 0`, `expire:
// MIN_PRERENDERABLE_EXPIRE`) for private caches, which have no real backing
// handler. The zero revalidate makes every read serve stale-while-revalidate
// (regenerating a fresh entry in the background), and
// `MIN_PRERENDERABLE_EXPIRE` (5 minutes) caps how long an entry lingers in
// the dedicated in-memory private handler. It is the shortest `expire` that
// isn't treated as dynamic; a smaller `expire` would exclude the entry from
// prerenders. Two other cases deliberately do NOT force this and keep their
// resolved cache life, relying instead on the dev handler's minimum retention
// and a dev revalidation (see the cache-hit path below) to keep reloads fast
// and fresh. The size-0 case (`cacheMaxMemorySize: 0`) keeps its life so the
// entry can be considered prerenderable instead of being misread as a dynamic
// hole. An explicit short-`expire` public cache (e.g. `cacheLife({ expire: 0
// })`) keeps its life so it stays correctly excluded from static prerenders
// via its real `expire` while a reload still hits the cache; forcing
// `revalidate: 0` here would instead corrupt the cache life propagated to an
// enclosing cache and trigger the nested-dynamic error. A cache backed by a
// custom handler keeps its real cache life too, since that handler owns it.
const forceDynamicCacheLifeInDev = isPrivateCacheInDev;
// If cacheLife() was used to set an explicit revalidate/expire/stale time we
// use that. Otherwise, we use the lowest of all inner fetch(),
// unstable_cache() or nested "use cache", if they're lower than our default.
const collectedRevalidate = forceDynamicCacheLifeInDev ? 0 : innerCacheStore.explicitRevalidate !== undefined ? innerCacheStore.explicitRevalidate : innerCacheStore.revalidate;
const collectedExpire = forceDynamicCacheLifeInDev ? _constants.MIN_PRERENDERABLE_EXPIRE : innerCacheStore.explicitExpire !== undefined ? innerCacheStore.explicitExpire : innerCacheStore.expire;
const collectedStale = innerCacheStore.explicitStale !== undefined ? innerCacheStore.explicitStale : innerCacheStore.stale;
const entry = {
value: bufferStream,
timestamp: startTime,
revalidate: collectedRevalidate,
expire: collectedExpire,
stale: collectedStale,
tags: collectedTags === null ? [] : collectedTags
};
const collected = {
entry,
hasExplicitRevalidate: innerCacheStore.explicitRevalidate !== undefined,
hasExplicitExpire: innerCacheStore.explicitExpire !== undefined,
readRootParamNames: innerCacheStore.type === 'cache' || isPrivateCacheInDev ? innerCacheStore.readRootParamNames : undefined,
// The store accumulates this from nested public caches that propagated a
// dynamic life into us.
dynamicNestedCacheError: innerCacheStore.type === 'cache' ? innerCacheStore.dynamicNestedCacheError : undefined
};
if (!cacheContext.skipPropagation) {
maybePropagateCacheEntryMetadata(cacheContext, {
tags: collected.entry.tags,
revalidate: collected.entry.revalidate,
expire: collected.entry.expire,
stale: collected.entry.stale,
timestamp: collected.entry.timestamp,
hasExplicitRevalidate: collected.hasExplicitRevalidate,
hasExplicitExpire: collected.hasExplicitExpire,
readRootParamNames: collected.readRootParamNames,
dynamicNestedCacheError: collected.dynamicNestedCacheError
});
const cacheSignal = (0, _workunitasyncstorageexternal.getCacheSignal)(cacheContext.outerWorkUnitStore);
if (cacheSignal) {
cacheSignal.endRead();
}
}
return collected;
}
async function generateCacheEntryImpl(workStore, cacheContext, innerCacheStore, clientReferenceManifest, encodedArguments, fn, timeoutError, deadlockError) {
const temporaryReferences = (0, _server.createTemporaryReferenceSet)();
const outerWorkUnitStore = cacheContext.outerWorkUnitStore;
const [, , args] = typeof encodedArguments === 'string' ? await (0, _server.decodeReply)(encodedArguments, (0, _manifestssingleton.getServerModuleMap)(), {
temporaryReferences
}) : await (0, _server.decodeReplyFromAsyncIterable)({
async *[Symbol.asyncIterator] () {
for (const entry of encodedArguments){
yield entry;
}
switch(outerWorkUnitStore.type){
case 'prerender-runtime':
case 'prerender':
// The encoded arguments might contain hanging promises. In
// this case we don't want to reject with "Error: Connection
// closed.", so we intentionally keep the iterable alive. This
// is similar to the halting trick that we do while rendering.
await new Promise((resolve)=>{
if (outerWorkUnitStore.renderSignal.aborted) {
resolve();
} else {
outerWorkUnitStore.renderSignal.addEventListener('abort', ()=>resolve(), {
once: true
});
}
});
break;
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
case 'private-cache':
case 'unstable-cache':
case 'generate-static-params':
break;
default:
outerWorkUnitStore;
}
}
}, (0, _manifestssingleton.getServerModuleMap)(), {
temporaryReferences
});
// Track the timestamp when we started computing the result.
const startTime = performance.timeOrigin + performance.now();
// Invoke the inner function to load a new result. We delay the invocation
// though, until React awaits the promise so that React's request store (ALS)
// is available when the function is invoked. This allows us, for example, to
// capture logs so that we can later replay them.
const resultPromise = (0, _lazyresult.createLazyResult)(fn.bind(null, ...args));
const errors = [];
// In the "Cache" environment, we only need to make sure that the error
// digests are handled correctly. Error formatting and reporting is not
// necessary here; the errors are encoded in the stream, and will be reported
// in the "Server" environment.
const handleError = (0, _createerrorhandler.createReactServerErrorHandler)(process.env.NODE_ENV === 'development', workStore.isBuildTimePrerendering ?? false, workStore.reactServerErrorsByDigest, (error)=>{
// In production, we log the original error here. It gets a digest that
// can be used to associate the error with the obfuscated error that might
// be logged if the error is caught. In development, we prefer logging the
// transported error in the server environment. It's not obfuscated and
// also includes the (dev-only) environment name.
if (process.env.NODE_ENV === 'production') {
_log.error(error);
}
errors.push(error);
});
let stream;
let devTimeoutAbortController;
switch(outerWorkUnitStore.type){
case 'prerender-runtime':
case 'prerender':
{
var _dynamicAccessAsyncStorage_getStore;
const timeoutAbortController = new AbortController();
const timer = setTimeout(()=>{
workStore.invalidDynamicUsageError = timeoutError;
timeoutAbortController.abort(timeoutError);
}, getUseCacheFillTimeoutMs(workStore, outerWorkUnitStore.type));
const dynamicAccessAbortSignal = (_dynamicAccessAsyncStorage_getStore = _dynamicaccessasyncstorageexternal.dynamicAccessAsyncStorage.getStore()) == null ? void 0 : _dynamicAccessAsyncStorage_getStore.abortController.signal;
const abortSignal = dynamicAccessAbortSignal ? AbortSignal.any([
dynamicAccessAbortSignal,
timeoutAbortController.signal
]) : timeoutAbortController.signal;
const { prelude } = await (0, _static.prerender)(resultPromise, clientReferenceManifest.clientModules, {
environmentName: 'Cache',
filterStackFrame,
signal: abortSignal,
temporaryReferences,
onError (error) {
if (abortSignal.aborted && abortSignal.reason === error) {
return undefined;
}
return handleError(error);
}
});
clearTimeout(timer);
if (timeoutAbortController.signal.aborted) {
// When the timeout is reached we always error the stream. Even for
// fallback shell prerenders we don't want to return a hanging promise,
// which would allow the function to become a dynamic hole. Because that
// would mean that a non-empty shell could be generated which would be
// subject to revalidation, and we don't want to create long
// revalidation times.
stream = new ReadableStream({
start (controller) {
controller.error(timeoutAbortController.signal.reason);
}
});
} else if (dynamicAccessAbortSignal == null ? void 0 : dynamicAccessAbortSignal.aborted) {
// If the prerender is aborted because of dynamic access (e.g. reading
// fallback params), we return a hanging promise. This essentially makes
// the "use cache" function dynamic.
// The dynamic access is a fallback params read, which is runtime data.
const hangingPromise = (0, _dynamicrenderingutils.makeRuntimeHangingPromise)(outerWorkUnitStore.renderSignal, workStore.route, 'dynamic "use cache"', outerWorkUnitStore);
if (outerWorkUnitStore.cacheSignal) {
outerWorkUnitStore.cacheSignal.endRead();
}
return {
type: 'prerender-dynamic',
hangingPromise
};
} else {
stream = prelude;
}
break;
}
case 'request':
// TODO: We should just check if the render is abandonable. This is
// relevant in restart-on-cache-miss in general, so when we implement that
// for cached navs, it'll also be needed in prod
if (process.env.__NEXT_DEV_SERVER && outerWorkUnitStore.cacheSignal) {
const stagedRendering = outerWorkUnitStore.stagedRendering;
// Capture the render stage at the start of this cache read, before the
// yield below. A streamed staged render advances its controller on its
// own schedule, independently of this read, so by the time the yield
// resolves the controller may have raced ahead to the Dynamic stage even
// though the read began in an earlier (prerender) stage.
const stageAtReadStart = stagedRendering == null ? void 0 : stagedRendering.currentStage;
// If we're filling caches for a staged render, make sure that it takes
// at least a task, so we'll always notice a cache miss between stages.
//
// TODO(restart-on-cache-miss): This is suboptimal. Ideally microtasky
// caches wouldn't register as a miss, but short-lived caches are only
// omitted correctly when read back in a separate render (now the
// background validation render, not a restart of the streamed
// response), so forcing the miss is the best we can do until that's
// refactored.
await new Promise((resolve)=>setTimeout(resolve));
// Start a cache-fill timeout so a hanging `'use cache'` entry surfaces
// the same error in dev as during prerender. Cleared when
// pendingCacheResult settles.
//
// Skip the timeout only when the read began in the Dynamic stage, which
// mirrors prerender: a cache guarded by e.g. `await connection()` is a
// legitimate dynamic hole and isn't executed there. We use the stage
// captured at read start, not the current one, because the staged render
// may have advanced past it during the yield above.
if (stageAtReadStart !== _stagedrendering.RenderStage.Dynamic) {
const devRenderAbortController = new AbortController();
const fillTimeoutMs = getUseCacheFillTimeoutMs(workStore, outerWorkUnitStore.type);
const fillDeadlineAt = performance.now() + fillTimeoutMs;
const devRenderTimeoutTimer = setTimeout(()=>{
workStore.invalidDynamicUsageError = timeoutError;
devRenderAbortController.abort(timeoutError);
}, fillTimeoutMs);
devTimeoutAbortController = new AbortController();
devTimeoutAbortController.signal.addEventListener('abort', ()=>{
clearTimeout(devRenderTimeoutTimer);
}, {
once: true
});
stream = (0, _server.renderToReadableStream)(resultPromise, clientReferenceManifest.clientModules, {
environmentName: 'Cache',
filterStackFrame,
signal: devRenderAbortController.signal,
temporaryReferences,
onError (error) {
if (devRenderAbortController.signal.aborted && devRenderAbortController.signal.reason === error && error instanceof Error) {
// The abort reason is the same error stored as
// `workStore.invalidDynamicUsageError` (a fill timeout or
// deadlock). Register it under a digest and return that
// digest, so the error that surfaces on the consumer side of
// this Flight boundary carries it and the outer render's
// handler can recover *this* object via
// `reactServerErrorsByDigest`.
//
// We deliberately do not set `error.digest` here: whether the
// error actually surfaces (vs. being caught in userland) is
// the consumer's decision, so the "surfaced" mark is left to
// the outer handler.
const digest = (0, _errortelemetryutils.createDigestWithErrorCode)(error, (0, _stringhash.default)(error.message + (error.stack || '')).toString());
workStore.reactServerErrorsByDigest.set(digest, error);
return digest;
}
return handleError(error);
}
});
// `require` (rather than a top-level import) so the bundler can
// tree-shake the probe scheduler out of the production runtime, where
// this whole dev-server-gated branch is dead code.
const { setupProbeScheduler } = require('./use-cache-probe-scheduler');
stream = setupProbeScheduler({
workStore,
outerRequestStore: outerWorkUnitStore,
cacheContext,
encodedArguments,
fillDeadlineAt,
stream,
abortSignal: AbortSignal.any([
devRenderAbortController.signal,
devTimeoutAbortController.signal
]),
onProbeCompleted () {
const error = deadlockError ?? Object.defineProperty(new _invarianterror.InvariantError('`deadlockError` should be constructed inside `cache()` before reaching the probe scheduler.'), "__NEXT_ERROR_CODE", {