UNPKG

next

Version:

The React Framework

956 lines 70.2 kB
"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.