380 lines
20 KiB
JavaScript
380 lines
20 KiB
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
|