UNPKG

next

Version:

The React Framework

937 lines (936 loc) 147 kB
"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", {