"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { FreshnessPolicy: null, createInitialCacheNodeForHydration: null, isDeferredRsc: null, spawnDynamicRequests: null, startPPRNavigation: null }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { FreshnessPolicy: function() { return FreshnessPolicy; }, createInitialCacheNodeForHydration: function() { return createInitialCacheNodeForHydration; }, isDeferredRsc: function() { return isDeferredRsc; }, spawnDynamicRequests: function() { return spawnDynamicRequests; }, startPPRNavigation: function() { return startPPRNavigation; } }); const _approutertypes = require("../../../shared/lib/app-router-types"); const _segment = require("../../../shared/lib/segment"); const _matchsegments = require("../match-segments"); const _createhreffromurl = require("./create-href-from-url"); const _fetchserverresponse = require("./fetch-server-response"); const _useactionqueue = require("../use-action-queue"); const _routerreducertypes = require("./router-reducer-types"); const _isnavigatingtonewrootlayout = require("./is-navigating-to-new-root-layout"); const _committedstate = require("./reducers/committed-state"); const _navigation = require("../segment-cache/navigation"); const _cache = require("../segment-cache/cache"); const _types = require("../segment-cache/types"); const _optimisticroutes = require("../segment-cache/optimistic-routes"); const _constants = require("../../../lib/constants"); const _varypath = require("../segment-cache/vary-path"); const _bfcache = require("../segment-cache/bfcache"); var FreshnessPolicy = /*#__PURE__*/ function(FreshnessPolicy) { FreshnessPolicy[FreshnessPolicy["Default"] = 0] = "Default"; FreshnessPolicy[FreshnessPolicy["Hydration"] = 1] = "Hydration"; FreshnessPolicy[FreshnessPolicy["HistoryTraversal"] = 2] = "HistoryTraversal"; FreshnessPolicy[FreshnessPolicy["RefreshAll"] = 3] = "RefreshAll"; FreshnessPolicy[FreshnessPolicy["HMRRefresh"] = 4] = "HMRRefresh"; FreshnessPolicy[FreshnessPolicy["Gesture"] = 5] = "Gesture"; return FreshnessPolicy; }({}); const noop = ()=>{}; function createInitialCacheNodeForHydration(navigatedAt, initialTree, seedData, seedHead, seedDynamicStaleAt) { // Create the initial cache node tree, using the data embedded into the // HTML document. const accumulation = { separateRefreshUrls: null, scrollRef: null }; const task = createCacheNodeOnNavigation(navigatedAt, initialTree, null, 1, seedData, seedHead, seedDynamicStaleAt, false, accumulation); return task; } function startPPRNavigation(navigatedAt, oldUrl, oldRenderedSearch, oldCacheNode, oldRouterState, newRouteTree, newMetadataVaryPath, freshness, seedData, seedHead, seedDynamicStaleAt, isSamePageNavigation, accumulation) { const didFindRootLayout = false; const parentNeedsDynamicRequest = false; const parentRefreshState = null; const oldRootRefreshState = { canonicalUrl: (0, _createhreffromurl.createHrefFromUrl)(oldUrl), renderedSearch: oldRenderedSearch }; return updateCacheNodeOnNavigation(navigatedAt, oldUrl, oldCacheNode !== null ? oldCacheNode : undefined, oldRouterState, newRouteTree, newMetadataVaryPath, freshness, didFindRootLayout, seedData, seedHead, seedDynamicStaleAt, isSamePageNavigation, parentNeedsDynamicRequest, oldRootRefreshState, parentRefreshState, accumulation); } function updateCacheNodeOnNavigation(navigatedAt, oldUrl, oldCacheNode, oldRouterState, newRouteTree, newMetadataVaryPath, freshness, didFindRootLayout, seedData, seedHead, seedDynamicStaleAt, isSamePageNavigation, parentNeedsDynamicRequest, oldRootRefreshState, parentRefreshState, accumulation) { // Check if this segment matches the one in the previous route. const oldSegment = oldRouterState[0]; const newSegment = createSegmentFromRouteTree(newRouteTree); if (!(0, _matchsegments.matchSegment)(newSegment, oldSegment)) { // This segment does not match the previous route. We're now entering the // new part of the target route. Switch to the "create" path. if (// Check if the route tree changed before we reached a layout. (The // highest-level layout in a route tree is referred to as the "root" // layout.) This could mean that we're navigating between two different // root layouts. When this happens, we perform a full-page (MPA-style) // navigation. // // However, the algorithm for deciding where to start rendering a route // (i.e. the one performed in order to reach this function) is stricter // than the one used to detect a change in the root layout. So just // because we're re-rendering a segment outside of the root layout does // not mean we should trigger a full-page navigation. // // Specifically, we handle dynamic parameters differently: two segments // are considered the same even if their parameter values are different. // // Refer to isNavigatingToNewRootLayout for details. // // Note that we only have to perform this extra traversal if we didn't // already discover a root layout in the part of the tree that is // unchanged. We also only need to compare the subtree that is not // shared. In the common case, this branch is skipped completely. !didFindRootLayout && (0, _isnavigatingtonewrootlayout.isNavigatingToNewRootLayout)(oldRouterState, newRouteTree) || // The global Not Found route (app/global-not-found.tsx) is a special // case, because it acts like a root layout, but in the router tree, it // is rendered in the same position as app/layout.tsx. // // Any navigation to the global Not Found route should trigger a // full-page navigation. // // TODO: We should probably model this by changing the key of the root // segment when this happens. Then the root layout check would work // as expected, without a special case. newSegment === _segment.NOT_FOUND_SEGMENT_KEY) { return null; } return createCacheNodeOnNavigation(navigatedAt, newRouteTree, newMetadataVaryPath, freshness, seedData, seedHead, seedDynamicStaleAt, parentNeedsDynamicRequest, accumulation); } const newSlots = newRouteTree.slots; const oldRouterStateChildren = oldRouterState[1]; const seedDataChildren = seedData !== null ? seedData[1] : null; // We're currently traversing the part of the tree that was also part of // the previous route. If we discover a root layout, then we don't need to // trigger an MPA navigation. const childDidFindRootLayout = didFindRootLayout || (newRouteTree.prefetchHints & _approutertypes.PrefetchHint.IsRootLayout) !== 0; let shouldRefreshDynamicData = false; switch(freshness){ case 0: case 2: case 1: case 5: shouldRefreshDynamicData = false; break; case 3: case 4: shouldRefreshDynamicData = true; break; default: freshness; break; } // TODO: We're not consistent about how we do this check. Some places // check if the segment starts with PAGE_SEGMENT_KEY, but most seem to // check if there any any children, which is why I'm doing it here. We // should probably encode an empty children set as `null` though. Either // way, we should update all the checks to be consistent. const isLeafSegment = newSlots === null; // Get the data for this segment. Since it was part of the previous route, // usually we just clone the data from the old CacheNode. However, during a // refresh or a revalidation, there won't be any existing CacheNode. So we // may need to consult the prefetch cache, like we would for a new segment. let newCacheNode; let needsDynamicRequest; if (oldCacheNode !== undefined && !shouldRefreshDynamicData && // During a same-page navigation, we always refetch the page segments !(isLeafSegment && isSamePageNavigation)) { // Reuse the existing CacheNode const dropPrefetchRsc = false; newCacheNode = reuseSharedCacheNode(dropPrefetchRsc, oldCacheNode); needsDynamicRequest = false; } else { // If this is part of a refresh, ignore the existing CacheNode and create a // new one. const seedRsc = seedData !== null ? seedData[0] : null; const result = createCacheNodeForSegment(navigatedAt, newRouteTree, seedRsc, newMetadataVaryPath, seedHead, freshness, seedDynamicStaleAt); newCacheNode = result.cacheNode; needsDynamicRequest = result.needsDynamicRequest; // Carry forward the old node's scrollRef. This preserves scroll // intent when a prior navigation's cache node is replaced by a // refresh before the scroll handler has had a chance to fire — // e.g. when router.push() and router.refresh() are called in the // same startTransition batch. if (oldCacheNode !== undefined) { newCacheNode.scrollRef = oldCacheNode.scrollRef; } } // During a refresh navigation, there's a special case that happens when // entering a "default" slot. The default slot may not be part of the // current route; it may have been reused from an older route. If so, // we need to fetch its data from the old route's URL rather than current // route's URL. Keep track of this as we traverse the tree. const maybeRefreshState = newRouteTree.refreshState; const refreshState = maybeRefreshState !== undefined && maybeRefreshState !== null ? // refresh URL as we continue traversing the tree. maybeRefreshState : parentRefreshState; // If this segment itself needs to fetch new data from the server, then by // definition it is being refreshed. Track its refresh URL so we know which // URL to request the data from. if (needsDynamicRequest && refreshState !== null) { accumulateRefreshUrl(accumulation, refreshState); } // As we diff the trees, we may sometimes modify (copy-on-write, not mutate) // the Route Tree that was returned by the server — for example, in the case // of default parallel routes, we preserve the currently active segment. To // avoid mutating the original tree, we clone the router state children along // the return path. let patchedRouterStateChildren = {}; let taskChildren = null; // Most navigations require a request to fetch additional data from the // server, either because the data was not already prefetched, or because the // target route contains dynamic data that cannot be prefetched. // // However, if the target route is fully static, and it's already completely // loaded into the segment cache, then we can skip the server request. // // This starts off as `false`, and is set to `true` if any of the child // routes requires a dynamic request. let childNeedsDynamicRequest = false; // As we traverse the children, we'll construct a FlightRouterState that can // be sent to the server to request the dynamic data. If it turns out that // nothing in the subtree is dynamic (i.e. childNeedsDynamicRequest is false // at the end), then this will be discarded. // TODO: We can probably optimize the format of this data structure to only // include paths that are dynamic. Instead of reusing the // FlightRouterState type. let dynamicRequestTreeChildren = {}; let newCacheNodeSlots = null; if (newSlots !== null) { const oldCacheNodeSlots = oldCacheNode !== undefined ? oldCacheNode.slots : null; newCacheNode.slots = newCacheNodeSlots = {}; taskChildren = new Map(); for(let parallelRouteKey in newSlots){ let newRouteTreeChild = newSlots[parallelRouteKey]; const oldRouterStateChild = oldRouterStateChildren[parallelRouteKey]; if (oldRouterStateChild === undefined) { // This should never happen, but if it does, it suggests a malformed // server response. Trigger a full-page navigation. return null; } let seedDataChild = seedDataChildren !== null ? seedDataChildren[parallelRouteKey] : null; const oldSegmentChild = oldRouterStateChild[0]; let newSegmentChild = createSegmentFromRouteTree(newRouteTreeChild); let seedHeadChild = seedHead; if (// Skip this branch during a history traversal. We restore the tree that // was stashed in the history entry as-is. freshness !== 2 && newSegmentChild === _segment.DEFAULT_SEGMENT_KEY && oldSegmentChild !== _segment.DEFAULT_SEGMENT_KEY) { // This is a "default" segment. These are never sent by the server during // a soft navigation; instead, the client reuses whatever segment was // already active in that slot on the previous route. newRouteTreeChild = reuseActiveSegmentInDefaultSlot(newRouteTree, parallelRouteKey, oldRootRefreshState, oldRouterStateChild); newSegmentChild = createSegmentFromRouteTree(newRouteTreeChild); // Since we're switching to a different route tree, these are no // longer valid, because they correspond to the outer tree. seedDataChild = null; seedHeadChild = null; } const oldCacheNodeChild = oldCacheNodeSlots !== null ? oldCacheNodeSlots[parallelRouteKey] : undefined; const taskChild = updateCacheNodeOnNavigation(navigatedAt, oldUrl, oldCacheNodeChild, oldRouterStateChild, newRouteTreeChild, newMetadataVaryPath, freshness, childDidFindRootLayout, seedDataChild ?? null, seedHeadChild, seedDynamicStaleAt, isSamePageNavigation, parentNeedsDynamicRequest || needsDynamicRequest, oldRootRefreshState, refreshState, accumulation); if (taskChild === null) { // One of the child tasks discovered a change to the root layout. // Immediately unwind from this recursive traversal. This will trigger a // full-page navigation. return null; } // Recursively propagate up the child tasks. taskChildren.set(parallelRouteKey, taskChild); newCacheNodeSlots[parallelRouteKey] = taskChild.node; // The child tree's route state may be different from the prefetched // route sent by the server. We need to clone it as we traverse back up // the tree. const taskChildRoute = taskChild.route; patchedRouterStateChildren[parallelRouteKey] = taskChildRoute; const dynamicRequestTreeChild = taskChild.dynamicRequestTree; if (dynamicRequestTreeChild !== null) { // Something in the child tree is dynamic. childNeedsDynamicRequest = true; dynamicRequestTreeChildren[parallelRouteKey] = dynamicRequestTreeChild; } else { dynamicRequestTreeChildren[parallelRouteKey] = taskChildRoute; } } } const newFlightRouterState = [ createSegmentFromRouteTree(newRouteTree), patchedRouterStateChildren, refreshState !== null ? [ refreshState.canonicalUrl, refreshState.renderedSearch ] : null, null, newRouteTree.prefetchHints ]; return { status: needsDynamicRequest ? 0 : 1, route: newFlightRouterState, node: newCacheNode, dynamicRequestTree: createDynamicRequestTree(newFlightRouterState, dynamicRequestTreeChildren, needsDynamicRequest, childNeedsDynamicRequest, parentNeedsDynamicRequest), refreshState, children: taskChildren }; } /** * Assigns a ScrollRef to a new leaf CacheNode so the scroll handler * knows to scroll to it after navigation. All leaves in the same * navigation share the same ScrollRef — the first segment to scroll * consumes it, preventing others from also scrolling. * * This is only called inside `createCacheNodeOnNavigation`, which only * runs when segments diverge from the previous route. So for a refresh * where the route structure stays the same, segments match, the update * path is taken, and this function is never called — no scroll ref is * assigned. A scroll ref is only assigned when the route actually * changed (e.g. a redirect, or a dynamic condition on the server that * produces a different route). * * Skipped during hydration (initial render should not scroll) and * history traversal (scroll restoration is handled separately). */ function accumulateScrollRef(freshness, cacheNode, accumulation) { switch(freshness){ case 0: case 5: case 3: case 4: if (accumulation.scrollRef === null) { accumulation.scrollRef = { current: true }; } cacheNode.scrollRef = accumulation.scrollRef; break; case 1: break; case 2: break; default: freshness; break; } } function createCacheNodeOnNavigation(navigatedAt, newRouteTree, newMetadataVaryPath, freshness, seedData, seedHead, seedDynamicStaleAt, parentNeedsDynamicRequest, accumulation) { // Same traversal as updateCacheNodeNavigation, but simpler. We switch to this // path once we reach the part of the tree that was not in the previous route. // We don't need to diff against the old tree, we just need to create a new // one. We also don't need to worry about any refresh-related logic. // // For the most part, this is a subset of updateCacheNodeOnNavigation, so any // change that happens in this function likely needs to be applied to that // one, too. However there are some places where the behavior intentionally // diverges, which is why we keep them separate. const newSegment = createSegmentFromRouteTree(newRouteTree); const newSlots = newRouteTree.slots; const seedDataChildren = seedData !== null ? seedData[1] : null; const seedRsc = seedData !== null ? seedData[0] : null; const result = createCacheNodeForSegment(navigatedAt, newRouteTree, seedRsc, newMetadataVaryPath, seedHead, freshness, seedDynamicStaleAt); const newCacheNode = result.cacheNode; const needsDynamicRequest = result.needsDynamicRequest; const isLeafSegment = newSlots === null; if (isLeafSegment) { accumulateScrollRef(freshness, newCacheNode, accumulation); } let patchedRouterStateChildren = {}; let taskChildren = null; let childNeedsDynamicRequest = false; let dynamicRequestTreeChildren = {}; let newCacheNodeSlots = null; if (newSlots !== null) { newCacheNode.slots = newCacheNodeSlots = {}; taskChildren = new Map(); for(let parallelRouteKey in newSlots){ const newRouteTreeChild = newSlots[parallelRouteKey]; const seedDataChild = seedDataChildren !== null ? seedDataChildren[parallelRouteKey] : null; const taskChild = createCacheNodeOnNavigation(navigatedAt, newRouteTreeChild, newMetadataVaryPath, freshness, seedDataChild ?? null, seedHead, seedDynamicStaleAt, parentNeedsDynamicRequest || needsDynamicRequest, accumulation); taskChildren.set(parallelRouteKey, taskChild); newCacheNodeSlots[parallelRouteKey] = taskChild.node; const taskChildRoute = taskChild.route; patchedRouterStateChildren[parallelRouteKey] = taskChildRoute; const dynamicRequestTreeChild = taskChild.dynamicRequestTree; if (dynamicRequestTreeChild !== null) { childNeedsDynamicRequest = true; dynamicRequestTreeChildren[parallelRouteKey] = dynamicRequestTreeChild; } else { dynamicRequestTreeChildren[parallelRouteKey] = taskChildRoute; } } } const newFlightRouterState = [ newSegment, patchedRouterStateChildren, null, null, newRouteTree.prefetchHints ]; return { status: needsDynamicRequest ? 0 : 1, route: newFlightRouterState, node: newCacheNode, dynamicRequestTree: createDynamicRequestTree(newFlightRouterState, dynamicRequestTreeChildren, needsDynamicRequest, childNeedsDynamicRequest, parentNeedsDynamicRequest), // This route is not part of the current tree, so there's no reason to // track the refresh URL. refreshState: null, children: taskChildren }; } function createSegmentFromRouteTree(newRouteTree) { if (newRouteTree.isPage) { // In a dynamic server response, the server embeds the search params into // the segment key, but in a static one it's omitted. The client handles // this inconsistency by adding the search params back right at the end. // // TODO: The only thing this is used for is to create a cache key for // ChildSegmentMap. But we already track the `renderedSearch` everywhere as // part of the varyPath. The plan is get rid of ChildSegmentMap and // store the page data in a CacheMap using the varyPath, like we do // for prefetches. Then we can remove it from the segment key. // // As an incremental step, we can grab the search params from the varyPath. const renderedSearch = (0, _varypath.getRenderedSearchFromVaryPath)(newRouteTree.varyPath); if (renderedSearch === null) { return _segment.PAGE_SEGMENT_KEY; } // This is based on equivalent logic in addSearchParamsIfPageSegment, used // on the server. const stringifiedQuery = JSON.stringify(Object.fromEntries(new URLSearchParams(renderedSearch))); return stringifiedQuery !== '{}' ? _segment.PAGE_SEGMENT_KEY + '?' + stringifiedQuery : _segment.PAGE_SEGMENT_KEY; } return newRouteTree.segment; } function patchRouterStateWithNewChildren(baseRouterState, newChildren) { const clone = [ baseRouterState[0], newChildren ]; // Based on equivalent logic in apply-router-state-patch-to-tree, but should // confirm whether we need to copy all of these fields. Not sure the server // ever sends, e.g. the refetch marker. if (2 in baseRouterState) { clone[2] = baseRouterState[2]; } if (3 in baseRouterState) { clone[3] = baseRouterState[3]; } if (4 in baseRouterState) { clone[4] = baseRouterState[4]; } return clone; } function createDynamicRequestTree(newRouterState, dynamicRequestTreeChildren, needsDynamicRequest, childNeedsDynamicRequest, parentNeedsDynamicRequest) { // Create a FlightRouterState that instructs the server how to render the // requested segment. // // Or, if neither this segment nor any of the children require a new data, // then we return `null` to skip the request. let dynamicRequestTree = null; if (needsDynamicRequest) { dynamicRequestTree = patchRouterStateWithNewChildren(newRouterState, dynamicRequestTreeChildren); // The "refetch" marker is set on the top-most segment that requires new // data. We can omit it if a parent was already marked. if (!parentNeedsDynamicRequest) { dynamicRequestTree[3] = 'refetch'; } } else if (childNeedsDynamicRequest) { // This segment does not request new data, but at least one of its // children does. dynamicRequestTree = patchRouterStateWithNewChildren(newRouterState, dynamicRequestTreeChildren); } else { dynamicRequestTree = null; } return dynamicRequestTree; } function accumulateRefreshUrl(accumulation, refreshState) { // This is a refresh navigation, and we're inside a "default" slot that's // not part of the current route; it was reused from an older route. In // order to get fresh data for this reused route, we need to issue a // separate request using the old route's URL. // // Track these extra URLs in the accumulated result. Later, we'll construct // an appropriate request for each unique URL in the final set. The reason // we don't do it immediately here is so we can deduplicate multiple // instances of the same URL into a single request. See // listenForDynamicRequest for more details. const refreshUrl = refreshState.canonicalUrl; const separateRefreshUrls = accumulation.separateRefreshUrls; if (separateRefreshUrls === null) { accumulation.separateRefreshUrls = new Set([ refreshUrl ]); } else { separateRefreshUrls.add(refreshUrl); } } function reuseActiveSegmentInDefaultSlot(parentRouteTree, parallelRouteKey, oldRootRefreshState, oldRouterState) { // This is a "default" segment. These are never sent by the server during a // soft navigation; instead, the client reuses whatever segment was already // active in that slot on the previous route. This means if we later need to // refresh the segment, it will have to be refetched from the previous route's // URL. We store it in the Flight Router State. let reusedUrl; let reusedRenderedSearch; const oldRefreshState = oldRouterState[2]; if (oldRefreshState !== undefined && oldRefreshState !== null) { // This segment was already reused from an even older route. Keep its // existing URL and refresh state. reusedUrl = oldRefreshState[0]; reusedRenderedSearch = oldRefreshState[1]; } else { // Since this route didn't already have a refresh state, it must have been // reachable from the root of the old route. So we use the refresh state // that represents the old route. reusedUrl = oldRootRefreshState.canonicalUrl; reusedRenderedSearch = oldRootRefreshState.renderedSearch; } const acc = { metadataVaryPath: null }; const reusedRouteTree = (0, _cache.convertReusedFlightRouterStateToRouteTree)(parentRouteTree, parallelRouteKey, oldRouterState, reusedRenderedSearch, acc); reusedRouteTree.refreshState = { canonicalUrl: reusedUrl, renderedSearch: reusedRenderedSearch }; return reusedRouteTree; } function reuseSharedCacheNode(dropPrefetchRsc, existingCacheNode) { // Clone the CacheNode that was already present in the previous tree. // Carry forward the scrollRef so scroll intent from a prior navigation // survives tree rebuilds (e.g. push + refresh in the same batch). return createCacheNode(existingCacheNode.rsc, dropPrefetchRsc ? null : existingCacheNode.prefetchRsc, existingCacheNode.head, dropPrefetchRsc ? null : existingCacheNode.prefetchHead, existingCacheNode.scrollRef); } function createCacheNodeForSegment(now, tree, seedRsc, metadataVaryPath, seedHead, freshness, dynamicStaleAt) { // Construct a new CacheNode using data from the BFCache, the client's // Segment Cache, or seeded from a server response. // // If there's a cache miss, or if we only have a partial hit, we'll render // the partial state immediately, and spawn a request to the server to fill // in the missing data. // // If the segment is fully cached on the client already, we can omit this // segment from the server request. // // If we already have a dynamic data response associated with this navigation, // as in the case of a Server Action-initiated redirect or refresh, we may // also be able to use that data without spawning a new request. (This is // referred to as the "seed" data.) const isPage = tree.isPage; // During certain kinds of navigations, we may be able to render from // the BFCache. switch(freshness){ case 0: { // Check BFCache during regular navigations. The entry's staleAt // determines whether it's still fresh. This is used when // staleTimes.dynamic is configured globally or when a page exports // unstable_dynamicStaleTime for per-page control. const bfcacheEntry = (0, _bfcache.readFromBFCacheDuringRegularNavigation)(now, tree.varyPath); if (bfcacheEntry !== null) { return { cacheNode: createCacheNode(bfcacheEntry.rsc, bfcacheEntry.prefetchRsc, bfcacheEntry.head, bfcacheEntry.prefetchHead), needsDynamicRequest: false }; } break; } case 1: { // This is not related to the BFCache but it is a special case. // // We should never spawn network requests during hydration. We must treat // the initial payload as authoritative, because the initial page load is // used as a last-ditch mechanism for recovering the app. // // This is also an important safety check because if this leaks into the // server rendering path (which theoretically it never should because the // server payload should be consistent), the server would hang because these // promises would never resolve. // // TODO: There is an existing case where the global "not found" boundary // triggers this path. But it does render correctly despite that. That's an // unusual render path so it's not surprising, but we should look into // modeling it in a more consistent way. See also the /_notFound special // case in updateCacheNodeOnNavigation. const rsc = seedRsc; const prefetchRsc = null; const head = isPage ? seedHead : null; const prefetchHead = null; (0, _bfcache.writeToBFCache)(now, tree.varyPath, rsc, prefetchRsc, head, prefetchHead, dynamicStaleAt); if (isPage && metadataVaryPath !== null) { (0, _bfcache.writeHeadToBFCache)(now, metadataVaryPath, head, prefetchHead, dynamicStaleAt); } return { cacheNode: createCacheNode(rsc, prefetchRsc, head, prefetchHead), needsDynamicRequest: false }; } case 2: const bfcacheEntry = (0, _bfcache.readFromBFCache)(tree.varyPath); if (bfcacheEntry !== null) { // Only show prefetched data if the dynamic data is still pending. This // avoids a flash back to the prefetch state in a case where it's highly // likely to have already streamed in. // // Tehnically, what we're actually checking is whether the dynamic // network response was received. But since it's a streaming response, // this does not mean that all the dynamic data has fully streamed in. // It just means that _some_ of the dynamic data was received. But as a // heuristic, we assume that the rest dynamic data will stream in // quickly, so it's still better to skip the prefetch state. const oldRsc = bfcacheEntry.rsc; const oldRscDidResolve = !isDeferredRsc(oldRsc) || oldRsc.status !== 'pending'; const dropPrefetchRsc = oldRscDidResolve; return { cacheNode: createCacheNode(bfcacheEntry.rsc, dropPrefetchRsc ? null : bfcacheEntry.prefetchRsc, bfcacheEntry.head, dropPrefetchRsc ? null : bfcacheEntry.prefetchHead), needsDynamicRequest: false }; } break; case 3: case 4: case 5: break; default: freshness; break; } let cachedRsc = null; let isCachedRscPartial = true; const segmentEntry = (0, _cache.readSegmentCacheEntry)(now, tree.varyPath); if (segmentEntry !== null) { switch(segmentEntry.status){ case _cache.EntryStatus.Fulfilled: { // Happy path: a cache hit cachedRsc = segmentEntry.rsc; isCachedRscPartial = segmentEntry.isPartial; break; } case _cache.EntryStatus.Pending: { // We haven't received data for this segment yet, but there's already // an in-progress request. Since it's extremely likely to arrive // before the dynamic data response, we might as well use it. const promiseForFulfilledEntry = (0, _cache.waitForSegmentCacheEntry)(segmentEntry); cachedRsc = promiseForFulfilledEntry.then((entry)=>entry !== null ? entry.rsc : null); // Because the request is still pending, we typically don't know yet // whether the response will be partial. We shouldn't skip this segment // during the dynamic navigation request. Otherwise, we might need to // do yet another request to fill in the remaining data, creating // a waterfall. // // The one exception is if this segment is being fetched with via // prefetch={true} (i.e. the "force stale" or "full" strategy). If so, // we can assume the response will be full. This field is set to `false` // for such segments. isCachedRscPartial = segmentEntry.isPartial; break; } case _cache.EntryStatus.Empty: case _cache.EntryStatus.Rejected: { break; } default: { segmentEntry; break; } } } // Now combine the cached data with the seed data to determine what we can // render immediately, versus what needs to stream in later. // A partial state to show immediately while we wait for the final data to // arrive. If `rsc` is already a complete value (not partial), or if we // don't have any useful partial state, this will be `null`. let prefetchRsc; // The final, resolved segment data. If the data is missing, this will be a // promise that resolves to the eventual data. A resolved value of `null` // means the data failed to load; the LayoutRouter will suspend indefinitely // until the router updates again (refer to finishNavigationTask). let rsc; let doesSegmentNeedDynamicRequest; if (seedRsc !== null) { // We already have a dynamic server response for this segment. if (isCachedRscPartial) { // The seed data may still be streaming in, so it's worth showing the // partial cached state in the meantime. prefetchRsc = cachedRsc; rsc = seedRsc; } else { // We already have a completely cached segment. Ignore the seed data, // which may still be streaming in. This shouldn't happen in the normal // case because the client will inform the server which segments are // already fully cached, and the server will skip rendering them. prefetchRsc = null; rsc = cachedRsc; } doesSegmentNeedDynamicRequest = false; } else { if (isCachedRscPartial) { // The cached data contains dynamic holes, or it's missing entirely. We'll // show the partial state immediately (if available), and stream in the // final data. // // Create a pending promise that we can later write to when the // data arrives from the server. prefetchRsc = cachedRsc; rsc = createDeferredRsc(); } else { // The data is fully cached. prefetchRsc = null; rsc = cachedRsc; } doesSegmentNeedDynamicRequest = isCachedRscPartial; } // If this is a page segment, we need to do the same for the head. This // follows analogous logic to the segment data above. // TODO: We don't need to store the head on the page segment's CacheNode; we // can lift it to the main state object. Then we can also delete // findHeadCache. let prefetchHead = null; let head = null; let doesHeadNeedDynamicRequest = isPage; if (isPage) { let cachedHead = null; let isCachedHeadPartial = true; if (metadataVaryPath !== null) { const metadataEntry = (0, _cache.readSegmentCacheEntry)(now, metadataVaryPath); if (metadataEntry !== null) { switch(metadataEntry.status){ case _cache.EntryStatus.Fulfilled: { cachedHead = metadataEntry.rsc; isCachedHeadPartial = metadataEntry.isPartial; break; } case _cache.EntryStatus.Pending: { cachedHead = (0, _cache.waitForSegmentCacheEntry)(metadataEntry).then((entry)=>entry !== null ? entry.rsc : null); isCachedHeadPartial = metadataEntry.isPartial; break; } case _cache.EntryStatus.Empty: case _cache.EntryStatus.Rejected: { break; } default: { metadataEntry; break; } } } } if (process.env.__NEXT_OPTIMISTIC_ROUTING && isCachedHeadPartial) { // TODO: When optimistic routing is enabled, don't block on waiting for // the viewport to resolve. This is a temporary workaround until Vary // Params are tracked when rendering the metadata. We'll fix it before // this feature is stable. However, it's not a critical issue because 1) // it will stream in eventually anyway 2) metadata is wrapped in an // internal Suspense boundary, so is always non-blocking; this only // affects the viewport node, which is meant to blocking, however... 3) // before Segment Cache landed this wasn't always the case, anyway, so // it's unlikely that many people are relying on this behavior. Still, // will be fixed before stable. It's the very next step in the sequence of // work on this project. // // This line of code works because the App Router treats `null` as // "no renderable head available", rather than an empty head. React treats // an empty string as empty. cachedHead = ''; } if (seedHead !== null) { if (isCachedHeadPartial) { prefetchHead = cachedHead; head = seedHead; } else { prefetchHead = null; head = cachedHead; } doesHeadNeedDynamicRequest = false; } else { if (isCachedHeadPartial) { prefetchHead = cachedHead; head = createDeferredRsc(); } else { prefetchHead = null; head = cachedHead; } doesHeadNeedDynamicRequest = isCachedHeadPartial; } } // Now that we're creating a new segment, write its data to the BFCache. A // subsequent back/forward navigation will reuse this same data, until or // unless it's cleared by a refresh/revalidation. // // Skip BFCache writes for optimistic navigations since they are transient // and will be replaced by the canonical navigation. if (freshness !== 5) { (0, _bfcache.writeToBFCache)(now, tree.varyPath, rsc, prefetchRsc, head, prefetchHead, dynamicStaleAt); if (isPage && metadataVaryPath !== null) { (0, _bfcache.writeHeadToBFCache)(now, metadataVaryPath, head, prefetchHead, dynamicStaleAt); } } return { cacheNode: createCacheNode(rsc, prefetchRsc, head, prefetchHead), // TODO: We should store this field on the CacheNode itself. I think we can // probably unify NavigationTask, CacheNode, and DeferredRsc into a // single type. Or at least CacheNode and DeferredRsc. needsDynamicRequest: doesSegmentNeedDynamicRequest || doesHeadNeedDynamicRequest }; } function createCacheNode(rsc, prefetchRsc, head, prefetchHead, scrollRef = null) { return { rsc, prefetchRsc, head, prefetchHead, slots: null, scrollRef }; } // Represents whether the previuos navigation resulted in a route tree mismatch. // A mismatch results in a refresh of the page. If there are two successive // mismatches, we will fall back to an MPA navigation, to prevent a retry loop. let previousNavigationDidMismatch = false; function spawnDynamicRequests(task, primaryUrl, nextUrl, freshnessPolicy, accumulation, // The route cache entry used for this navigation, if it came from route // prediction. Passed through so it can be marked as having a dynamic rewrite // if the server returns a different pathname than expected (indicating // dynamic rewrite behavior that varies by param value). routeCacheEntry, // The original navigation's push/replace intent. Threaded through to the // server-patch retry logic so it can inherit the intent if the original // transition hasn't committed yet. navigateType) { const dynamicRequestTree = task.dynamicRequestTree; if (dynamicRequestTree === null) { // This navigation was fully cached. There are no dynamic requests to spawn. previousNavigationDidMismatch = false; return; } // This is intentionally not an async function to discourage the caller from // awaiting the result. Any subsequent async operations spawned by this // function should result in a separate navigation task, rather than // block the original one. // // In this function we spawn (but do not await) all the network requests that // block the navigation, and collect the promises. The next function, // `finishNavigationTask`, can await the promises in any order without // accidentally introducing a network waterfall. const primaryRequestPromise = fetchMissingDynamicData(task, dynamicRequestTree, primaryUrl, nextUrl, freshnessPolicy, routeCacheEntry); const separateRefreshUrls = accumulation.separateRefreshUrls; let refreshRequestPromises = null; if (separateRefreshUrls !== null) { // There are multiple URLs that we need to request the data from. This // happens when a "default" parallel route slot is present in the tree, and // its data cannot be fetched from the current route. We need to split the // combined dynamic request tree into separate requests per URL. // TODO: Create a scoped dynamic request tree that omits anything that // is not relevant to the given URL. Without doing this, the server may // sometimes render more data than necessary; this is not a regression // compared to the pre-Segment Cache implementation, though, just an // optimization we can make in the future. // Construct a request tree for each additional refresh URL. This will // prune away everything except the parts of the tree that match the // given refresh URL. refreshRequestPromises = []; const canonicalUrl = (0, _createhreffromurl.createHrefFromUrl)(primaryUrl); for (const refreshUrl of separateRefreshUrls){ if (refreshUrl === canonicalUrl) { continue; } // TODO: Create a scoped dynamic request tree that omits anything that // is not relevant to the given URL. Without doing this, the server may // sometimes render more data than necessary; this is not a regression // compared to the pre-Segment Cache implementation, though, just an // optimization we can make in the future. // const scopedDynamicRequestTree = splitTaskByURL(task, refreshUrl) const scopedDynamicRequestTree = dynamicRequestTree; if (scopedDynamicRequestTree !== null) { refreshRequestPromises.push(fetchMissingDynamicData(task, scopedDynamicRequestTree, new URL(refreshUrl, location.origin), // TODO: Just noticed that this should actually the Next-Url at the // time the refresh URL was set, not the current Next-Url. Need to // start tracking this alongside the refresh URL. In the meantime, // if a refresh fails due to a mismatch, it will trigger a // hard refresh. nextUrl, freshnessPolicy, routeCacheEntry)); } } } // Further async operations are moved into this separate function to // discourage sequential network requests. const voidPromise = finishNavigationTask(task, nextUrl, primaryRequestPromise, refreshRequestPromises, routeCacheEntry, navigateType); // `finishNavigationTask` is responsible for error handling, so we can attach // noop callbacks to this promise. voidPromise.then(noop, noop); } async function finishNavigationTask(task, nextUrl, primaryRequestPromise, refreshRequestPromises, routeCacheEntry, navigateType) { // Wait for all the requests to finish, or for the first one to fail. let exitStatus = await waitForRequestsToFinish(primaryRequestPromise, refreshRequestPromises); // Once the all the requests have finished, check the tree for any remaining // pending tasks. If anything is still pending, it means the server response // does not match the client, and we must refresh to get back to a consistent // state. We can skip this step if we already detected a mismatch during the // first phase; it doesn't matter in that case because we're going to refresh // the whole tree regardless. if (exitStatus === 0) { exitStatus = abortRemainingPendingTasks(task, null, null); } switch(exitStatus){ case 0: { // The task has completely finished. There's no missing data. Exit. previousNavigationDidMismatch = false; return; } case 1: { // Some data failed to finish loading. Trigger a soft retry. // TODO: As an extra precaution against soft retry loops, consider // tracking whether a navigation was itself triggered by a retry. If two // happen in a row, fall back to a hard retry. const isHardRetry = false; const primaryRequestResult = await primaryRequestPromise; dispatchRetryDueToTreeMismatch(isHardRetry, primaryRequestResult.url, nextUrl, primaryRequestResult.seed, task.route, routeCacheEntry, navigateType); return; } case 2: { // Some data failed to finish loading in a non-recoverable way, such as a // network error. Trigger an MPA navigation. // // Hard navigating/refreshing is how we prevent an infinite retry loop // caused by a network error — when the network fails, we fall back to the // browser behavior for offline navigations. In the future, Next.js may // introduce its own custom handling of offline navigations, but that // doesn't exist yet. const isHardRetry = true; const primaryRequestResult = await primaryRequestPromise; dispatchRetryDueToTreeMismatch(isHardRetry, primaryRequestResult.url, nextUrl, primaryRequestResult.seed, task.route, routeCacheEntry, navigateType); return; } default: { return exitStatus; } } } function waitForRequestsToFinish(primaryRequestPromise, refreshRequestPromises) { // Custom async combinator logic. This could be replaced by Promise.any but // we don't assume that's available. // // Each promise resolves once the server responsds and the data is written // into the CacheNode tree. Resolve the combined promise once all the // requests finish. // // Or, resolve as soon as one of the requests fails, without waiting for the // others to finish. return new Promise((resolve)=>{ const onFulfill = (result)=>{ if (result.exitStatus === 0) { remainingCount--; if (remainingCount === 0) { // All the requests finished successfully. resolve(0); } } else { // One of the requests failed. Exit with a failing status. // NOTE: It's possible for one of the requests to fail with SoftRetry // and a later one to fail with HardRetry. In this case, we choose to // retry immediately, rather than delay the retry until all the requests // finish. If it fails again, we will hard retry on the next // attempt, anyway. resolve(result.exitStatus); } }; // onReject shouldn't ever be called because fetchMissingDynamicData's // entire body is wrapped in a try/catch. This is just defensive. const onReject = ()=>resolve(2); // Attach the listeners to the promises. let remainingCount = 1; primaryRequestPromise.then(onFulfill, onReject); if (refreshRequestPromises !== null) { remainingCount += refreshRequestPromises.length; refreshRequestPromises.forEach((refreshRequestPromise)=>refreshRequestPromise.then(onFulfill, onReject)); } }); } function dispatchRetryDueToTreeMismatch(isHardRetry, retryUrl, retryNextUrl, seed, baseTree, // The route cache entry used for this navigation, if it came from route // prediction. If the navigation results in a mismatch, we mark it as having // a dynamic rewrite so future predictions bail out. routeCacheEntry, // The original navigation's push/replace intent. originalNavigateType) { // If the navigation used a route prediction, mark it as having a dynamic // rewrite since it resulted in a mismatch. if (routeCacheEntry !== null) { (0, _cache.markRouteEntryAsDynamicRewrite)(routeCacheEntry); } else if (seed !== null) { // Even without a direct reference to the route cache entry, we can still // mark the route as having a dynamic rewrite by traversing the known route // tree. This handles cases where the navigation didn't originate from a // route prediction, but still needs to mark the pattern. const metadataVaryPath = seed.metadataVaryPath; if (metadataVaryPath !== null) { const now = Date.now(); (0, _optimisticroutes.discoverKnownRoute)(now, retryUrl.pathname, retryNextUrl, null, seed.routeTree, metadataVaryPath, false, (0, _createhreffromurl.createHrefFromUrl)(retryUrl), false, true // hasDynamicRewrite ); } } // Invalidate all route cache entries. Other entries may have been derived // from the template before we knew it had a dynamic rewrite. This also // triggers re-prefetching of visible links. (0, _cache.invalidateRouteCacheEntries)(retryNextUrl, baseTree); // If this is the second time in a row that a navigation resulted in a // mismatch, fall back to a hard (MPA) refresh. isHardRetry = isHardRetry || previousNavigationDidMismatch; previousNavigationDidMismatch = true; // If the original navigation hasn't committed to the browser history yet // (the transition suspended before React committed), inherit its push/replace // intent. Otherwise, the pushState already ran, so use 'replace' to avoid // creating a duplicate history entry. // // This works because React entangles the retry's state update with the // original pending transition — they commit together as a single batch, // so the navigate type from the retry is what HistoryUpdater ultimately sees. // // TODO: Ideally this check would happen right before we schedule the React // update (i.e., closer to where the action is dispatched into the queue), // not here where the action is constructed. But the current action queue // doesn't provide a natural place for that. Revisit when we refactor the // action queue into a more reactive navigation model. const lastCommitted = (0, _committedstate.getLastCommittedTree)(); const retryNavigateType = lastCommitted !== null && baseTree !== lastCommitted ? originalNavigateType : 'replace'; const retryAction = { type: _routerreducertypes.ACTION_SERVER_PATCH, previousTree: baseTree, url: retryUrl, nextUrl: retryNextUrl, seed, mpa: isHardRetry, navigateType: retryNavigateType }; (0, _useactionqueue.dispatchAppRouterAction)(retryAction); } async function fetchMissingDynamicData(task, dynamicRequestTree, url, nextUrl, freshnessPolicy, routeCacheEntry) { try { const result = await (0, _fetchserverresponse.fetchServerResponse)(url, { flightRouterState: dynamicRequestTree, nextUrl, isHmrRefresh: freshnessPolicy === 4 }); if (typeof result === 'string') { // fetchServerResponse will return an href to indicate that the SPA // navigation failed. For example, if the server triggered a hard // redirect, or the fetch request errored. Initiate an MPA navigation // to the given href. return { exitStatus: 2, url: new URL(result, location.origin), seed: null }; } const now = Date.now(); const seed = (0, _navigation.convertServerPatchToFullTree)(now, task.route, result.flightData, result.renderedSearch, result.dynamicStaleTime); // If the navigation lock is active, wait for it to be released before // writing the dynamic data. This allows tests to assert on the prefetched // UI state. if (process.env.__NEXT_EXPOSE_TESTING_API) { await waitForNavigationLock(); } if (routeCacheEntry !== null && result.staticStageData !== null) { const { response: staticStageResponse, isResponsePartial } = result.staticStageData; (0, _cache.getStaleAt)(now, staticStageResponse.s).then((staleAt)=>{ const buildId = result.responseHeaders.get(_constants.NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? staticStageResponse.b; (0, _cache.writeStaticStageResponseIntoCache)(now, staticStageResponse.f, buildId, staticStageResponse.h, staleAt, dynamicRequestTree, result.renderedSearch, isResponsePartial); }).catch(()=>{ // The static stage processing failed. Not fatal — the navigation // completed normally, we just won't write into the cache. }); } if (routeCacheEntry !== null && result.runtimePrefetchStream !== null) { (0, _cache.processRuntimePrefetchStream)(now, result.runtimePrefetchStream, dynamicRequestTree, result.renderedSearch).then((processed)=>{ if (processed !== null) { (0, _cache.writeDynamicRenderResponseIntoCache)(now, _types.FetchStrategy.PPRRuntime, processed.flightDatas, processed.buildId, processed.isResponsePartial, processed.headVaryParams, processed.staleAt, processed.navigationSeed, null); } }).catch(()=>{ // The runtime prefetch cache write failed. Not fatal — the // navigation completed normally, we just won't cache runtime data. }); } // result.dynamicStaleTime is in seconds (from the server's `d` field). // Convert to an absolute timestamp using the centralized helper. const dynamicStaleAt = (0, _bfcache.computeDynamicStaleAt)(now, result.dynamicStaleTime); const didReceiveUnknownParallelRoute = writeDynamicDataIntoNavigationTask(task, seed.routeTree, seed.data, seed.head, dynamicStaleAt, result.debugInfo); return { exitStatus: didReceiveUnknownParallelRoute ? 1 : 0, url: new URL(result.canonicalUrl, location.origin), seed }; } catch { // This shouldn't happen because fetchServerResponse's entire body is // wrapped in a try/catch. If it does, though, it implies the server failed // to respond with any tree at all. So we must fall back to a hard retry. return { exitStatus: 2, url: url, seed: null }; } } function writeDynamicDataIntoNavigationTask(task, serverRouteTree, dynamicData, dynamicHead, dynamicStaleAt, debugInfo) { if (task.status === 0 && dynamicData !== null) { task.status = 1; finishPendingCacheNode(task.node, dynamicData, dynamicHead, debugInfo); // Update the BFCache entry's staleAt for this segment with the value // from the dynamic response. This applies the per-page // unstable_dynamicStaleTime if set, or the default DYNAMIC_STALETIME_MS. // We only update segments that received dynamic data — static segments // are unaffected. (0, _bfcache.updateBFCacheEntryStaleAt)(serverRouteTree.varyPath, dynamicStaleAt); } const taskChildren = task.children; const serverChildren = serverRouteTree.slots; const dynamicDataChildren = dynamicData !== null ? dynamicData[1] : null; // Detect whether the server sends a parallel route slot that the client // doesn't know about. let didReceiveUnknownParallelRoute = false; if (taskChildren !== null) { if (serverChildren !== null) { for(const parallelRouteKey in serverChildren){ const serverRouteTreeChild = serverChildren[parallelRouteKey]; const dynamicDataChild = dynamicDataChildren !== null ? dynamicDataChildren[parallelRouteKey] : null; const taskChild = taskChildren.get(parallelRouteKey); if (taskChild === undefined) { // The server sent a child segment that the client doesn't know about. // // When we receive an unknown parallel route, we must consider it a // mismatch. This is unlike the case where the segment itself // mismatches, because multiple routes can be active simultaneously. // But a given layout should never have a mismatching set of // child slots. // // Theoretically, this should only happen in development during an HMR // refresh, because the set of parallel routes for a layout does not // change over the lifetime of a build/deployment. In production, we // should have already mismatched on either the build id or the segment // path. But as an extra precaution, we validate in prod, too. didReceiveUnknownParallelRoute = true; } else { const taskSegment = taskChild.route[0]; const serverSegment = createSegmentFromRouteTree(serverRouteTreeChild); if ((0, _matchsegments.matchSegment)(serverSegment, taskSegment) && dynamicDataChild !== null && dynamicDataChild !== undefined) { // Found a match for this task. Keep traversing down the task tree. const childDidReceiveUnknownParallelRoute = writeDynamicDataIntoNavigationTask(taskChild, serverRouteTreeChild, dynamicDataChild, dynamicHead, dynamicStaleAt, debugInfo); if (childDidReceiveUnknownParallelRoute) { didReceiveUnknownParallelRoute = true; } } } } } else { if (serverChildren !== null) { // The server sent a child segment that the client doesn't know about. didReceiveUnknownParallelRoute = true; } } } return didReceiveUnknownParallelRoute; } function finishPendingCacheNode(cacheNode, dynamicData, dynamicHead, debugInfo) { // Writes a dynamic response into an existing Cache Node tree. This does _not_ // create a new tree, it updates the existing tree in-place. So it must follow // the Suspense rules of cache safety — it can resolve pending promises, but // it cannot overwrite existing data. It can add segments to the tree (because // a missing segment will cause the layout router to suspend). // but it cannot delete them. // // We must resolve every promise in the tree, or else it will suspend // indefinitely. If we did not receive data for a segment, we will resolve its // data promise to `null` to trigger a lazy fetch during render. // Use the dynamic data from the server to fulfill the deferred RSC promise // on the Cache Node. const rsc = cacheNode.rsc; const dynamicSegmentData = dynamicData[0]; if (dynamicSegmentData === null) { // This is an empty CacheNode; this particular server request did not // render this segment. There may be a separate pending request that will, // though, so we won't abort the task until all pending requests finish. return; } if (rsc === null) { // This is a lazy cache node. We can overwrite it. This is only safe // because we know that the LayoutRouter suspends if `rsc` is `null`. cacheNode.rsc = dynamicSegmentData; } else if (isDeferredRsc(rsc)) { // This is a deferred RSC promise. We can fulfill it with the data we just // received from the server. If it was already resolved by a different // navigation, then this does nothing because we can't overwrite data. rsc.resolve(dynamicSegmentData, debugInfo); } else { // This is not a deferred RSC promise, nor is it empty, so it must have // been populated by a different navigation. We must not overwrite it. } // Check if this is a leaf segment. If so, it will have a `head` property with // a pending promise that needs to be resolved with the dynamic head from // the server. const head = cacheNode.head; if (isDeferredRsc(head)) { head.resolve(dynamicHead, debugInfo); } } function abortRemainingPendingTasks(task, error, debugInfo) { let exitStatus; if (task.status === 0) { // The data for this segment is still missing. task.status = 2; abortPendingCacheNode(task.node, error, debugInfo); // If the server failed to fulfill the data for this segment, it implies // that the route tree received from the server mismatched the tree that // was previously prefetched. // // In an app with fully static routes and no proxy-driven redirects or // rewrites, this should never happen, because the route for a URL would // always be the same across multiple requests. So, this implies that some // runtime routing condition changed, likely in a proxy, without being // pushed to the client. // // When this happens, we treat this the same as a refresh(). The entire // tree will be re-rendered from the root. if (task.refreshState === null) { // Trigger a "soft" refresh. Essentially the same as calling `refresh()` // in a Server Action. exitStatus = 1; } else { // The mismatch was discovered inside an inactive parallel route. This // implies the inactive parallel route is no longer reachable at the URL // that originally rendered it. Fall back to an MPA refresh. // TODO: An alternative could be to trigger a soft refresh but to _not_ // re-use the inactive parallel routes this time. Similar to what would // happen if were to do a hard refrehs, but without the HTML page. exitStatus = 2; } } else { // This segment finished. (An error here is treated as Done because they are // surfaced to the application during render.) exitStatus = 0; } const taskChildren = task.children; if (taskChildren !== null) { for (const [, taskChild] of taskChildren){ const childExitStatus = abortRemainingPendingTasks(taskChild, error, debugInfo); // Propagate the exit status up the tree. The statuses are ordered by // their precedence. if (childExitStatus > exitStatus) { exitStatus = childExitStatus; } } } return exitStatus; } function abortPendingCacheNode(cacheNode, error, debugInfo) { const rsc = cacheNode.rsc; if (isDeferredRsc(rsc)) { if (error === null) { // This will trigger a lazy fetch during render. rsc.resolve(null, debugInfo); } else { // This will trigger an error during rendering. rsc.reject(error, debugInfo); } } // Check if this is a leaf segment. If so, it will have a `head` property with // a pending promise that needs to be resolved. If an error was provided, we // will not resolve it with an error, since this is rendered at the root of // the app. We want the segment to error, not the entire app. const head = cacheNode.head; if (isDeferredRsc(head)) { head.resolve(null, debugInfo); } } const DEFERRED = Symbol(); function isDeferredRsc(value) { return value && typeof value === 'object' && value.tag === DEFERRED; } function createDeferredRsc() { // Create an unresolved promise that represents data derived from a Flight // response. The promise will be resolved later as soon as we start receiving // data from the server, i.e. as soon as the Flight client decodes and returns // the top-level response object. // The `_debugInfo` field contains profiling information. Promises that are // created by Flight already have this info added by React; for any derived // promise created by the router, we need to transfer the Flight debug info // onto the derived promise. // // The debug info represents the latency between the start of the navigation // and the start of rendering. (It does not represent the time it takes for // whole stream to finish.) const debugInfo = []; let resolve; let reject; const pendingRsc = new Promise((res, rej)=>{ resolve = res; reject = rej; }); pendingRsc.status = 'pending'; pendingRsc.resolve = (value, responseDebugInfo)=>{ if (pendingRsc.status === 'pending') { const fulfilledRsc = pendingRsc; fulfilledRsc.status = 'fulfilled'; fulfilledRsc.value = value; if (responseDebugInfo !== null) { // Transfer the debug info to the derived promise. debugInfo.push.apply(debugInfo, responseDebugInfo); } resolve(value); } }; pendingRsc.reject = (error, responseDebugInfo)=>{ if (pendingRsc.status === 'pending') { const rejectedRsc = pendingRsc; rejectedRsc.status = 'rejected'; rejectedRsc.reason = error; if (responseDebugInfo !== null) { // Transfer the debug info to the derived promise. debugInfo.push.apply(debugInfo, responseDebugInfo); } reject(error); } }; pendingRsc.tag = DEFERRED; pendingRsc._debugInfo = debugInfo; return pendingRsc; } /** * Helper for the Instant Navigation Testing API. Waits for the navigation lock * to be released before returning. The network request has already completed by * the time this is called, so this only delays writing the dynamic data. * * Not exposed in production builds by default. */ async function waitForNavigationLock() { if (process.env.__NEXT_EXPOSE_TESTING_API) { const { waitForNavigationLockIfActive } = require('../segment-cache/navigation-testing-lock'); await waitForNavigationLockIfActive(); } } if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') { Object.defineProperty(exports.default, '__esModule', { value: true }); Object.assign(exports.default, exports); module.exports = exports.default; } //# sourceMappingURL=ppr-navigations.js.map