next
Version:
The React Framework
380 lines (378 loc) • 20.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
handleExternalUrl: null,
navigateReducer: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
handleExternalUrl: function() {
return handleExternalUrl;
},
navigateReducer: function() {
return navigateReducer;
}
});
const _fetchserverresponse = require("../fetch-server-response");
const _createhreffromurl = require("../create-href-from-url");
const _invalidatecachebelowflightsegmentpath = require("../invalidate-cache-below-flight-segmentpath");
const _applyrouterstatepatchtotree = require("../apply-router-state-patch-to-tree");
const _shouldhardnavigate = require("../should-hard-navigate");
const _isnavigatingtonewrootlayout = require("../is-navigating-to-new-root-layout");
const _routerreducertypes = require("../router-reducer-types");
const _handlemutable = require("../handle-mutable");
const _applyflightdata = require("../apply-flight-data");
const _prefetchreducer = require("./prefetch-reducer");
const _approuter = require("../../app-router");
const _segment = require("../../../../shared/lib/segment");
const _pprnavigations = require("../ppr-navigations");
const _prefetchcacheutils = require("../prefetch-cache-utils");
const _clearcachenodedataforsegmentpath = require("../clear-cache-node-data-for-segment-path");
const _aliasedprefetchnavigations = require("../aliased-prefetch-navigations");
const _segmentcache = require("../../segment-cache");
function handleExternalUrl(state, mutable, url, pendingPush) {
mutable.mpaNavigation = true;
mutable.canonicalUrl = url;
mutable.pendingPush = pendingPush;
mutable.scrollableSegments = undefined;
return (0, _handlemutable.handleMutable)(state, mutable);
}
function generateSegmentsFromPatch(flightRouterPatch) {
const segments = [];
const [segment, parallelRoutes] = flightRouterPatch;
if (Object.keys(parallelRoutes).length === 0) {
return [
[
segment
]
];
}
for (const [parallelRouteKey, parallelRoute] of Object.entries(parallelRoutes)){
for (const childSegment of generateSegmentsFromPatch(parallelRoute)){
// If the segment is empty, it means we are at the root of the tree
if (segment === '') {
segments.push([
parallelRouteKey,
...childSegment
]);
} else {
segments.push([
segment,
parallelRouteKey,
...childSegment
]);
}
}
}
return segments;
}
function triggerLazyFetchForLeafSegments(newCache, currentCache, flightSegmentPath, treePatch) {
let appliedPatch = false;
newCache.rsc = currentCache.rsc;
newCache.prefetchRsc = currentCache.prefetchRsc;
newCache.loading = currentCache.loading;
newCache.parallelRoutes = new Map(currentCache.parallelRoutes);
const segmentPathsToFill = generateSegmentsFromPatch(treePatch).map((segment)=>[
...flightSegmentPath,
...segment
]);
for (const segmentPaths of segmentPathsToFill){
(0, _clearcachenodedataforsegmentpath.clearCacheNodeDataForSegmentPath)(newCache, currentCache, segmentPaths);
appliedPatch = true;
}
return appliedPatch;
}
function handleNavigationResult(url, state, mutable, pendingPush, result) {
switch(result.tag){
case _segmentcache.NavigationResultTag.MPA:
{
// Perform an MPA navigation.
const newUrl = result.data;
return handleExternalUrl(state, mutable, newUrl, pendingPush);
}
case _segmentcache.NavigationResultTag.NoOp:
{
// The server responded with no change to the current page. However, if
// the URL changed, we still need to update that.
const newCanonicalUrl = result.data.canonicalUrl;
mutable.canonicalUrl = newCanonicalUrl;
// Check if the only thing that changed was the hash fragment.
const oldUrl = new URL(state.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;
if (onlyHashChange) {
// The only updated part of the URL is the hash.
mutable.onlyHashChange = true;
mutable.shouldScroll = result.data.shouldScroll;
mutable.hashFragment = url.hash;
// Setting this to an empty array triggers a scroll for all new and
// updated segments. See `ScrollAndFocusHandler` for more details.
mutable.scrollableSegments = [];
}
return (0, _handlemutable.handleMutable)(state, mutable);
}
case _segmentcache.NavigationResultTag.Success:
{
// Received a new result.
mutable.cache = result.data.cacheNode;
mutable.patchedTree = result.data.flightRouterState;
mutable.canonicalUrl = result.data.canonicalUrl;
mutable.scrollableSegments = result.data.scrollableSegments;
mutable.shouldScroll = result.data.shouldScroll;
mutable.hashFragment = result.data.hash;
return (0, _handlemutable.handleMutable)(state, mutable);
}
case _segmentcache.NavigationResultTag.Async:
{
return result.data.then((asyncResult)=>handleNavigationResult(url, state, mutable, pendingPush, asyncResult), // If the navigation failed, return the current state.
// TODO: This matches the current behavior but we need to do something
// better here if the network fails.
()=>{
return state;
});
}
default:
{
result;
return state;
}
}
}
function navigateReducer(state, action) {
const { url, isExternalUrl, navigateType, shouldScroll, allowAliasing } = action;
const mutable = {};
const { hash } = url;
const href = (0, _createhreffromurl.createHrefFromUrl)(url);
const pendingPush = navigateType === 'push';
// we want to prune the prefetch cache on every navigation to avoid it growing too large
(0, _prefetchcacheutils.prunePrefetchCache)(state.prefetchCache);
mutable.preserveCustomHistoryState = false;
mutable.pendingPush = pendingPush;
if (isExternalUrl) {
return handleExternalUrl(state, mutable, url.toString(), pendingPush);
}
// Handles case where `<meta http-equiv="refresh">` tag is present,
// which will trigger an MPA navigation.
if (document.getElementById('__next-page-redirect')) {
return handleExternalUrl(state, mutable, href, pendingPush);
}
if (process.env.__NEXT_CLIENT_SEGMENT_CACHE) {
// (Very Early Experimental Feature) Segment Cache
//
// Bypass the normal prefetch cache and use the new per-segment cache
// implementation instead. This is only supported if PPR is enabled, too.
//
// Temporary glue code between the router reducer and the new navigation
// implementation. Eventually we'll rewrite the router reducer to a
// state machine.
const result = (0, _segmentcache.navigate)(url, state.cache, state.tree, state.nextUrl, shouldScroll);
return handleNavigationResult(url, state, mutable, pendingPush, result);
}
const prefetchValues = (0, _prefetchcacheutils.getOrCreatePrefetchCacheEntry)({
url,
nextUrl: state.nextUrl,
tree: state.tree,
prefetchCache: state.prefetchCache,
allowAliasing
});
const { treeAtTimeOfPrefetch, data } = prefetchValues;
_prefetchreducer.prefetchQueue.bump(data);
return data.then((param)=>{
let { flightData, canonicalUrl: canonicalUrlOverride, postponed } = param;
const navigatedAt = Date.now();
let isFirstRead = false;
// we only want to mark this once
if (!prefetchValues.lastUsedTime) {
// important: we should only mark the cache node as dirty after we unsuspend from the call above
prefetchValues.lastUsedTime = navigatedAt;
isFirstRead = true;
}
if (prefetchValues.aliased) {
const result = (0, _aliasedprefetchnavigations.handleAliasedPrefetchEntry)(navigatedAt, state, flightData, url, mutable);
// We didn't return new router state because we didn't apply the aliased entry for some reason.
// We'll re-invoke the navigation handler but ensure that we don't attempt to use the aliased entry. This
// will create an on-demand prefetch entry.
if (result === false) {
return navigateReducer(state, {
...action,
allowAliasing: false
});
}
return result;
}
// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
return handleExternalUrl(state, mutable, flightData, pendingPush);
}
const updatedCanonicalUrl = canonicalUrlOverride ? (0, _createhreffromurl.createHrefFromUrl)(canonicalUrlOverride) : href;
const onlyHashChange = !!hash && state.canonicalUrl.split('#', 1)[0] === updatedCanonicalUrl.split('#', 1)[0];
// If only the hash has changed, the server hasn't sent us any new data. We can just update
// the mutable properties responsible for URL and scroll handling and return early.
if (onlyHashChange) {
mutable.onlyHashChange = true;
mutable.canonicalUrl = updatedCanonicalUrl;
mutable.shouldScroll = shouldScroll;
mutable.hashFragment = hash;
mutable.scrollableSegments = [];
return (0, _handlemutable.handleMutable)(state, mutable);
}
let currentTree = state.tree;
let currentCache = state.cache;
let scrollableSegments = [];
for (const normalizedFlightData of flightData){
const { pathToSegment: flightSegmentPath, seedData, head, isHeadPartial, isRootRender } = normalizedFlightData;
let treePatch = normalizedFlightData.tree;
// TODO-APP: remove ''
const flightSegmentPathWithLeadingEmpty = [
'',
...flightSegmentPath
];
// Create new tree based on the flightSegmentPath and router state patch
let newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
flightSegmentPathWithLeadingEmpty, currentTree, treePatch, href);
// If the tree patch can't be applied to the current tree then we use the tree at time of prefetch
// TODO-APP: This should instead fill in the missing pieces in `currentTree` with the data from `treeAtTimeOfPrefetch`, then apply the patch.
if (newTree === null) {
newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
flightSegmentPathWithLeadingEmpty, treeAtTimeOfPrefetch, treePatch, href);
}
if (newTree !== null) {
if (// This is just a paranoid check. When a route is PPRed, the server
// will send back a static response that's rendered from
// the root. If for some reason it doesn't, we fall back to the
// non-PPR implementation.
// TODO: We should get rid of the else branch and do all navigations
// via startPPRNavigation. The current structure is just
// an incremental step.
seedData && isRootRender && postponed) {
const task = (0, _pprnavigations.startPPRNavigation)(navigatedAt, currentCache, currentTree, treePatch, seedData, head, isHeadPartial, false, scrollableSegments);
if (task !== null) {
if (task.route === null) {
// Detected a change to the root layout. Perform an full-
// page navigation.
return handleExternalUrl(state, mutable, href, pendingPush);
}
// Use the tree computed by startPPRNavigation instead
// of the one computed by applyRouterStatePatchToTree.
// TODO: We should remove applyRouterStatePatchToTree
// from the PPR path entirely.
const patchedRouterState = task.route;
newTree = patchedRouterState;
const newCache = task.node;
if (newCache !== null) {
// We've created a new Cache Node tree that contains a prefetched
// version of the next page. This can be rendered instantly.
mutable.cache = newCache;
}
const dynamicRequestTree = task.dynamicRequestTree;
if (dynamicRequestTree !== null) {
// The prefetched tree has dynamic holes in it. We initiate a
// dynamic request to fill them in.
//
// Do not block on the result. We'll immediately render the Cache
// Node tree and suspend on the dynamic parts. When the request
// comes in, we'll fill in missing data and ping React to
// re-render. Unlike the lazy fetching model in the non-PPR
// implementation, this is modeled as a single React update +
// streaming, rather than multiple top-level updates. (However,
// even in the new model, we'll still need to sometimes update the
// root multiple times per navigation, like if the server sends us
// a different response than we expected. For now, we revert back
// to the lazy fetching mechanism in that case.)
const dynamicRequest = (0, _fetchserverresponse.fetchServerResponse)(url, {
flightRouterState: dynamicRequestTree,
nextUrl: state.nextUrl
});
(0, _pprnavigations.listenForDynamicRequest)(task, dynamicRequest);
// We store the dynamic request on the `lazyData` property of the CacheNode
// because we're not going to await the dynamic request here. Since we're not blocking
// on the dynamic request, `layout-router` will
// task.node.lazyData = dynamicRequest
} else {
// The prefetched tree does not contain dynamic holes — it's
// fully static. We can skip the dynamic request.
}
} else {
// Nothing changed, so reuse the old cache.
// TODO: What if the head changed but not any of the segment data?
// Is that possible? If so, we should clone the whole tree and
// update the head.
newTree = treePatch;
}
} else {
// The static response does not include any dynamic holes, so
// there's no need to do a second request.
// TODO: As an incremental step this just reverts back to the
// non-PPR implementation. We can simplify this branch further,
// given that PPR prefetches are always static and return the whole
// tree. Or in the meantime we could factor it out into a
// separate function.
if ((0, _isnavigatingtonewrootlayout.isNavigatingToNewRootLayout)(currentTree, newTree)) {
return handleExternalUrl(state, mutable, href, pendingPush);
}
const cache = (0, _approuter.createEmptyCacheNode)();
let applied = false;
if (prefetchValues.status === _routerreducertypes.PrefetchCacheEntryStatus.stale && !isFirstRead) {
// When we have a stale prefetch entry, we only want to re-use the loading state of the route we're navigating to, to support instant loading navigations
// this will trigger a lazy fetch for the actual page data by nulling the `rsc` and `prefetchRsc` values for page data,
// while copying over the `loading` for the segment that contains the page data.
// We only do this on subsequent reads, as otherwise there'd be no loading data to re-use.
// We skip this branch if only the hash fragment has changed, as we don't want to trigger a lazy fetch in that case
applied = triggerLazyFetchForLeafSegments(cache, currentCache, flightSegmentPath, treePatch);
// since we re-used the stale cache's loading state & refreshed the data,
// update the `lastUsedTime` so that it can continue to be re-used for the next 30s
prefetchValues.lastUsedTime = navigatedAt;
} else {
applied = (0, _applyflightdata.applyFlightData)(navigatedAt, currentCache, cache, normalizedFlightData, prefetchValues);
}
const hardNavigate = (0, _shouldhardnavigate.shouldHardNavigate)(// TODO-APP: remove ''
flightSegmentPathWithLeadingEmpty, currentTree);
if (hardNavigate) {
// Copy rsc for the root node of the cache.
cache.rsc = currentCache.rsc;
cache.prefetchRsc = currentCache.prefetchRsc;
(0, _invalidatecachebelowflightsegmentpath.invalidateCacheBelowFlightSegmentPath)(cache, currentCache, flightSegmentPath);
// Ensure the existing cache value is used when the cache was not invalidated.
mutable.cache = cache;
} else if (applied) {
mutable.cache = cache;
// If we applied the cache, we update the "current cache" value so any other
// segments in the FlightDataPath will be able to reference the updated cache.
currentCache = cache;
}
for (const subSegment of generateSegmentsFromPatch(treePatch)){
const scrollableSegmentPath = [
...flightSegmentPath,
...subSegment
];
// Filter out the __DEFAULT__ paths as they shouldn't be scrolled to in this case.
if (scrollableSegmentPath[scrollableSegmentPath.length - 1] !== _segment.DEFAULT_SEGMENT_KEY) {
scrollableSegments.push(scrollableSegmentPath);
}
}
}
currentTree = newTree;
}
}
mutable.patchedTree = currentTree;
mutable.canonicalUrl = updatedCanonicalUrl;
mutable.scrollableSegments = scrollableSegments;
mutable.hashFragment = hash;
mutable.shouldScroll = shouldScroll;
return (0, _handlemutable.handleMutable)(state, mutable);
}, ()=>state);
}
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=navigate-reducer.js.map