Spaces:
Running
Running
| ; | |
| 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 |