next
Version:
The React Framework
974 lines • 150 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
EntryStatus: null,
MetadataOnlyRequestTree: null,
attemptToFulfillDynamicSegmentFromBFCache: null,
attemptToUpgradeSegmentFromBFCache: null,
canNewFetchStrategyProvideMoreContent: null,
convertReusedFlightRouterStateToRouteTree: null,
convertRootFlightRouterStateToRouteTree: null,
convertRouteTreeToFlightRouterState: null,
createDetachedSegmentCacheEntry: null,
createMetadataRouteTree: null,
createNonTaskyPrefetchResponseStream: null,
deprecated_requestOptimisticRouteCacheEntry: null,
fetchRouteOnCacheMiss: null,
fetchSegmentPrefetchesUsingDynamicRequest: null,
fetchSegmentsOnCacheMiss: null,
fulfillRouteCacheEntry: null,
getCurrentRouteCacheVersion: null,
getCurrentSegmentCacheVersion: null,
getStaleTimeMs: null,
invalidateEntirePrefetchCache: null,
invalidateRouteCacheEntries: null,
invalidateSegmentCacheEntries: null,
markRouteEntryAsDynamicRewrite: null,
overwriteRevalidatingSegmentCacheEntry: null,
pingInvalidationListeners: null,
processRuntimePrefetchStream: null,
readOrCreateRevalidatingSegmentEntry: null,
readOrCreateRouteCacheEntry: null,
readOrCreateSegmentCacheEntry: null,
readRouteCacheEntry: null,
readSegmentCacheEntry: null,
readSegmentCacheEntryForNavigation: null,
resolveStaleAt: null,
stripIsPartialByte: null,
upgradeToPendingSegment: null,
upsertSegmentEntry: null,
waitForSegmentCacheEntry: null,
writeDynamicRenderResponseIntoCache: null,
writePrerenderResponseIntoCache: null,
writeRouteIntoCache: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
EntryStatus: function() {
return _cachemap.EntryStatus;
},
MetadataOnlyRequestTree: function() {
return MetadataOnlyRequestTree;
},
attemptToFulfillDynamicSegmentFromBFCache: function() {
return attemptToFulfillDynamicSegmentFromBFCache;
},
attemptToUpgradeSegmentFromBFCache: function() {
return attemptToUpgradeSegmentFromBFCache;
},
canNewFetchStrategyProvideMoreContent: function() {
return canNewFetchStrategyProvideMoreContent;
},
convertReusedFlightRouterStateToRouteTree: function() {
return convertReusedFlightRouterStateToRouteTree;
},
convertRootFlightRouterStateToRouteTree: function() {
return convertRootFlightRouterStateToRouteTree;
},
convertRouteTreeToFlightRouterState: function() {
return convertRouteTreeToFlightRouterState;
},
createDetachedSegmentCacheEntry: function() {
return createDetachedSegmentCacheEntry;
},
createMetadataRouteTree: function() {
return createMetadataRouteTree;
},
createNonTaskyPrefetchResponseStream: function() {
return createNonTaskyPrefetchResponseStream;
},
deprecated_requestOptimisticRouteCacheEntry: function() {
return deprecated_requestOptimisticRouteCacheEntry;
},
fetchRouteOnCacheMiss: function() {
return fetchRouteOnCacheMiss;
},
fetchSegmentPrefetchesUsingDynamicRequest: function() {
return fetchSegmentPrefetchesUsingDynamicRequest;
},
fetchSegmentsOnCacheMiss: function() {
return fetchSegmentsOnCacheMiss;
},
fulfillRouteCacheEntry: function() {
return fulfillRouteCacheEntry;
},
getCurrentRouteCacheVersion: function() {
return getCurrentRouteCacheVersion;
},
getCurrentSegmentCacheVersion: function() {
return getCurrentSegmentCacheVersion;
},
getStaleTimeMs: function() {
return getStaleTimeMs;
},
invalidateEntirePrefetchCache: function() {
return invalidateEntirePrefetchCache;
},
invalidateRouteCacheEntries: function() {
return invalidateRouteCacheEntries;
},
invalidateSegmentCacheEntries: function() {
return invalidateSegmentCacheEntries;
},
markRouteEntryAsDynamicRewrite: function() {
return markRouteEntryAsDynamicRewrite;
},
overwriteRevalidatingSegmentCacheEntry: function() {
return overwriteRevalidatingSegmentCacheEntry;
},
pingInvalidationListeners: function() {
return pingInvalidationListeners;
},
processRuntimePrefetchStream: function() {
return processRuntimePrefetchStream;
},
readOrCreateRevalidatingSegmentEntry: function() {
return readOrCreateRevalidatingSegmentEntry;
},
readOrCreateRouteCacheEntry: function() {
return readOrCreateRouteCacheEntry;
},
readOrCreateSegmentCacheEntry: function() {
return readOrCreateSegmentCacheEntry;
},
readRouteCacheEntry: function() {
return readRouteCacheEntry;
},
readSegmentCacheEntry: function() {
return readSegmentCacheEntry;
},
readSegmentCacheEntryForNavigation: function() {
return readSegmentCacheEntryForNavigation;
},
resolveStaleAt: function() {
return resolveStaleAt;
},
stripIsPartialByte: function() {
return stripIsPartialByte;
},
upgradeToPendingSegment: function() {
return upgradeToPendingSegment;
},
upsertSegmentEntry: function() {
return upsertSegmentEntry;
},
waitForSegmentCacheEntry: function() {
return waitForSegmentCacheEntry;
},
writeDynamicRenderResponseIntoCache: function() {
return writeDynamicRenderResponseIntoCache;
},
writePrerenderResponseIntoCache: function() {
return writePrerenderResponseIntoCache;
},
writeRouteIntoCache: function() {
return writeRouteIntoCache;
}
});
const _approutertypes = require("../../../shared/lib/app-router-types");
const _varyparamsdecoding = require("../../../shared/lib/segment-cache/vary-params-decoding");
const _approuterheaders = require("../app-router-headers");
const _fetchserverresponse = require("../router-reducer/fetch-server-response");
const _fetch = require("./fetch");
const _scheduler = require("./scheduler");
const _varypath = require("./vary-path");
const _createhreffromurl = require("../router-reducer/create-href-from-url");
const _cachekey = require("./cache-key");
const _routeparams = require("../../route-params");
const _cachemap = require("./cache-map");
const _segmentvalueencoding = require("../../../shared/lib/segment-cache/segment-value-encoding");
const _flightdatahelpers = require("../../flight-data-helpers");
const _navigatereducer = require("../router-reducer/reducers/navigate-reducer");
const _links = require("../links");
const _segment = require("../../../shared/lib/segment");
const _types = require("./types");
const _promisewithresolvers = require("../../../shared/lib/promise-with-resolvers");
const _bfcache = require("./bfcache");
const _optimisticroutes = require("./optimistic-routes");
const _navigation = require("./navigation");
const _navigationbuildid = require("../../navigation-build-id");
const _constants = require("../../../lib/constants");
function getStaleTimeMs(staleTimeSeconds) {
return Math.max(staleTimeSeconds, 30) * 1000;
}
const isOutputExportMode = process.env.NODE_ENV === 'production' && process.env.__NEXT_CONFIG_OUTPUT === 'export';
const MetadataOnlyRequestTree = [
'',
{},
null,
'metadata-only'
];
let routeCacheMap = (0, _cachemap.createCacheMap)();
let segmentCacheMap = (0, _cachemap.createCacheMap)();
// All invalidation listeners for the whole cache are tracked in single set.
// Since we don't yet support tag or path-based invalidation, there's no point
// tracking them any more granularly than this. Once we add granular
// invalidation, that may change, though generally the model is to just notify
// the listeners and allow the caller to poll the prefetch cache with a new
// prefetch task if desired.
let invalidationListeners = null;
// Incrementing counters used to track cache invalidations. Route and segment
// caches have separate versions so they can be invalidated independently.
// Invalidation does not eagerly evict anything from the cache; entries are
// lazily evicted when read.
let currentRouteCacheVersion = 0;
let currentSegmentCacheVersion = 0;
function getCurrentRouteCacheVersion() {
return currentRouteCacheVersion;
}
function getCurrentSegmentCacheVersion() {
return currentSegmentCacheVersion;
}
function invalidateEntirePrefetchCache(nextUrl, tree) {
currentRouteCacheVersion++;
currentSegmentCacheVersion++;
(0, _links.pingVisibleLinks)(nextUrl, tree);
pingInvalidationListeners(nextUrl, tree);
}
function invalidateRouteCacheEntries(nextUrl, tree) {
currentRouteCacheVersion++;
(0, _links.pingVisibleLinks)(nextUrl, tree);
pingInvalidationListeners(nextUrl, tree);
}
function invalidateSegmentCacheEntries(nextUrl, tree) {
currentSegmentCacheVersion++;
(0, _links.pingVisibleLinks)(nextUrl, tree);
pingInvalidationListeners(nextUrl, tree);
}
function attachInvalidationListener(task) {
// This function is called whenever a prefetch task reads a cache entry. If
// the task has an onInvalidate function associated with it — i.e. the one
// optionally passed to router.prefetch(onInvalidate) — then we attach that
// listener to the every cache entry that the task reads. Then, if an entry
// is invalidated, we call the function.
if (task.onInvalidate !== null) {
if (invalidationListeners === null) {
invalidationListeners = new Set([
task
]);
} else {
invalidationListeners.add(task);
}
}
}
function notifyInvalidationListener(task) {
const onInvalidate = task.onInvalidate;
if (onInvalidate !== null) {
// Clear the callback from the task object to guarantee it's not called more
// than once.
task.onInvalidate = null;
// This is a user-space function, so we must wrap in try/catch.
try {
onInvalidate();
} catch (error) {
if (typeof reportError === 'function') {
reportError(error);
} else {
console.error(error);
}
}
}
}
function pingInvalidationListeners(nextUrl, tree) {
// The rough equivalent of pingVisibleLinks, but for onInvalidate callbacks.
// This is called when the Next-Url or the base tree changes, since those
// may affect the result of a prefetch task. It's also called after a
// cache invalidation.
if (invalidationListeners !== null) {
const tasks = invalidationListeners;
invalidationListeners = null;
for (const task of tasks){
if ((0, _scheduler.isPrefetchTaskDirty)(task, nextUrl, tree)) {
notifyInvalidationListener(task);
}
}
}
}
function readRouteCacheEntry(now, key) {
const varyPath = (0, _varypath.getRouteVaryPath)(key.pathname, key.search, key.nextUrl);
const isRevalidation = false;
const existingEntry = (0, _cachemap.getFromCacheMap)(now, getCurrentRouteCacheVersion(), routeCacheMap, varyPath, isRevalidation, false);
if (existingEntry !== null) {
return existingEntry;
}
// No cache hit. Attempt to construct from template using the new
// optimistic routing mechanism (pattern-based matching).
if (process.env.__NEXT_OPTIMISTIC_ROUTING) {
return (0, _optimisticroutes.matchKnownRoute)(now, key.pathname, key.search);
}
return null;
}
function readSegmentCacheEntry(now, varyPath) {
const isRevalidation = false;
return (0, _cachemap.getFromCacheMap)(now, getCurrentSegmentCacheVersion(), segmentCacheMap, varyPath, isRevalidation, false);
}
function readSegmentCacheEntryForNavigation(now, varyPath, restrictToShell = false) {
const isRevalidation = false;
if (process.env.__NEXT_EXPOSE_TESTING_API) {
const { getCurrentNavigationLock } = require('./navigation-testing-lock');
const lock = getCurrentNavigationLock();
if (lock !== null) {
// Instant Navigation Testing API
//
// Modify the lookup logic to simulate the behavior that we would expect
// to mostly realistically happen in a production environment with a
// warm prefetch cache.
// If restrictToShell is true, it means we're navigating to a link that
// 1) has Partial Prefetching enabled, and 2) does not have a prefetch
// prop set. We should only allow the shell to render, not anything that
// varies on concrete route params.
const lookupVaryPath = restrictToShell ? (0, _varypath.getShellSegmentVaryPath)(varyPath) : varyPath;
// To prevent the test navigation from being "polluted" by earlier
// prefetches, we'll also only match entries that were created during
// the current lock scope. This is tracked by the `ownedEntries` set.
const ownedEntries = lock.ownedEntries;
// Besides that, the rest of the logic is the same as production.
const fulfilled = (0, _cachemap.getFromCacheMap)(now, getCurrentSegmentCacheVersion(), segmentCacheMap, lookupVaryPath, isRevalidation, true);
if (fulfilled !== null && ownedEntries.has(fulfilled)) {
return fulfilled;
}
const entry = (0, _cachemap.getFromCacheMap)(now, getCurrentSegmentCacheVersion(), segmentCacheMap, lookupVaryPath, isRevalidation, false);
if (entry !== null && ownedEntries.has(entry)) {
return entry;
}
return null;
}
}
// Prefer a Fulfilled entry (e.g. a cached shell) over a more-specific
// Pending/Rejected one so it renders immediately instead of blocking on an
// in-flight entry.
const fulfilled = (0, _cachemap.getFromCacheMap)(now, getCurrentSegmentCacheVersion(), segmentCacheMap, varyPath, isRevalidation, true);
if (fulfilled !== null) {
return fulfilled;
}
return (0, _cachemap.getFromCacheMap)(now, getCurrentSegmentCacheVersion(), segmentCacheMap, varyPath, isRevalidation, false);
}
function readRevalidatingSegmentCacheEntry(now, varyPath) {
const isRevalidation = true;
return (0, _cachemap.getFromCacheMap)(now, getCurrentSegmentCacheVersion(), segmentCacheMap, varyPath, isRevalidation, false);
}
function waitForSegmentCacheEntry(pendingEntry) {
// Because the entry is pending, there's already a in-progress request.
// Attach a promise to the entry that will resolve when the server responds.
let promiseWithResolvers = pendingEntry.promise;
if (promiseWithResolvers === null) {
promiseWithResolvers = pendingEntry.promise = (0, _promisewithresolvers.createPromiseWithResolvers)();
} else {
// There's already a promise we can use
}
return promiseWithResolvers.promise;
}
function createDetachedRouteCacheEntry() {
return {
canonicalUrl: null,
status: _cachemap.EntryStatus.Empty,
blockedTasks: null,
tree: null,
metadata: null,
// This is initialized to true because we don't know yet whether the route
// could be intercepted. It's only set to false once we receive a response
// from the server.
couldBeIntercepted: true,
// Similarly, we don't yet know if the route supports PPR.
supportsPerSegmentPrefetching: false,
hasDynamicRewrite: false,
renderedSearch: null,
// Map-related fields
ref: null,
size: 0,
// Since this is an empty entry, there's no reason to ever evict it. It will
// be updated when the data is populated.
staleAt: Infinity,
version: getCurrentRouteCacheVersion()
};
}
function readOrCreateRouteCacheEntry(now, task, key) {
attachInvalidationListener(task);
const existingEntry = readRouteCacheEntry(now, key);
if (existingEntry !== null) {
return existingEntry;
}
// Create a pending entry and add it to the cache.
const pendingEntry = createDetachedRouteCacheEntry();
const varyPath = (0, _varypath.getRouteVaryPath)(key.pathname, key.search, key.nextUrl);
const isRevalidation = false;
(0, _cachemap.setInCacheMap)(routeCacheMap, varyPath, pendingEntry, isRevalidation);
return pendingEntry;
}
function deprecated_requestOptimisticRouteCacheEntry(now, requestedUrl, nextUrl) {
// This function is called during a navigation when there was no matching
// route tree in the prefetch cache. Before de-opting to a blocking,
// unprefetched navigation, we will first attempt to construct an "optimistic"
// route tree by checking the cache for similar routes.
//
// Check if there's a route with the same pathname, but with different
// search params. We can then base our optimistic route tree on this entry.
//
// Conceptually, we are simulating what would happen if we did perform a
// prefetch the requested URL, under the assumption that the server will
// not redirect or rewrite the request in a different manner than the
// base route tree. This assumption might not hold, in which case we'll have
// to recover when we perform the dynamic navigation request. However, this
// is what would happen if a route were dynamically rewritten/redirected
// in between the prefetch and the navigation. So the logic needs to exist
// to handle this case regardless.
// Look for a route with the same pathname, but with an empty search string.
// TODO: There's nothing inherently special about the empty search string;
// it's chosen somewhat arbitrarily, with the rationale that it's the most
// likely one to exist. But we should update this to match _any_ search
// string. The plan is to generalize this logic alongside other improvements
// related to "fallback" cache entries.
const requestedSearch = requestedUrl.search;
if (requestedSearch === '') {
// The caller would have already checked if a route with an empty search
// string is in the cache. So we can bail out here.
return null;
}
const urlWithoutSearchParams = new URL(requestedUrl);
urlWithoutSearchParams.search = '';
const routeWithNoSearchParams = readRouteCacheEntry(now, (0, _cachekey.createCacheKey)(urlWithoutSearchParams.href, nextUrl));
if (routeWithNoSearchParams === null || routeWithNoSearchParams.status !== _cachemap.EntryStatus.Fulfilled) {
// Bail out of constructing an optimistic route tree. This will result in
// a blocking, unprefetched navigation.
return null;
}
// Now we have a base route tree we can "patch" with our optimistic values.
// Optimistically assume that redirects for the requested pathname do
// not vary on the search string. Therefore, if the base route was
// redirected to a different search string, then the optimistic route
// should be redirected to the same search string. Otherwise, we use
// the requested search string.
const canonicalUrlForRouteWithNoSearchParams = new URL(routeWithNoSearchParams.canonicalUrl, requestedUrl.origin);
const optimisticCanonicalSearch = canonicalUrlForRouteWithNoSearchParams.search !== '' ? canonicalUrlForRouteWithNoSearchParams.search : requestedSearch;
// Similarly, optimistically assume that rewrites for the requested
// pathname do not vary on the search string. Therefore, if the base
// route was rewritten to a different search string, then the optimistic
// route should be rewritten to the same search string. Otherwise, we use
// the requested search string.
const optimisticRenderedSearch = routeWithNoSearchParams.renderedSearch !== '' ? routeWithNoSearchParams.renderedSearch : requestedSearch;
const optimisticUrl = new URL(routeWithNoSearchParams.canonicalUrl, location.origin);
optimisticUrl.search = optimisticCanonicalSearch;
const optimisticCanonicalUrl = (0, _createhreffromurl.createHrefFromUrl)(optimisticUrl);
const optimisticRouteTree = deprecated_createOptimisticRouteTree(routeWithNoSearchParams.tree, optimisticRenderedSearch);
const optimisticMetadataTree = deprecated_createOptimisticRouteTree(routeWithNoSearchParams.metadata, optimisticRenderedSearch);
// Clone the base route tree, and override the relevant fields with our
// optimistic values.
const optimisticEntry = {
canonicalUrl: optimisticCanonicalUrl,
status: _cachemap.EntryStatus.Fulfilled,
// This isn't cloned because it's instance-specific
blockedTasks: null,
tree: optimisticRouteTree,
metadata: optimisticMetadataTree,
couldBeIntercepted: routeWithNoSearchParams.couldBeIntercepted,
supportsPerSegmentPrefetching: routeWithNoSearchParams.supportsPerSegmentPrefetching,
hasDynamicRewrite: routeWithNoSearchParams.hasDynamicRewrite,
// Override the rendered search with the optimistic value.
renderedSearch: optimisticRenderedSearch,
// Map-related fields
ref: null,
size: 0,
staleAt: routeWithNoSearchParams.staleAt,
version: routeWithNoSearchParams.version
};
// Do not insert this entry into the cache. It only exists so we can
// perform the current navigation. Just return it to the caller.
return optimisticEntry;
}
function deprecated_createOptimisticRouteTree(tree, newRenderedSearch) {
// Create a new route tree that identical to the original one except for
// the rendered search string, which is contained in the vary path.
let clonedSlots = null;
const originalSlots = tree.slots;
if (originalSlots !== null) {
clonedSlots = new Map();
for (const [parallelRouteKey, childTree] of originalSlots){
clonedSlots.set(parallelRouteKey, deprecated_createOptimisticRouteTree(childTree, newRenderedSearch));
}
}
// We only need to clone the vary path if the route is a page.
if (tree.isPage) {
// The shell vary path Fallbacks search params, so it's unaffected by the
// new rendered search and can be reused as-is.
return {
requestKey: tree.requestKey,
segment: tree.segment,
shellVaryPath: tree.shellVaryPath,
refreshState: tree.refreshState,
varyPath: (0, _varypath.clonePageVaryPathWithNewSearchParams)(tree.varyPath, newRenderedSearch),
isPage: true,
slots: clonedSlots,
prefetchHints: tree.prefetchHints
};
}
return {
requestKey: tree.requestKey,
segment: tree.segment,
shellVaryPath: tree.shellVaryPath,
refreshState: tree.refreshState,
varyPath: tree.varyPath,
isPage: false,
slots: clonedSlots,
prefetchHints: tree.prefetchHints
};
}
function readOrCreateSegmentCacheEntry(now, fetchStrategy, tree, // Non-null when this read is part of a locked navigation's prefetch (Instant
// Navigation Testing API only; always null in production). See below.
navigationLockPrefetch) {
const existingEntry = readSegmentCacheEntry(now, tree.varyPath);
if (existingEntry !== null) {
if (process.env.__NEXT_EXPOSE_TESTING_API && navigationLockPrefetch !== null) {
// Locked navigation: ignore entries that predate the lock so each
// navigation reads only data (re)fetched within the lock scope — a
// "clean read." But an entry we already created within this scope is
// reused like normal; otherwise the prefetch would discard the entry it
// just fetched on every scheduler pass and refetch forever. See
// navigation-testing-lock.ts.
const { getCurrentNavigationLock, trackNavigationLockPrefetchEntry } = require('./navigation-testing-lock');
const lock = getCurrentNavigationLock();
if (lock !== null && lock.ownedEntries.has(existingEntry)) {
// Track-on-reuse: when this navigation reuses an in-flight (Pending)
// entry it didn't spawn — e.g. a runtime-prefetch (PPRRuntime) upgrade
// started by an earlier prefetch in the scope — register it on this
// navigation's prefetch so the navigation awaits it before reading.
// Without this, the navigation can read while that upgrade is still
// pending and fall back to a less-specific fulfilled entry (the shell),
// never surfacing the resolved value.
//
// This is content-neutral: the entry is found by the concrete vary-path
// (not by strategy), so it's whatever the navigation would read at this
// key anyway. Tracking only controls whether we await it now versus
// suspend on it during the render, so it can't surface an entry the
// navigation wouldn't otherwise read. Tracking is deduped, so it's a
// no-op if we already spawned/tracked this entry.
if (existingEntry.status === _cachemap.EntryStatus.Pending) {
trackNavigationLockPrefetchEntry(navigationLockPrefetch, existingEntry);
}
return existingEntry;
}
} else {
return existingEntry;
}
}
// No reusable entry, or a locked navigation discarding a pre-lock entry.
// Create a pending entry and add it to the cache. The stale time is set to a
// default value; the actual stale time will be set when the entry is
// fulfilled with data from the server response.
const varyPathForRequest = (0, _varypath.getSegmentVaryPathForRequest)(fetchStrategy, tree);
const pendingEntry = createDetachedSegmentCacheEntry(now);
const isRevalidation = false;
(0, _cachemap.setInCacheMap)(segmentCacheMap, varyPathForRequest, pendingEntry, isRevalidation);
return pendingEntry;
}
function readOrCreateRevalidatingSegmentEntry(now, fetchStrategy, tree) {
// This function is called when we've already confirmed that a particular
// segment is cached, but we want to perform another request anyway in case it
// returns more complete and/or fresher data than we already have. The logic
// for deciding whether to replace the existing entry is handled elsewhere;
// this function just handles retrieving a cache entry that we can use to
// track the revalidation.
//
// The reason revalidations are stored in the cache is because we need to be
// able to dedupe multiple revalidation requests. The reason they have to be
// handled specially is because we shouldn't overwrite a "normal" entry if
// one exists at the same keypath. So, for each internal cache location, there
// is a special "revalidation" slot that is used solely for this purpose.
//
// You can think of it as if all the revalidation entries were stored in a
// separate cache map from the canonical entries, and then transfered to the
// canonical cache map once the request is complete — this isn't how it's
// actually implemented, since it's more efficient to store them in the same
// data structure as the normal entries, but that's how it's modeled
// conceptually.
// TODO: Once we implement Fallback behavior for params, where an entry is
// re-keyed based on response information, we'll need to account for the
// possibility that the keypath of the previous entry is more generic than
// the keypath of the revalidating entry. In other words, the server could
// return a less generic entry upon revalidation. For now, though, this isn't
// a concern because the keypath is based solely on the prefetch strategy,
// not on data contained in the response.
const existingEntry = readRevalidatingSegmentCacheEntry(now, tree.varyPath);
if (existingEntry !== null) {
return existingEntry;
}
// Create a pending entry and add it to the cache. The stale time is set to a
// default value; the actual stale time will be set when the entry is
// fulfilled with data from the server response.
const varyPathForRequest = (0, _varypath.getSegmentVaryPathForRequest)(fetchStrategy, tree);
const pendingEntry = createDetachedSegmentCacheEntry(now);
const isRevalidation = true;
(0, _cachemap.setInCacheMap)(segmentCacheMap, varyPathForRequest, pendingEntry, isRevalidation);
return pendingEntry;
}
function overwriteRevalidatingSegmentCacheEntry(now, fetchStrategy, tree) {
// This function is called when we've already decided to replace an existing
// revalidation entry. Create a new entry and write it into the cache,
// overwriting the previous value. The stale time is set to a default value;
// the actual stale time will be set when the entry is fulfilled with data
// from the server response.
const varyPathForRequest = (0, _varypath.getSegmentVaryPathForRequest)(fetchStrategy, tree);
const pendingEntry = createDetachedSegmentCacheEntry(now);
const isRevalidation = true;
(0, _cachemap.setInCacheMap)(segmentCacheMap, varyPathForRequest, pendingEntry, isRevalidation);
return pendingEntry;
}
/**
* Whether an existing cache entry is preferred over an incoming candidate —
* i.e. the candidate does NOT supersede it. (On an exact tie — same fetch
* strategy, same partialness — this returns false, so the candidate replaces
* the existing entry.) This is the precedence rule used both when deciding
* whether an upsert may replace the entry at its own keypath, and when
* deciding whether an entry at a more specific keypath may be evicted because
* it shadows a just-inserted candidate (see `evictShadowingSegmentEntries`).
*
* Note that "less/more specific" in the comments below refers to fetch
* strategy content tiers (how much content a strategy can produce), not the
* vary-path specificity the eviction docs are concerned with.
*/ function isExistingSegmentEntryPreferred(existingEntry, candidateEntry) {
return(// We fetched the new segment using a different, less specific fetch
// strategy than the segment we already have in the cache, so it can't
// have more content.
candidateEntry.fetchStrategy !== existingEntry.fetchStrategy && !canNewFetchStrategyProvideMoreContent(existingEntry.fetchStrategy, candidateEntry.fetchStrategy) || // The existing entry isn't partial, but the new one is.
// (TODO: can this be true if `candidateEntry.fetchStrategy >= existingEntry.fetchStrategy`?)
!existingEntry.isPartial && candidateEntry.isPartial);
}
function upsertSegmentEntry(now, varyPath, candidateEntry, // The fully concrete vary path a read for this segment position resolves
// against (all concrete param values, i.e. `tree.varyPath`) — the most
// specific path a read would use. Note this is the opposite of the
// generalized keying path that `getSegmentVaryPathForRequest` computes.
// Used to detect and evict stale entries at more specific keypaths that
// would otherwise shadow the candidate. Pass null when there's no request
// context; the shadow check is skipped.
lookupVaryPath) {
// We have a new entry that has not yet been inserted into the cache. Before
// we do so, we need to confirm whether it takes precedence over the existing
// entry (if one exists).
// TODO: We should not upsert an entry if its key was invalidated in the time
// since the request was made. We can do that by passing the "owner" entry to
// this function and confirming it's the same as `existingEntry`.
if ((0, _cachemap.isValueExpired)(now, getCurrentSegmentCacheVersion(), candidateEntry)) {
// The entry is expired. We cannot upsert it.
return null;
}
const existingEntry = readSegmentCacheEntry(now, varyPath);
if (existingEntry !== null) {
// Don't replace a more specific segment with a less-specific one. A case where this
// might happen is if the existing segment was fetched via
// `<Link prefetch={true}>`.
if (isExistingSegmentEntryPreferred(existingEntry, candidateEntry)) {
// The candidate does not supersede the existing entry. Leave the
// existing entry in place and discard the candidate by not inserting it.
//
// We must not mutate the candidate here (e.g. downgrade it to Rejected or
// null out its `rsc`). The caller does not transfer exclusive ownership
// of it: it may already have been fulfilled, resolving its promise to a
// waiter that holds the entry and reads `rsc` off it later. A navigation
// seed is such a waiter, via `waitForSegmentCacheEntry`. Nulling `rsc`
// after the fact resolves that read to `null`, so the waiter loses the
// data it was about to render. Declining to insert it is enough: the
// existing entry stays canonical, and the candidate keeps its valid (if
// less complete) data for any waiter that already took it.
return null;
}
// Ping any tasks blocked on the existing entry before replacing it so they
// re-run and pick up the new entry. Without this, tasks waiting on the
// existing Empty/Pending entry would be stranded — the new fulfilled
// candidate has no blockedTasks of its own.
if (existingEntry.status === _cachemap.EntryStatus.Empty || existingEntry.status === _cachemap.EntryStatus.Pending) {
pingBlockedTasks(existingEntry);
}
// Replace the existing entry by writing the candidate over its keypath
// below (the same mechanism `overwriteRevalidatingSegmentCacheEntry`
// uses). We intentionally do NOT call `deleteFromCacheMap` first: deleting
// vacates the canonical slot, and `deleteMapEntry` promotes a pending
// Revalidation-slot entry into the vacated slot — which the immediate
// insert below would then silently overwrite. The in-flight revalidation
// would vanish from the map, so the next scheduler pass would find an
// empty revalidation slot and spawn a duplicate request instead of
// deduping against it. Replacing in place never vacates the slot, so
// promotion never runs and the pending revalidating entry stays in its
// Revalidation slot where `readOrCreateRevalidatingSegmentEntry`'s dedupe
// finds it.
//
// The displaced entry's map/LRU accounting is handled by the replacement
// itself: `setMapEntryValue` drops the displaced value's `ref` and
// `updateLruSize` swaps its size for the candidate's, which is exactly
// what delete-then-insert did.
}
const isRevalidation = false;
(0, _cachemap.setInCacheMap)(segmentCacheMap, varyPath, candidateEntry, isRevalidation);
if (lookupVaryPath !== null) {
evictShadowingSegmentEntries(now, lookupVaryPath, candidateEntry);
}
return candidateEntry;
}
/**
* Evicts stale entries at more specific keypaths that shadow a just-inserted
* candidate entry.
*
* A response can be written to the cache at a MORE GENERIC vary path than the
* path the request was issued against — for example, the server may report
* that a segment doesn't vary on a param, so the entry is re-keyed with that
* param as Fallback. Meanwhile, an older, less useful entry can exist at a
* more specific path within the same fallback chain — for example, a partial
* shell entry keyed with root params concrete (see
* `getShellSegmentVaryPath`). Because segment lookup is
* most-specific-match-wins, every subsequent read at the concrete request
* path keeps returning the stale specific entry, and the more complete
* generic entry is unreachable from that URL. That both wastes the completed
* request and can loop: a prefetch task that revalidated the segment reads
* back the same stale entry, decides it needs to revalidate again, and
* repeats forever.
*
* The upsert is the one moment we know the ordering between the two entries:
* the candidate was produced by a request for this segment position, and
* `lookupVaryPath` is the fully concrete path a read for that position
* resolves against, so any entry that a read at that path would return in the
* candidate's stead is directly comparable to it. If such an entry is settled
* and the candidate supersedes it — under the same precedence rules the
* upsert applies at its own keypath — we know we never want to match against
* it again, so delete it, making the candidate reachable.
*
* Non-settled entries are never evicted here: a Pending entry is owned by an
* in-flight request that will settle it, and an Empty entry is a placeholder
* that a scheduler pass may still claim and upgrade.
*/ function evictShadowingSegmentEntries(now, lookupVaryPath, candidateEntry) {
// There can in principle be multiple shadowing entries at successively less
// specific keypaths, so loop until the read returns the candidate (or an
// entry we don't supersede). Each iteration re-reads and re-checks from
// scratch (in part because `deleteFromCacheMap` can promote a settled
// Revalidation-slot value into the just-vacated slot, surfacing a new entry
// at the same keypath). Each iteration deletes an entry from the map, so
// the loop terminates naturally; the bound is defensive, and 32 is far
// beyond any real fallback chain, which is bounded by the vary
// path's length.
for(let i = 0; i < 32; i++){
const shadowEntry = readSegmentCacheEntry(now, lookupVaryPath);
if (shadowEntry === null || shadowEntry === candidateEntry) {
// The candidate is reachable from the lookup path (or the read missed
// entirely, e.g. because the candidate expired). Done.
return;
}
if (shadowEntry.status !== _cachemap.EntryStatus.Fulfilled && shadowEntry.status !== _cachemap.EntryStatus.Rejected) {
// Only settled entries may be evicted. A Pending entry is held by an
// in-flight request and will settle on its own.
return;
}
if (isExistingSegmentEntryPreferred(shadowEntry, candidateEntry)) {
// The shadowing entry is preferred over the candidate (e.g. it's a
// complete entry fetched with a more specific strategy). Leave it —
// reads at this path should keep matching it.
return;
}
// The candidate supersedes the shadowing entry. Evict it. Settled entries
// shouldn't have blocked tasks (Fulfilled always has `blockedTasks:
// null`, and Rejected entries were pinged at rejection), but ping
// defensively before deleting, matching the upsert-evict pattern above.
pingBlockedTasks(shadowEntry);
(0, _cachemap.deleteFromCacheMap)(shadowEntry);
}
}
function createDetachedSegmentCacheEntry(now) {
// Default stale time for pending segment cache entries. The actual stale time
// is set when the entry is fulfilled with data from the server response.
const staleAt = now + 30 * 1000;
const emptyEntry = {
status: _cachemap.EntryStatus.Empty,
blockedTasks: null,
// Default to assuming the fetch strategy will be PPR. This will be updated
// when a fetch is actually initiated.
fetchStrategy: _types.FetchStrategy.PPR,
rsc: null,
isPartial: true,
isUpgradeableISRFallback: false,
promise: null,
// Map-related fields
ref: null,
size: 0,
staleAt,
version: 0
};
if (process.env.__NEXT_EXPOSE_TESTING_API) {
// Instant Navigation Testing API: mark entries created during a lock scope
// as owned, so locked navigations match only data (re)fetched within the
// scope. No-op when no lock is held (always in production).
const { recordNavigationLockOwnedEntry } = require('./navigation-testing-lock');
recordNavigationLockOwnedEntry(emptyEntry);
}
return emptyEntry;
}
function upgradeToPendingSegment(emptyEntry, fetchStrategy, navigationLockPrefetch) {
const pendingEntry = emptyEntry;
pendingEntry.status = _cachemap.EntryStatus.Pending;
pendingEntry.fetchStrategy = fetchStrategy;
if (fetchStrategy === _types.FetchStrategy.Full) {
// We can assume the response will contain the full segment data. Set this
// to false so we know it's OK to omit this segment from any navigation
// requests that may happen while the data is still pending.
pendingEntry.isPartial = false;
}
// Set the version here, since this is right before the request is initiated.
// The next time the segment cache version is incremented, the entry will
// effectively be evicted. This happens before initiating the request, rather
// than when receiving the response, because it's guaranteed to happen
// before the data is read on the server.
pendingEntry.version = getCurrentSegmentCacheVersion();
if (process.env.__NEXT_EXPOSE_TESTING_API && // Instant Navigation Testing API only. Non-null when the requesting
// prefetch is driving a locked navigation, in which case the
// freshly-spawned pending entry is tracked against that navigation's
// prefetch state so the navigation waits for it to fulfill before reading
// it. Null at non-scheduler call sites (BFCache fulfillment, response
// processing), which don't spawn an in-flight request to wait on, and
// always in production.
navigationLockPrefetch !== null) {
const { trackNavigationLockPrefetchEntry } = require('./navigation-testing-lock');
trackNavigationLockPrefetchEntry(navigationLockPrefetch, pendingEntry);
}
return pendingEntry;
}
function attemptToFulfillDynamicSegmentFromBFCache(now, segment, tree) {
// Attempts to fulfill an empty segment cache entry using data from the
// bfcache. This is only valid during a Full prefetch (i.e. one that includes
// dynamic data), because the bfcache stores data from navigations which
// always include dynamic data.
// We always use the canonical vary path when checking the bfcache. This is
// the same operation we'd use to access the cache during a
// regular navigation.
const varyPath = tree.varyPath;
// Read from the BFCache without expiring it (pass -1). We check freshness
// ourselves using navigatedAt, because the BFCache's staleAt may have been
// overridden by a per-page unstable_dynamicStaleTime and can't be used to
// derive the original request time.
const bfcacheEntry = (0, _bfcache.readFromBFCache)(varyPath);
if (bfcacheEntry !== null) {
// The stale time for dynamic prefetches (default: 5 mins) is different
// from the stale time for regular navigations (default: 0 secs). Use
// navigatedAt to compute the correct expiry for prefetch purposes.
const dynamicPrefetchStaleAt = bfcacheEntry.navigatedAt + _navigatereducer.STATIC_STALETIME_MS;
if (now > dynamicPrefetchStaleAt) {
return null;
}
const pendingSegment = upgradeToPendingSegment(segment, _types.FetchStrategy.Full, // Fulfilled synchronously from the BFCache; nothing for a locked
// navigation to wait on.
null);
const isPartial = false;
return fulfillSegmentCacheEntry(pendingSegment, bfcacheEntry.rsc, dynamicPrefetchStaleAt, isPartial, // bfcache data is concrete, never an ISR fallback.
false, _types.FetchStrategy.Full);
}
return null;
}
function attemptToUpgradeSegmentFromBFCache(now, tree) {
const varyPath = tree.varyPath;
const bfcacheEntry = (0, _bfcache.readFromBFCache)(varyPath);
if (bfcacheEntry !== null) {
const dynamicPrefetchStaleAt = bfcacheEntry.navigatedAt + _navigatereducer.STATIC_STALETIME_MS;
if (now > dynamicPrefetchStaleAt) {
return null;
}
const pendingSegment = upgradeToPendingSegment(createDetachedSegmentCacheEntry(now), _types.FetchStrategy.Full, // Fulfilled synchronously from the BFCache; nothing for a locked
// navigation to wait on.
null);
const isPartial = false;
const newEntry = fulfillSegmentCacheEntry(pendingSegment, bfcacheEntry.rsc, dynamicPrefetchStaleAt, isPartial, // bfcache data is concrete, never an ISR fallback.
false, _types.FetchStrategy.Full);
const segmentVaryPath = (0, _varypath.getSegmentVaryPathForRequest)(_types.FetchStrategy.Full, tree);
const upserted = upsertSegmentEntry(now, segmentVaryPath, newEntry, // The concrete lookup path this BFCache upgrade applies to. (In
// practice a Full request path is already fully concrete, so nothing
// can shadow the new entry and the shadow check is a no-op.)
tree.varyPath);
if (upserted !== null && upserted.status === _cachemap.EntryStatus.Fulfilled) {
return upserted;
}
}
return null;
}
function pingBlockedTasks(entry) {
const blockedTasks = entry.blockedTasks;
if (blockedTasks !== null) {
for (const task of blockedTasks){
(0, _scheduler.pingPrefetchTask)(task);
}
entry.blockedTasks = null;
}
}
function createMetadataRouteTree(metadataVaryPath) {
// The Head is not actually part of the route tree, but other than that, it's
// fetched and cached like a segment. Some functions expect a RouteTree
// object, so rather than fork the logic in all those places, we use this
// "fake" one.
const metadata = {
requestKey: _segmentvalueencoding.HEAD_REQUEST_KEY,
segment: _segmentvalueencoding.HEAD_REQUEST_KEY,
shellVaryPath: (0, _varypath.getShellSegmentVaryPath)(metadataVaryPath),
refreshState: null,
varyPath: metadataVaryPath,
// The metadata isn't really a "page" (though it isn't really a "segment"
// either) but for the purposes of how this field is used, it behaves like
// one. If this logic ever gets more complex we can change this to an enum.
isPage: true,
slots: null,
prefetchHints: 0
};
return metadata;
}
function fulfillRouteCacheEntry(now, entry, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching) {
// Get the rendered search from the vary path
const renderedSearch = (0, _varypath.getRenderedSearchFromVaryPath)(metadataVaryPath) ?? '';
const fulfilledEntry = entry;
fulfilledEntry.status = _cachemap.EntryStatus.Fulfilled;
fulfilledEntry.tree = tree;
fulfilledEntry.metadata = createMetadataRouteTree(metadataVaryPath);
// Route structure is essentially static — it only changes on deploy.
// Always use the static stale time.
// NOTE: An exception is rewrites/redirects in middleware or proxy, which can
// change routes dynamically. We have other strategies for handling those.
//
// If the route tree has stale inlining hints (e.g. the initial RSC payload
// for a build-time static page, generated before collectPrefetchHints ran),
// immediately expire the entry so it gets re-fetched with correct hints.
// The segment data itself is still valid — only the route tree (which
// contains the hint bits) needs to be re-fetched.
if (tree.prefetchHints & _approutertypes.PrefetchHint.InliningHintsStale) {
fulfilledEntry.staleAt = -1;
} else {
fulfilledEntry.staleAt = now + _navigatereducer.STATIC_STALETIME_MS;
}
fulfilledEntry.couldBeIntercepted = couldBeIntercepted;
fulfilledEntry.canonicalUrl = canonicalUrl;
fulfilledEntry.renderedSearch = renderedSearch;
fulfilledEntry.supportsPerSegmentPrefetching = supportsPerSegmentPrefetching;
fulfilledEntry.hasDynamicRewrite = false;
pingBlockedTasks(entry);
return fulfilledEntry;
}
function writeRouteIntoCache(now, pathname, search, nextUrl, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching) {
const pendingEntry = createDetachedRouteCacheEntry();
const fulfilledEntry = fulfillRouteCacheEntry(now, pendingEntry, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching);
const varyPath = (0, _varypath.getFulfilledRouteVaryPath)(pathname, search, nextUrl, couldBeIntercepted);
const isRevalidation = false;
(0, _cachemap.setInCacheMap)(routeCacheMap, varyPath, fulfilledEntry, isRevalidation);
return fulfilledEntry;
}
function markRouteEntryAsDynamicRewrite(entry) {
entry.hasDynamicRewrite = true;
// Note: The caller is responsible for also calling invalidateRouteCacheEntries
// to invalidate other entries that may have been derived from this template
// before we knew it had a dynamic rewrite.
}
function fulfillSegmentCacheEntry(segmentCacheEntry, rsc, staleAt, isPartial, // Only static (per-segment PPR) responses can be ISR fallbacks; all other
// callers pass false. Always assigned (even when false) so that re-fulfilling
// a previously-fallback entry with a concrete response clears the flag and
// ends the retry loop.
isUpgradeableISRFallback, // The strategy tier describing the CONTENT this entry is fulfilled with —
// which comes from the response, not the tier the entry was requested at.
// Usually the two agree, but when a response's shell payload IS the full
// response (no shell/full split), shell-spawned entries are fulfilled with
// full-tier content and recorded as such (see the promotion in
// writeSegmentBundle