.
This commit is contained in:
+597
@@ -0,0 +1,597 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
0 && (module.exports = {
|
||||
completeHardNavigation: null,
|
||||
completeSoftNavigation: null,
|
||||
completeTraverseNavigation: null,
|
||||
convertServerPatchToFullTree: null,
|
||||
navigate: null,
|
||||
navigateToKnownRoute: null
|
||||
});
|
||||
function _export(target, all) {
|
||||
for(var name in all)Object.defineProperty(target, name, {
|
||||
enumerable: true,
|
||||
get: all[name]
|
||||
});
|
||||
}
|
||||
_export(exports, {
|
||||
completeHardNavigation: function() {
|
||||
return completeHardNavigation;
|
||||
},
|
||||
completeSoftNavigation: function() {
|
||||
return completeSoftNavigation;
|
||||
},
|
||||
completeTraverseNavigation: function() {
|
||||
return completeTraverseNavigation;
|
||||
},
|
||||
convertServerPatchToFullTree: function() {
|
||||
return convertServerPatchToFullTree;
|
||||
},
|
||||
navigate: function() {
|
||||
return navigate;
|
||||
},
|
||||
navigateToKnownRoute: function() {
|
||||
return navigateToKnownRoute;
|
||||
}
|
||||
});
|
||||
const _fetchserverresponse = require("../router-reducer/fetch-server-response");
|
||||
const _pprnavigations = require("../router-reducer/ppr-navigations");
|
||||
const _createhreffromurl = require("../router-reducer/create-href-from-url");
|
||||
const _constants = require("../../../lib/constants");
|
||||
const _cache = require("./cache");
|
||||
const _optimisticroutes = require("./optimistic-routes");
|
||||
const _cachekey = require("./cache-key");
|
||||
const _scheduler = require("./scheduler");
|
||||
const _types = require("./types");
|
||||
const _links = require("../links");
|
||||
const _routerreducertypes = require("../router-reducer/router-reducer-types");
|
||||
const _computechangedpath = require("../router-reducer/compute-changed-path");
|
||||
const _javascripturl = require("../../lib/javascript-url");
|
||||
const _bfcache = require("./bfcache");
|
||||
function navigate(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType) {
|
||||
// Instant Navigation Testing API: when the lock is active, ensure a
|
||||
// prefetch task has been initiated before proceeding with the navigation.
|
||||
// This guarantees that segment data requests are at least pending, even
|
||||
// for routes that already have a cached route tree. Without this, the
|
||||
// static shell might be incomplete because some segments were never
|
||||
// requested.
|
||||
if (process.env.__NEXT_EXPOSE_TESTING_API) {
|
||||
const { isNavigationLocked } = require('./navigation-testing-lock');
|
||||
if (isNavigationLocked()) {
|
||||
return ensurePrefetchThenNavigate(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType);
|
||||
}
|
||||
}
|
||||
return navigateImpl(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType);
|
||||
}
|
||||
function navigateImpl(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType) {
|
||||
const now = Date.now();
|
||||
const href = url.href;
|
||||
const cacheKey = (0, _cachekey.createCacheKey)(href, nextUrl);
|
||||
const route = (0, _cache.readRouteCacheEntry)(now, cacheKey);
|
||||
if (route !== null && route.status === _cache.EntryStatus.Fulfilled) {
|
||||
// We have a matching prefetch.
|
||||
return navigateUsingPrefetchedRouteTree(now, state, url, currentUrl, currentRenderedSearch, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, scrollBehavior, navigateType, route);
|
||||
}
|
||||
// There was no matching route tree in the cache. Let's see if we can
|
||||
// construct an "optimistic" route tree using the deprecated search-params
|
||||
// based matching. This is only used when the new optimisticRouting flag is
|
||||
// disabled.
|
||||
//
|
||||
// Do not construct an optimistic route tree if there was a cache hit, but
|
||||
// the entry has a rejected status, since it may have been rejected due to a
|
||||
// rewrite or redirect based on the search params.
|
||||
//
|
||||
// TODO: There are multiple reasons a prefetch might be rejected; we should
|
||||
// track them explicitly and choose what to do here based on that.
|
||||
if (!process.env.__NEXT_OPTIMISTIC_ROUTING) {
|
||||
if (route === null || route.status !== _cache.EntryStatus.Rejected) {
|
||||
const optimisticRoute = (0, _cache.deprecated_requestOptimisticRouteCacheEntry)(now, url, nextUrl);
|
||||
if (optimisticRoute !== null) {
|
||||
// We have an optimistic route tree. Proceed with the normal flow.
|
||||
return navigateUsingPrefetchedRouteTree(now, state, url, currentUrl, currentRenderedSearch, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, scrollBehavior, navigateType, optimisticRoute);
|
||||
}
|
||||
}
|
||||
}
|
||||
// There's no matching prefetch for this route in the cache. We must lazily
|
||||
// fetch it from the server before we can perform the navigation.
|
||||
//
|
||||
// TODO: If this is a gesture navigation, instead of performing a
|
||||
// dynamic request, we should do a runtime prefetch.
|
||||
return navigateToUnknownRoute(now, state, url, currentUrl, currentRenderedSearch, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, scrollBehavior, navigateType).catch(()=>{
|
||||
// If the navigation fails, return the current state
|
||||
return state;
|
||||
});
|
||||
}
|
||||
function navigateToKnownRoute(now, state, url, canonicalUrl, navigationSeed, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, scrollBehavior, navigateType, debugInfo, // 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 (indicating dynamic rewrite
|
||||
// behavior).
|
||||
//
|
||||
// When null, the navigation did not use route prediction - either because
|
||||
// the route was already fully cached, or it's a navigation that doesn't
|
||||
// involve prediction (refresh, history traversal, server action, etc.).
|
||||
// In these cases, if a mismatch occurs, we still mark the route as having a
|
||||
// dynamic rewrite by traversing the known route tree (see
|
||||
// dispatchRetryDueToTreeMismatch).
|
||||
routeCacheEntry) {
|
||||
// A version of navigate() that accepts the target route tree as an argument
|
||||
// rather than reading it from the prefetch cache.
|
||||
const accumulation = {
|
||||
separateRefreshUrls: null,
|
||||
scrollRef: null
|
||||
};
|
||||
// We special case navigations to the exact same URL as the current location.
|
||||
// It's a common UI pattern for apps to refresh when you click a link to the
|
||||
// current page. So when this happens, we refresh the dynamic data in the page
|
||||
// segments.
|
||||
//
|
||||
// Note that this does not apply if the any part of the hash or search query
|
||||
// has changed. This might feel a bit weird but it makes more sense when you
|
||||
// consider that the way to trigger this behavior is to click the same link
|
||||
// multiple times.
|
||||
//
|
||||
// TODO: We should probably refresh the *entire* route when this case occurs,
|
||||
// not just the page segments. Essentially treating it the same as a refresh()
|
||||
// triggered by an action, which is the more explicit way of modeling the UI
|
||||
// pattern described above.
|
||||
//
|
||||
// Also note that this only refreshes the dynamic data, not static/ cached
|
||||
// data. If the page segment is fully static and prefetched, the request is
|
||||
// skipped. (This is also how refresh() works.)
|
||||
const isSamePageNavigation = url.href === currentUrl.href;
|
||||
const task = (0, _pprnavigations.startPPRNavigation)(now, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, navigationSeed.routeTree, navigationSeed.metadataVaryPath, freshnessPolicy, navigationSeed.data, navigationSeed.head, navigationSeed.dynamicStaleAt, isSamePageNavigation, accumulation);
|
||||
if (task !== null) {
|
||||
if (freshnessPolicy !== _pprnavigations.FreshnessPolicy.Gesture) {
|
||||
(0, _pprnavigations.spawnDynamicRequests)(task, url, nextUrl, freshnessPolicy, accumulation, routeCacheEntry, navigateType);
|
||||
}
|
||||
return completeSoftNavigation(state, url, nextUrl, task.route, task.node, navigationSeed.renderedSearch, canonicalUrl, navigateType, scrollBehavior, accumulation.scrollRef, debugInfo);
|
||||
}
|
||||
// Could not perform a SPA navigation. Revert to a full-page (MPA) navigation.
|
||||
return completeHardNavigation(state, url, navigateType);
|
||||
}
|
||||
function navigateUsingPrefetchedRouteTree(now, state, url, currentUrl, currentRenderedSearch, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, scrollBehavior, navigateType, route) {
|
||||
const routeTree = route.tree;
|
||||
const canonicalUrl = route.canonicalUrl + url.hash;
|
||||
const renderedSearch = route.renderedSearch;
|
||||
const prefetchSeed = {
|
||||
renderedSearch,
|
||||
routeTree,
|
||||
metadataVaryPath: route.metadata.varyPath,
|
||||
data: null,
|
||||
head: null,
|
||||
dynamicStaleAt: (0, _bfcache.computeDynamicStaleAt)(now, _bfcache.UnknownDynamicStaleTime)
|
||||
};
|
||||
return navigateToKnownRoute(now, state, url, canonicalUrl, prefetchSeed, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, scrollBehavior, navigateType, null, route);
|
||||
}
|
||||
// Used to request all the dynamic data for a route, rather than just a subset,
|
||||
// e.g. during a refresh or a revalidation. Typically this gets constructed
|
||||
// during the normal flow when diffing the route tree, but for an unprefetched
|
||||
// navigation, where we don't know the structure of the target route, we use
|
||||
// this instead.
|
||||
const DynamicRequestTreeForEntireRoute = [
|
||||
'',
|
||||
{},
|
||||
null,
|
||||
'refetch'
|
||||
];
|
||||
async function navigateToUnknownRoute(now, state, url, currentUrl, currentRenderedSearch, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, scrollBehavior, navigateType) {
|
||||
// Runs when a navigation happens but there's no cached prefetch we can use.
|
||||
// Don't bother to wait for a prefetch response; go straight to a full
|
||||
// navigation that contains both static and dynamic data in a single stream.
|
||||
// (This is unlike the old navigation implementation, which instead blocks
|
||||
// the dynamic request until a prefetch request is received.)
|
||||
//
|
||||
// To avoid duplication of logic, we're going to pretend that the tree
|
||||
// returned by the dynamic request is, in fact, a prefetch tree. Then we can
|
||||
// use the same server response to write the actual data into the CacheNode
|
||||
// tree. So it's the same flow as the "happy path" (prefetch, then
|
||||
// navigation), except we use a single server response for both stages.
|
||||
let dynamicRequestTree;
|
||||
switch(freshnessPolicy){
|
||||
case _pprnavigations.FreshnessPolicy.Default:
|
||||
case _pprnavigations.FreshnessPolicy.HistoryTraversal:
|
||||
case _pprnavigations.FreshnessPolicy.Gesture:
|
||||
dynamicRequestTree = currentFlightRouterState;
|
||||
break;
|
||||
case _pprnavigations.FreshnessPolicy.Hydration:
|
||||
case _pprnavigations.FreshnessPolicy.RefreshAll:
|
||||
case _pprnavigations.FreshnessPolicy.HMRRefresh:
|
||||
dynamicRequestTree = DynamicRequestTreeForEntireRoute;
|
||||
break;
|
||||
default:
|
||||
freshnessPolicy;
|
||||
dynamicRequestTree = currentFlightRouterState;
|
||||
break;
|
||||
}
|
||||
const promiseForDynamicServerResponse = (0, _fetchserverresponse.fetchServerResponse)(url, {
|
||||
flightRouterState: dynamicRequestTree,
|
||||
nextUrl
|
||||
});
|
||||
const result = await promiseForDynamicServerResponse;
|
||||
if (typeof result === 'string') {
|
||||
// This is an MPA navigation.
|
||||
const redirectUrl = new URL(result, location.origin);
|
||||
return completeHardNavigation(state, redirectUrl, navigateType);
|
||||
}
|
||||
const { flightData, canonicalUrl, renderedSearch, couldBeIntercepted, supportsPerSegmentPrefetching, dynamicStaleTime, staticStageData, runtimePrefetchStream, responseHeaders, debugInfo } = result;
|
||||
// Since the response format of dynamic requests and prefetches is slightly
|
||||
// different, we'll need to massage the data a bit. Create FlightRouterState
|
||||
// tree that simulates what we'd receive as the result of a prefetch.
|
||||
const navigationSeed = convertServerPatchToFullTree(now, currentFlightRouterState, flightData, renderedSearch, dynamicStaleTime);
|
||||
// Learn the route pattern so we can predict it for future navigations.
|
||||
// hasDynamicRewrite is false because this is a fresh navigation to an
|
||||
// unknown route - any rewrite detection happens during the traversal inside
|
||||
// discoverKnownRoute. The hasDynamicRewrite param is only set to true when
|
||||
// retrying after a tree mismatch (see dispatchRetryDueToTreeMismatch).
|
||||
const metadataVaryPath = navigationSeed.metadataVaryPath;
|
||||
if (metadataVaryPath !== null) {
|
||||
(0, _optimisticroutes.discoverKnownRoute)(now, url.pathname, nextUrl, null, navigationSeed.routeTree, metadataVaryPath, couldBeIntercepted, (0, _createhreffromurl.createHrefFromUrl)(canonicalUrl), supportsPerSegmentPrefetching, false // hasDynamicRewrite - not a retry, rewrite detection happens during traversal
|
||||
);
|
||||
if (staticStageData !== null) {
|
||||
const { response: staticStageResponse, isResponsePartial } = staticStageData;
|
||||
// Write the static stage of the response into the segment cache so that
|
||||
// subsequent navigations can serve cached static segments instantly.
|
||||
(0, _cache.getStaleAt)(now, staticStageResponse.s).then((staleAt)=>{
|
||||
const buildId = responseHeaders.get(_constants.NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? staticStageResponse.b;
|
||||
(0, _cache.writeStaticStageResponseIntoCache)(now, staticStageResponse.f, buildId, staticStageResponse.h, staleAt, currentFlightRouterState, renderedSearch, isResponsePartial);
|
||||
}).catch(()=>{
|
||||
// The static stage processing failed. Not fatal — the navigation
|
||||
// completed normally, we just won't write into the cache.
|
||||
});
|
||||
}
|
||||
if (runtimePrefetchStream !== null) {
|
||||
(0, _cache.processRuntimePrefetchStream)(now, runtimePrefetchStream, currentFlightRouterState, 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.
|
||||
});
|
||||
}
|
||||
}
|
||||
return navigateToKnownRoute(now, state, url, (0, _createhreffromurl.createHrefFromUrl)(canonicalUrl), navigationSeed, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, scrollBehavior, navigateType, debugInfo, // Unknown route navigations don't use route prediction - the route tree
|
||||
// came directly from the server. If a mismatch occurs during dynamic data
|
||||
// fetch, the retry handler will traverse the known route tree to mark the
|
||||
// entry as having a dynamic rewrite.
|
||||
null);
|
||||
}
|
||||
function completeHardNavigation(state, url, navigateType) {
|
||||
if ((0, _javascripturl.isJavaScriptURLString)(url.href)) {
|
||||
console.error('Next.js has blocked a javascript: URL as a security precaution.');
|
||||
return state;
|
||||
}
|
||||
const newState = {
|
||||
canonicalUrl: url.origin === location.origin ? (0, _createhreffromurl.createHrefFromUrl)(url) : url.href,
|
||||
pushRef: {
|
||||
pendingPush: navigateType === 'push',
|
||||
mpaNavigation: true,
|
||||
preserveCustomHistoryState: false
|
||||
},
|
||||
// TODO: None of the rest of these values are consistent with the incoming
|
||||
// navigation. We rely on the fact that AppRouter will suspend and trigger
|
||||
// a hard navigation before it accesses any of these values. But instead
|
||||
// we should trigger the hard navigation and blocking any subsequent
|
||||
// router updates without updating React.
|
||||
renderedSearch: state.renderedSearch,
|
||||
focusAndScrollRef: state.focusAndScrollRef,
|
||||
cache: state.cache,
|
||||
tree: state.tree,
|
||||
nextUrl: state.nextUrl,
|
||||
previousNextUrl: state.previousNextUrl,
|
||||
debugInfo: null
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
function completeSoftNavigation(oldState, url, referringNextUrl, tree, cache, renderedSearch, canonicalUrl, navigateType, scrollBehavior, scrollRef, collectedDebugInfo) {
|
||||
// The "Next-Url" is a special representation of the URL that Next.js
|
||||
// uses to implement interception routes.
|
||||
// TODO: Get rid of this extra traversal by computing this during the
|
||||
// same traversal that computes the tree itself. We should also figure out
|
||||
// what is the minimum information needed for the server to correctly
|
||||
// intercept the route.
|
||||
const changedPath = (0, _computechangedpath.computeChangedPath)(oldState.tree, tree);
|
||||
const nextUrlForNewRoute = changedPath ? changedPath : oldState.nextUrl;
|
||||
// This value is stored on the state as `previousNextUrl`; the naming is
|
||||
// confusing. What it represents is the "Next-Url" header that was used to
|
||||
// fetch the incoming route. It's essentially the refererer URL, but in a
|
||||
// Next.js specific format. During refreshes, this is sent back to the server
|
||||
// instead of the current route's "Next-Url" so that the same interception
|
||||
// logic is applied as during the original navigation.
|
||||
const previousNextUrl = referringNextUrl;
|
||||
// Check if the only thing that changed was the hash fragment.
|
||||
const oldUrl = new URL(oldState.canonicalUrl, url);
|
||||
const onlyHashChange = // We don't need to compare the origins, because client-driven
|
||||
// navigations are always same-origin.
|
||||
url.pathname === oldUrl.pathname && url.search === oldUrl.search && url.hash !== oldUrl.hash;
|
||||
// Determine whether and how the page should scroll after this
|
||||
// navigation.
|
||||
//
|
||||
// By default, we scroll to the segments that were navigated to — i.e.
|
||||
// segments in the new part of the route, as opposed to shared segments
|
||||
// that were already part of the previous route. All newly navigated
|
||||
// segments share a single ScrollRef. When they mount, the first one
|
||||
// to mount initiates the scroll. They share a ref so that only one
|
||||
// scroll happens per navigation.
|
||||
//
|
||||
// If a subsequent navigation produces new segments, those supersede
|
||||
// any pending scroll from the previous navigation by invalidating its
|
||||
// ScrollRef. If a navigation doesn't produce any new segments (e.g.
|
||||
// a refresh where the route structure didn't change), any pending
|
||||
// scrolls from previous navigations are unaffected.
|
||||
//
|
||||
// The branches below handle special cases layered on top of this
|
||||
// default model.
|
||||
let activeScrollRef;
|
||||
let forceScroll;
|
||||
if (scrollBehavior === _routerreducertypes.ScrollBehavior.NoScroll) {
|
||||
// The user explicitly opted out of scrolling (e.g. scroll={false}
|
||||
// on a Link or router.push).
|
||||
//
|
||||
// If this navigation created new scroll targets (scrollRef !== null),
|
||||
// neutralize them. If it didn't, any prior scroll targets carried
|
||||
// forward on the cache nodes via reuseSharedCacheNode remain active.
|
||||
if (scrollRef !== null) {
|
||||
scrollRef.current = false;
|
||||
}
|
||||
activeScrollRef = oldState.focusAndScrollRef.scrollRef;
|
||||
forceScroll = false;
|
||||
} else if (onlyHashChange) {
|
||||
// Hash-only navigations should scroll regardless of per-node state.
|
||||
// Create a fresh ref so the first segment to scroll consumes it.
|
||||
//
|
||||
// Invalidate any scroll ref from a prior navigation that hasn't
|
||||
// been consumed yet.
|
||||
const oldScrollRef = oldState.focusAndScrollRef.scrollRef;
|
||||
if (oldScrollRef !== null) {
|
||||
oldScrollRef.current = false;
|
||||
}
|
||||
// Also invalidate any per-node refs that were accumulated during
|
||||
// this navigation's tree construction — the hash-only ref
|
||||
// supersedes them.
|
||||
if (scrollRef !== null) {
|
||||
scrollRef.current = false;
|
||||
}
|
||||
activeScrollRef = {
|
||||
current: true
|
||||
};
|
||||
forceScroll = true;
|
||||
} else {
|
||||
// Default case. Use the accumulated scrollRef (may be null if no
|
||||
// new segments were created). The handler checks per-node refs, so
|
||||
// unchanged parallel route slots won't scroll.
|
||||
activeScrollRef = scrollRef;
|
||||
// If this navigation created new scroll targets, invalidate any
|
||||
// pending scroll from a previous navigation.
|
||||
if (scrollRef !== null) {
|
||||
const oldScrollRef = oldState.focusAndScrollRef.scrollRef;
|
||||
if (oldScrollRef !== null) {
|
||||
oldScrollRef.current = false;
|
||||
}
|
||||
}
|
||||
forceScroll = false;
|
||||
}
|
||||
const newState = {
|
||||
canonicalUrl,
|
||||
renderedSearch,
|
||||
pushRef: {
|
||||
pendingPush: navigateType === 'push',
|
||||
mpaNavigation: false,
|
||||
preserveCustomHistoryState: false
|
||||
},
|
||||
focusAndScrollRef: {
|
||||
scrollRef: activeScrollRef,
|
||||
forceScroll,
|
||||
onlyHashChange,
|
||||
hashFragment: // Remove leading # and decode hash to make non-latin hashes work.
|
||||
//
|
||||
// Empty hash should trigger default behavior of scrolling layout into
|
||||
// view. #top is handled in layout-router.
|
||||
//
|
||||
// Refer to `ScrollAndFocusHandler` for details on how this is used.
|
||||
scrollBehavior !== _routerreducertypes.ScrollBehavior.NoScroll && url.hash !== '' ? decodeURIComponent(url.hash.slice(1)) : oldState.focusAndScrollRef.hashFragment
|
||||
},
|
||||
cache,
|
||||
tree,
|
||||
nextUrl: nextUrlForNewRoute,
|
||||
previousNextUrl,
|
||||
debugInfo: collectedDebugInfo
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
function completeTraverseNavigation(state, url, renderedSearch, cache, tree, nextUrl) {
|
||||
return {
|
||||
// Set canonical url
|
||||
canonicalUrl: (0, _createhreffromurl.createHrefFromUrl)(url),
|
||||
renderedSearch,
|
||||
pushRef: {
|
||||
pendingPush: false,
|
||||
mpaNavigation: false,
|
||||
// Ensures that the custom history state that was set is preserved when applying this update.
|
||||
preserveCustomHistoryState: true
|
||||
},
|
||||
focusAndScrollRef: state.focusAndScrollRef,
|
||||
cache,
|
||||
// Restore provided tree
|
||||
tree,
|
||||
nextUrl,
|
||||
// TODO: We need to restore previousNextUrl, too, which represents the
|
||||
// Next-Url that was used to fetch the data. Anywhere we fetch using the
|
||||
// canonical URL, there should be a corresponding Next-Url.
|
||||
previousNextUrl: null,
|
||||
debugInfo: null
|
||||
};
|
||||
}
|
||||
function convertServerPatchToFullTree(now, currentTree, flightData, renderedSearch, dynamicStaleTimeSeconds) {
|
||||
// During a client navigation or prefetch, the server sends back only a patch
|
||||
// for the parts of the tree that have changed.
|
||||
//
|
||||
// This applies the patch to the base tree to create a full representation of
|
||||
// the resulting tree.
|
||||
//
|
||||
// The return type includes a full FlightRouterState tree and a full
|
||||
// CacheNodeSeedData tree. (Conceptually these are the same tree, and should
|
||||
// eventually be unified, but there's still lots of existing code that
|
||||
// operates on FlightRouterState trees alone without the CacheNodeSeedData.)
|
||||
//
|
||||
// TODO: This similar to what apply-router-state-patch-to-tree does. It
|
||||
// will eventually fully replace it. We should get rid of all the remaining
|
||||
// places where we iterate over the server patch format. This should also
|
||||
// eventually replace normalizeFlightData.
|
||||
let baseTree = currentTree;
|
||||
let baseData = null;
|
||||
let head = null;
|
||||
if (flightData !== null) {
|
||||
for (const { segmentPath, tree: treePatch, seedData: dataPatch, head: headPatch } of flightData){
|
||||
const result = convertServerPatchToFullTreeImpl(baseTree, baseData, treePatch, dataPatch, segmentPath, renderedSearch, 0);
|
||||
baseTree = result.tree;
|
||||
baseData = result.data;
|
||||
// This is the same for all patches per response, so just pick an
|
||||
// arbitrary one
|
||||
head = headPatch;
|
||||
}
|
||||
}
|
||||
const finalFlightRouterState = baseTree;
|
||||
// Convert the final FlightRouterState into a RouteTree type.
|
||||
//
|
||||
// TODO: Eventually, FlightRouterState will evolve to being a transport format
|
||||
// only. The RouteTree type will become the main type used for dealing with
|
||||
// routes on the client, and we'll store it in the state directly.
|
||||
const acc = {
|
||||
metadataVaryPath: null
|
||||
};
|
||||
const routeTree = (0, _cache.convertRootFlightRouterStateToRouteTree)(finalFlightRouterState, renderedSearch, acc);
|
||||
return {
|
||||
routeTree,
|
||||
metadataVaryPath: acc.metadataVaryPath,
|
||||
data: baseData,
|
||||
renderedSearch,
|
||||
head,
|
||||
dynamicStaleAt: (0, _bfcache.computeDynamicStaleAt)(now, dynamicStaleTimeSeconds)
|
||||
};
|
||||
}
|
||||
function convertServerPatchToFullTreeImpl(baseRouterState, baseData, treePatch, dataPatch, segmentPath, renderedSearch, index) {
|
||||
if (index === segmentPath.length) {
|
||||
// We reached the part of the tree that we need to patch.
|
||||
return {
|
||||
tree: treePatch,
|
||||
data: dataPatch
|
||||
};
|
||||
}
|
||||
// segmentPath represents the parent path of subtree. It's a repeating
|
||||
// pattern of parallel route key and segment:
|
||||
//
|
||||
// [string, Segment, string, Segment, string, Segment, ...]
|
||||
//
|
||||
// This path tells us which part of the base tree to apply the tree patch.
|
||||
//
|
||||
// NOTE: We receive the FlightRouterState patch in the same request as the
|
||||
// seed data patch. Therefore we don't need to worry about diffing the segment
|
||||
// values; we can assume the server sent us a correct result.
|
||||
const updatedParallelRouteKey = segmentPath[index];
|
||||
// const segment: Segment = segmentPath[index + 1] <-- Not used, see note above
|
||||
const baseTreeChildren = baseRouterState[1];
|
||||
const baseSeedDataChildren = baseData !== null ? baseData[1] : null;
|
||||
const newTreeChildren = {};
|
||||
const newSeedDataChildren = {};
|
||||
for(const parallelRouteKey in baseTreeChildren){
|
||||
const childBaseRouterState = baseTreeChildren[parallelRouteKey];
|
||||
const childBaseSeedData = baseSeedDataChildren !== null ? baseSeedDataChildren[parallelRouteKey] ?? null : null;
|
||||
if (parallelRouteKey === updatedParallelRouteKey) {
|
||||
const result = convertServerPatchToFullTreeImpl(childBaseRouterState, childBaseSeedData, treePatch, dataPatch, segmentPath, renderedSearch, // Advance the index by two and keep cloning until we reach
|
||||
// the end of the segment path.
|
||||
index + 2);
|
||||
newTreeChildren[parallelRouteKey] = result.tree;
|
||||
newSeedDataChildren[parallelRouteKey] = result.data;
|
||||
} else {
|
||||
// This child is not being patched. Copy it over as-is.
|
||||
newTreeChildren[parallelRouteKey] = childBaseRouterState;
|
||||
newSeedDataChildren[parallelRouteKey] = childBaseSeedData;
|
||||
}
|
||||
}
|
||||
let clonedTree;
|
||||
let clonedSeedData;
|
||||
// Clone all the fields except the children.
|
||||
// Clone the FlightRouterState tree. 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.
|
||||
clonedTree = [
|
||||
baseRouterState[0],
|
||||
newTreeChildren
|
||||
];
|
||||
if (2 in baseRouterState) {
|
||||
const compressedRefreshState = baseRouterState[2];
|
||||
if (compressedRefreshState !== undefined && compressedRefreshState !== null) {
|
||||
// Since this part of the tree was patched with new data, any parent
|
||||
// refresh states should be updated to reflect the new rendered search
|
||||
// value. (The refresh state acts like a "context provider".) All pages
|
||||
// within the same server response share the same renderedSearch value,
|
||||
// but the same RouteTree could be composed from multiple different
|
||||
// routes, and multiple responses.
|
||||
clonedTree[2] = [
|
||||
compressedRefreshState[0],
|
||||
renderedSearch
|
||||
];
|
||||
}
|
||||
}
|
||||
if (3 in baseRouterState) {
|
||||
clonedTree[3] = baseRouterState[3];
|
||||
}
|
||||
if (4 in baseRouterState) {
|
||||
clonedTree[4] = baseRouterState[4];
|
||||
}
|
||||
// Clone the CacheNodeSeedData tree.
|
||||
const isEmptySeedDataPartial = true;
|
||||
clonedSeedData = [
|
||||
null,
|
||||
newSeedDataChildren,
|
||||
null,
|
||||
isEmptySeedDataPartial,
|
||||
null
|
||||
];
|
||||
return {
|
||||
tree: clonedTree,
|
||||
data: clonedSeedData
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Instant Navigation Testing API: ensures a prefetch task has been initiated
|
||||
* and completed before proceeding with the navigation. This guarantees that
|
||||
* segment data requests are at least pending, even for routes whose route
|
||||
* tree is already cached.
|
||||
*
|
||||
* After the prefetch completes, delegates to the normal navigation flow.
|
||||
*/ async function ensurePrefetchThenNavigate(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType) {
|
||||
const link = (0, _links.getLinkForCurrentNavigation)();
|
||||
const fetchStrategy = link !== null ? link.fetchStrategy : _types.FetchStrategy.PPR;
|
||||
// Transition the cookie to captured-SPA immediately, before waiting
|
||||
// for the prefetch. This ensures the devtools panel can update its UI
|
||||
// right away, even if the prefetch takes time (e.g. dev compilation).
|
||||
// The "to" tree starts as null and is filled in after the prefetch
|
||||
// resolves and the navigation produces a new router state.
|
||||
const { transitionToCapturedSPA, updateCapturedSPAToTree } = require('./navigation-testing-lock');
|
||||
transitionToCapturedSPA(currentFlightRouterState, null);
|
||||
const cacheKey = (0, _cachekey.createCacheKey)(url.href, nextUrl);
|
||||
await new Promise((resolve)=>{
|
||||
(0, _scheduler.schedulePrefetchTask)(cacheKey, currentFlightRouterState, fetchStrategy, _types.PrefetchPriority.Default, null, resolve // _onComplete callback
|
||||
);
|
||||
});
|
||||
// Prefetch is complete. Proceed with the normal navigation flow, which
|
||||
// will now find the route in the cache.
|
||||
const result = await navigateImpl(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType);
|
||||
// Update the cookie with the resolved "to" tree so the devtools
|
||||
// panel can display both routes immediately.
|
||||
updateCapturedSPAToTree(currentFlightRouterState, result.tree);
|
||||
return result;
|
||||
}
|
||||
|
||||
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=navigation.js.map
|
||||
Reference in New Issue
Block a user