920 lines
45 KiB
JavaScript
920 lines
45 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.createHrefFromUrl = createHrefFromUrl;
|
|
exports.reducer = exports.ACTION_PREFETCH = exports.ACTION_SERVER_PATCH = exports.ACTION_RESTORE = exports.ACTION_NAVIGATE = exports.ACTION_REFRESH = void 0;
|
|
var _extends = require("@swc/helpers/lib/_extends.js").default;
|
|
var _appRouterContext = require("../../shared/lib/app-router-context");
|
|
var _matchSegments = require("./match-segments");
|
|
var _appRouter = require("./app-router");
|
|
/**
|
|
* Create data fetching record for Promise.
|
|
*/ // TODO-APP: change `any` to type inference.
|
|
function createRecordFromThenable(thenable) {
|
|
thenable.status = 'pending';
|
|
thenable.then((value)=>{
|
|
if (thenable.status === 'pending') {
|
|
thenable.status = 'fulfilled';
|
|
thenable.value = value;
|
|
}
|
|
}, (err)=>{
|
|
if (thenable.status === 'pending') {
|
|
thenable.status = 'rejected';
|
|
thenable.value = err;
|
|
}
|
|
});
|
|
return thenable;
|
|
}
|
|
/**
|
|
* Read record value or throw Promise if it's not resolved yet.
|
|
*/ function readRecordValue(thenable) {
|
|
// @ts-expect-error TODO: fix type
|
|
if (thenable.status === 'fulfilled') {
|
|
// @ts-expect-error TODO: fix type
|
|
return thenable.value;
|
|
} else {
|
|
throw thenable;
|
|
}
|
|
}
|
|
function createHrefFromUrl(url) {
|
|
return url.pathname + url.search + url.hash;
|
|
}
|
|
/**
|
|
* Invalidate cache one level down from the router state.
|
|
*/ function invalidateCacheByRouterState(newCache, existingCache, routerState) {
|
|
// Remove segment that we got data for so that it is filled in during rendering of subTreeData.
|
|
for(const key in routerState[1]){
|
|
const segmentForParallelRoute = routerState[1][key][0];
|
|
const cacheKey = Array.isArray(segmentForParallelRoute) ? segmentForParallelRoute[1] : segmentForParallelRoute;
|
|
const existingParallelRoutesCacheNode = existingCache.parallelRoutes.get(key);
|
|
if (existingParallelRoutesCacheNode) {
|
|
let parallelRouteCacheNode = new Map(existingParallelRoutesCacheNode);
|
|
parallelRouteCacheNode.delete(cacheKey);
|
|
newCache.parallelRoutes.set(key, parallelRouteCacheNode);
|
|
}
|
|
}
|
|
}
|
|
function fillLazyItemsTillLeafWithHead(newCache, existingCache, routerState, head) {
|
|
const isLastSegment = Object.keys(routerState[1]).length === 0;
|
|
if (isLastSegment) {
|
|
newCache.head = head;
|
|
return;
|
|
}
|
|
// Remove segment that we got data for so that it is filled in during rendering of subTreeData.
|
|
for(const key in routerState[1]){
|
|
const parallelRouteState = routerState[1][key];
|
|
const segmentForParallelRoute = parallelRouteState[0];
|
|
const cacheKey = Array.isArray(segmentForParallelRoute) ? segmentForParallelRoute[1] : segmentForParallelRoute;
|
|
if (existingCache) {
|
|
const existingParallelRoutesCacheNode = existingCache.parallelRoutes.get(key);
|
|
if (existingParallelRoutesCacheNode) {
|
|
let parallelRouteCacheNode = new Map(existingParallelRoutesCacheNode);
|
|
parallelRouteCacheNode.delete(cacheKey);
|
|
const newCacheNode = {
|
|
status: _appRouterContext.CacheStates.LAZY_INITIALIZED,
|
|
data: null,
|
|
subTreeData: null,
|
|
parallelRoutes: new Map()
|
|
};
|
|
parallelRouteCacheNode.set(cacheKey, newCacheNode);
|
|
fillLazyItemsTillLeafWithHead(newCacheNode, undefined, parallelRouteState, head);
|
|
newCache.parallelRoutes.set(key, parallelRouteCacheNode);
|
|
continue;
|
|
}
|
|
}
|
|
const newCacheNode = {
|
|
status: _appRouterContext.CacheStates.LAZY_INITIALIZED,
|
|
data: null,
|
|
subTreeData: null,
|
|
parallelRoutes: new Map()
|
|
};
|
|
newCache.parallelRoutes.set(key, new Map([
|
|
[
|
|
cacheKey,
|
|
newCacheNode
|
|
]
|
|
]));
|
|
fillLazyItemsTillLeafWithHead(newCacheNode, undefined, parallelRouteState, head);
|
|
}
|
|
}
|
|
/**
|
|
* Fill cache with subTreeData based on flightDataPath
|
|
*/ function fillCacheWithNewSubTreeData(newCache, existingCache, flightDataPath) {
|
|
const isLastEntry = flightDataPath.length <= 5;
|
|
const [parallelRouteKey, segment] = flightDataPath;
|
|
const segmentForCache = Array.isArray(segment) ? segment[1] : segment;
|
|
const existingChildSegmentMap = existingCache.parallelRoutes.get(parallelRouteKey);
|
|
if (!existingChildSegmentMap) {
|
|
// Bailout because the existing cache does not have the path to the leaf node
|
|
// Will trigger lazy fetch in layout-router because of missing segment
|
|
return;
|
|
}
|
|
let childSegmentMap = newCache.parallelRoutes.get(parallelRouteKey);
|
|
if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) {
|
|
childSegmentMap = new Map(existingChildSegmentMap);
|
|
newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap);
|
|
}
|
|
const existingChildCacheNode = existingChildSegmentMap.get(segmentForCache);
|
|
let childCacheNode = childSegmentMap.get(segmentForCache);
|
|
if (isLastEntry) {
|
|
if (!childCacheNode || !childCacheNode.data || childCacheNode === existingChildCacheNode) {
|
|
childCacheNode = {
|
|
status: _appRouterContext.CacheStates.READY,
|
|
data: null,
|
|
subTreeData: flightDataPath[3],
|
|
// Ensure segments other than the one we got data for are preserved.
|
|
parallelRoutes: existingChildCacheNode ? new Map(existingChildCacheNode.parallelRoutes) : new Map()
|
|
};
|
|
if (existingChildCacheNode) {
|
|
invalidateCacheByRouterState(childCacheNode, existingChildCacheNode, flightDataPath[2]);
|
|
}
|
|
fillLazyItemsTillLeafWithHead(childCacheNode, existingChildCacheNode, flightDataPath[2], flightDataPath[4]);
|
|
childSegmentMap.set(segmentForCache, childCacheNode);
|
|
}
|
|
return;
|
|
}
|
|
if (!childCacheNode || !existingChildCacheNode) {
|
|
// Bailout because the existing cache does not have the path to the leaf node
|
|
// Will trigger lazy fetch in layout-router because of missing segment
|
|
return;
|
|
}
|
|
if (childCacheNode === existingChildCacheNode) {
|
|
childCacheNode = {
|
|
status: childCacheNode.status,
|
|
data: childCacheNode.data,
|
|
subTreeData: childCacheNode.subTreeData,
|
|
parallelRoutes: new Map(childCacheNode.parallelRoutes)
|
|
};
|
|
childSegmentMap.set(segmentForCache, childCacheNode);
|
|
}
|
|
fillCacheWithNewSubTreeData(childCacheNode, existingChildCacheNode, flightDataPath.slice(2));
|
|
}
|
|
/**
|
|
* Fill cache up to the end of the flightSegmentPath, invalidating anything below it.
|
|
*/ function invalidateCacheBelowFlightSegmentPath(newCache, existingCache, flightSegmentPath) {
|
|
const isLastEntry = flightSegmentPath.length <= 2;
|
|
const [parallelRouteKey, segment] = flightSegmentPath;
|
|
const segmentForCache = Array.isArray(segment) ? segment[1] : segment;
|
|
const existingChildSegmentMap = existingCache.parallelRoutes.get(parallelRouteKey);
|
|
if (!existingChildSegmentMap) {
|
|
// Bailout because the existing cache does not have the path to the leaf node
|
|
// Will trigger lazy fetch in layout-router because of missing segment
|
|
return;
|
|
}
|
|
let childSegmentMap = newCache.parallelRoutes.get(parallelRouteKey);
|
|
if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) {
|
|
childSegmentMap = new Map(existingChildSegmentMap);
|
|
newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap);
|
|
}
|
|
// In case of last entry don't copy further down.
|
|
if (isLastEntry) {
|
|
childSegmentMap.delete(segmentForCache);
|
|
return;
|
|
}
|
|
const existingChildCacheNode = existingChildSegmentMap.get(segmentForCache);
|
|
let childCacheNode = childSegmentMap.get(segmentForCache);
|
|
if (!childCacheNode || !existingChildCacheNode) {
|
|
// Bailout because the existing cache does not have the path to the leaf node
|
|
// Will trigger lazy fetch in layout-router because of missing segment
|
|
return;
|
|
}
|
|
if (childCacheNode === existingChildCacheNode) {
|
|
childCacheNode = {
|
|
status: childCacheNode.status,
|
|
data: childCacheNode.data,
|
|
subTreeData: childCacheNode.subTreeData,
|
|
parallelRoutes: new Map(childCacheNode.parallelRoutes)
|
|
};
|
|
childSegmentMap.set(segmentForCache, childCacheNode);
|
|
}
|
|
invalidateCacheBelowFlightSegmentPath(childCacheNode, existingChildCacheNode, flightSegmentPath.slice(2));
|
|
}
|
|
/**
|
|
* Fill cache with subTreeData based on flightDataPath that was prefetched
|
|
* This operation is append-only to the existing cache.
|
|
*/ function fillCacheWithPrefetchedSubTreeData(existingCache, flightDataPath) {
|
|
const isLastEntry = flightDataPath.length <= 5;
|
|
const [parallelRouteKey, segment] = flightDataPath;
|
|
const segmentForCache = Array.isArray(segment) ? segment[1] : segment;
|
|
const existingChildSegmentMap = existingCache.parallelRoutes.get(parallelRouteKey);
|
|
if (!existingChildSegmentMap) {
|
|
// Bailout because the existing cache does not have the path to the leaf node
|
|
return;
|
|
}
|
|
const existingChildCacheNode = existingChildSegmentMap.get(segmentForCache);
|
|
if (isLastEntry) {
|
|
if (!existingChildCacheNode) {
|
|
const childCacheNode = {
|
|
status: _appRouterContext.CacheStates.READY,
|
|
data: null,
|
|
subTreeData: flightDataPath[3],
|
|
parallelRoutes: new Map()
|
|
};
|
|
fillLazyItemsTillLeafWithHead(childCacheNode, existingChildCacheNode, flightDataPath[2], flightDataPath[4]);
|
|
existingChildSegmentMap.set(segmentForCache, childCacheNode);
|
|
}
|
|
return;
|
|
}
|
|
if (!existingChildCacheNode) {
|
|
// Bailout because the existing cache does not have the path to the leaf node
|
|
return;
|
|
}
|
|
fillCacheWithPrefetchedSubTreeData(existingChildCacheNode, flightDataPath.slice(2));
|
|
}
|
|
/**
|
|
* Kick off fetch based on the common layout between two routes. Fill cache with data property holding the in-progress fetch.
|
|
*/ function fillCacheWithDataProperty(newCache, existingCache, segments, fetchResponse) {
|
|
const isLastEntry = segments.length === 1;
|
|
const parallelRouteKey = 'children';
|
|
const [segment] = segments;
|
|
const existingChildSegmentMap = existingCache.parallelRoutes.get(parallelRouteKey);
|
|
if (!existingChildSegmentMap) {
|
|
// Bailout because the existing cache does not have the path to the leaf node
|
|
// Will trigger lazy fetch in layout-router because of missing segment
|
|
return {
|
|
bailOptimistic: true
|
|
};
|
|
}
|
|
let childSegmentMap = newCache.parallelRoutes.get(parallelRouteKey);
|
|
if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) {
|
|
childSegmentMap = new Map(existingChildSegmentMap);
|
|
newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap);
|
|
}
|
|
const existingChildCacheNode = existingChildSegmentMap.get(segment);
|
|
let childCacheNode = childSegmentMap.get(segment);
|
|
// In case of last segment start off the fetch at this level and don't copy further down.
|
|
if (isLastEntry) {
|
|
if (!childCacheNode || !childCacheNode.data || childCacheNode === existingChildCacheNode) {
|
|
childSegmentMap.set(segment, {
|
|
status: _appRouterContext.CacheStates.DATA_FETCH,
|
|
data: fetchResponse(),
|
|
subTreeData: null,
|
|
parallelRoutes: new Map()
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (!childCacheNode || !existingChildCacheNode) {
|
|
// Start fetch in the place where the existing cache doesn't have the data yet.
|
|
if (!childCacheNode) {
|
|
childSegmentMap.set(segment, {
|
|
status: _appRouterContext.CacheStates.DATA_FETCH,
|
|
data: fetchResponse(),
|
|
subTreeData: null,
|
|
parallelRoutes: new Map()
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (childCacheNode === existingChildCacheNode) {
|
|
childCacheNode = {
|
|
status: childCacheNode.status,
|
|
data: childCacheNode.data,
|
|
subTreeData: childCacheNode.subTreeData,
|
|
parallelRoutes: new Map(childCacheNode.parallelRoutes)
|
|
};
|
|
childSegmentMap.set(segment, childCacheNode);
|
|
}
|
|
return fillCacheWithDataProperty(childCacheNode, existingChildCacheNode, segments.slice(1), fetchResponse);
|
|
}
|
|
/**
|
|
* Create optimistic version of router state based on the existing router state and segments.
|
|
* This is used to allow rendering layout-routers up till the point where data is missing.
|
|
*/ function createOptimisticTree(segments, flightRouterState, _isFirstSegment, parentRefetch, _href) {
|
|
const [existingSegment, existingParallelRoutes] = flightRouterState || [
|
|
null,
|
|
{},
|
|
];
|
|
const segment = segments[0];
|
|
const isLastSegment = segments.length === 1;
|
|
const segmentMatches = existingSegment !== null && (0, _matchSegments).matchSegment(existingSegment, segment);
|
|
const shouldRefetchThisLevel = !flightRouterState || !segmentMatches;
|
|
let parallelRoutes = {};
|
|
if (existingSegment !== null && segmentMatches) {
|
|
parallelRoutes = existingParallelRoutes;
|
|
}
|
|
let childTree;
|
|
if (!isLastSegment) {
|
|
const childItem = createOptimisticTree(segments.slice(1), parallelRoutes ? parallelRoutes.children : null, false, parentRefetch || shouldRefetchThisLevel);
|
|
childTree = childItem;
|
|
}
|
|
const result = [
|
|
segment,
|
|
_extends({}, parallelRoutes, childTree ? {
|
|
children: childTree
|
|
} : {}),
|
|
];
|
|
if (!parentRefetch && shouldRefetchThisLevel) {
|
|
result[3] = 'refetch';
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Apply the router state from the Flight response. Creates a new router state tree.
|
|
*/ function applyRouterStatePatchToTree(flightSegmentPath, flightRouterState, treePatch) {
|
|
const [segment, parallelRoutes, , , isRootLayout] = flightRouterState;
|
|
// Root refresh
|
|
if (flightSegmentPath.length === 1) {
|
|
const tree = [
|
|
...treePatch
|
|
];
|
|
return tree;
|
|
}
|
|
const [currentSegment, parallelRouteKey] = flightSegmentPath;
|
|
// Tree path returned from the server should always match up with the current tree in the browser
|
|
if (!(0, _matchSegments).matchSegment(currentSegment, segment)) {
|
|
return null;
|
|
}
|
|
const lastSegment = flightSegmentPath.length === 2;
|
|
let parallelRoutePatch;
|
|
if (lastSegment) {
|
|
parallelRoutePatch = treePatch;
|
|
} else {
|
|
parallelRoutePatch = applyRouterStatePatchToTree(flightSegmentPath.slice(2), parallelRoutes[parallelRouteKey], treePatch);
|
|
if (parallelRoutePatch === null) {
|
|
return null;
|
|
}
|
|
}
|
|
const tree = [
|
|
flightSegmentPath[0],
|
|
_extends({}, parallelRoutes, {
|
|
[parallelRouteKey]: parallelRoutePatch
|
|
}),
|
|
];
|
|
// Current segment is the root layout
|
|
if (isRootLayout) {
|
|
tree[4] = true;
|
|
}
|
|
return tree;
|
|
}
|
|
function shouldHardNavigate(flightSegmentPath, flightRouterState, treePatch) {
|
|
const [segment, parallelRoutes] = flightRouterState;
|
|
// TODO-APP: Check if `as` can be replaced.
|
|
const [currentSegment, parallelRouteKey] = flightSegmentPath;
|
|
// Check if current segment matches the existing segment.
|
|
if (!(0, _matchSegments).matchSegment(currentSegment, segment)) {
|
|
// If dynamic parameter in tree doesn't match up with segment path a hard navigation is triggered.
|
|
if (Array.isArray(currentSegment)) {
|
|
return true;
|
|
}
|
|
// If the existing segment did not match soft navigation is triggered.
|
|
return false;
|
|
}
|
|
const lastSegment = flightSegmentPath.length <= 2;
|
|
if (lastSegment) {
|
|
return false;
|
|
}
|
|
return shouldHardNavigate(flightSegmentPath.slice(2), parallelRoutes[parallelRouteKey], treePatch);
|
|
}
|
|
function isNavigatingToNewRootLayout(currentTree, nextTree) {
|
|
// Compare segments
|
|
const currentTreeSegment = currentTree[0];
|
|
const nextTreeSegment = nextTree[0];
|
|
// If any segment is different before we find the root layout, the root layout has changed.
|
|
// E.g. /same/(group1)/layout.js -> /same/(group2)/layout.js
|
|
// First segment is 'same' for both, keep looking. (group1) changed to (group2) before the root layout was found, it must have changed.
|
|
if (Array.isArray(currentTreeSegment) && Array.isArray(nextTreeSegment)) {
|
|
// Compare dynamic param name and type but ignore the value, different values would not affect the current root layout
|
|
// /[name] - /slug1 and /slug2, both values (slug1 & slug2) still has the same layout /[name]/layout.js
|
|
if (currentTreeSegment[0] !== nextTreeSegment[0] || currentTreeSegment[2] !== nextTreeSegment[2]) {
|
|
return true;
|
|
}
|
|
} else if (currentTreeSegment !== nextTreeSegment) {
|
|
return true;
|
|
}
|
|
// Current tree root layout found
|
|
if (currentTree[4]) {
|
|
// If the next tree doesn't have the root layout flag, it must have changed.
|
|
return !nextTree[4];
|
|
}
|
|
// Current tree didn't have its root layout here, must have changed.
|
|
if (nextTree[4]) {
|
|
return true;
|
|
}
|
|
// We can't assume it's `parallelRoutes.children` here in case the root layout is `app/@something/layout.js`
|
|
// But it's not possible to be more than one parallelRoutes before the root layout is found
|
|
// TODO-APP: change to traverse all parallel routes
|
|
const currentTreeChild = Object.values(currentTree[1])[0];
|
|
const nextTreeChild = Object.values(nextTree[1])[0];
|
|
if (!currentTreeChild || !nextTreeChild) return true;
|
|
return isNavigatingToNewRootLayout(currentTreeChild, nextTreeChild);
|
|
}
|
|
const ACTION_REFRESH = 'refresh';
|
|
exports.ACTION_REFRESH = ACTION_REFRESH;
|
|
const ACTION_NAVIGATE = 'navigate';
|
|
exports.ACTION_NAVIGATE = ACTION_NAVIGATE;
|
|
const ACTION_RESTORE = 'restore';
|
|
exports.ACTION_RESTORE = ACTION_RESTORE;
|
|
const ACTION_SERVER_PATCH = 'server-patch';
|
|
exports.ACTION_SERVER_PATCH = ACTION_SERVER_PATCH;
|
|
const ACTION_PREFETCH = 'prefetch';
|
|
exports.ACTION_PREFETCH = ACTION_PREFETCH;
|
|
/**
|
|
* Reducer that handles the app-router state updates.
|
|
*/ function clientReducer(state, action) {
|
|
switch(action.type){
|
|
case ACTION_NAVIGATE:
|
|
{
|
|
const { url , navigateType , cache , mutable , forceOptimisticNavigation } = action;
|
|
const { pathname , search } = url;
|
|
const href = createHrefFromUrl(url);
|
|
const pendingPush = navigateType === 'push';
|
|
const isForCurrentTree = JSON.stringify(mutable.previousTree) === JSON.stringify(state.tree);
|
|
if (mutable.mpaNavigation && isForCurrentTree) {
|
|
return {
|
|
// Set href.
|
|
canonicalUrl: mutable.canonicalUrlOverride ? mutable.canonicalUrlOverride : href,
|
|
pushRef: {
|
|
pendingPush,
|
|
mpaNavigation: mutable.mpaNavigation
|
|
},
|
|
// All navigation requires scroll and focus management to trigger.
|
|
focusAndScrollRef: {
|
|
apply: false
|
|
},
|
|
// Apply cache.
|
|
cache: state.cache,
|
|
prefetchCache: state.prefetchCache,
|
|
// Apply patched router state.
|
|
tree: state.tree
|
|
};
|
|
}
|
|
// Handle concurrent rendering / strict mode case where the cache and tree were already populated.
|
|
if (mutable.patchedTree && isForCurrentTree) {
|
|
return {
|
|
// Set href.
|
|
canonicalUrl: mutable.canonicalUrlOverride ? mutable.canonicalUrlOverride : href,
|
|
pushRef: {
|
|
pendingPush,
|
|
mpaNavigation: false
|
|
},
|
|
// All navigation requires scroll and focus management to trigger.
|
|
focusAndScrollRef: {
|
|
apply: true
|
|
},
|
|
// Apply cache.
|
|
cache: mutable.useExistingCache ? state.cache : cache,
|
|
prefetchCache: state.prefetchCache,
|
|
// Apply patched router state.
|
|
tree: mutable.patchedTree
|
|
};
|
|
}
|
|
const prefetchValues = state.prefetchCache.get(href);
|
|
if (prefetchValues) {
|
|
// The one before last item is the router state tree patch
|
|
const { flightSegmentPath , tree: newTree , canonicalUrlOverride , } = prefetchValues;
|
|
if (newTree !== null) {
|
|
mutable.previousTree = state.tree;
|
|
mutable.patchedTree = newTree;
|
|
mutable.mpaNavigation = isNavigatingToNewRootLayout(state.tree, newTree);
|
|
const hardNavigate = // TODO-APP: Revisit if this is correct.
|
|
search !== location.search || shouldHardNavigate(// TODO-APP: remove ''
|
|
[
|
|
'',
|
|
...flightSegmentPath
|
|
], state.tree, newTree);
|
|
if (hardNavigate) {
|
|
// Copy subTreeData for the root node of the cache.
|
|
cache.subTreeData = state.cache.subTreeData;
|
|
invalidateCacheBelowFlightSegmentPath(cache, state.cache, flightSegmentPath);
|
|
} else {
|
|
mutable.useExistingCache = true;
|
|
}
|
|
const canonicalUrlOverrideHref = canonicalUrlOverride ? createHrefFromUrl(canonicalUrlOverride) : undefined;
|
|
if (canonicalUrlOverrideHref) {
|
|
mutable.canonicalUrlOverride = canonicalUrlOverrideHref;
|
|
}
|
|
return {
|
|
// Set href.
|
|
canonicalUrl: canonicalUrlOverrideHref ? canonicalUrlOverrideHref : href,
|
|
// Set pendingPush.
|
|
pushRef: {
|
|
pendingPush,
|
|
mpaNavigation: false
|
|
},
|
|
// All navigation requires scroll and focus management to trigger.
|
|
focusAndScrollRef: {
|
|
apply: true
|
|
},
|
|
// Apply patched cache.
|
|
cache: mutable.useExistingCache ? state.cache : cache,
|
|
prefetchCache: state.prefetchCache,
|
|
// Apply patched tree.
|
|
tree: newTree
|
|
};
|
|
}
|
|
}
|
|
// When doing a hard push there can be two cases: with optimistic tree and without
|
|
// The with optimistic tree case only happens when the layouts have a loading state (loading.js)
|
|
// The without optimistic tree case happens when there is no loading state, in that case we suspend in this reducer
|
|
// forceOptimisticNavigation is used for links that have `prefetch={false}`.
|
|
if (forceOptimisticNavigation) {
|
|
const segments = pathname.split('/');
|
|
// TODO-APP: figure out something better for index pages
|
|
segments.push('');
|
|
// Optimistic tree case.
|
|
// If the optimistic tree is deeper than the current state leave that deeper part out of the fetch
|
|
const optimisticTree = createOptimisticTree(segments, state.tree, true, false, href);
|
|
// Copy subTreeData for the root node of the cache.
|
|
cache.subTreeData = state.cache.subTreeData;
|
|
// Copy existing cache nodes as far as possible and fill in `data` property with the started data fetch.
|
|
// The `data` property is used to suspend in layout-router during render if it hasn't resolved yet by the time it renders.
|
|
const res = fillCacheWithDataProperty(cache, state.cache, // TODO-APP: segments.slice(1) strips '', we can get rid of '' altogether.
|
|
segments.slice(1), ()=>(0, _appRouter).fetchServerResponse(url, optimisticTree));
|
|
// If optimistic fetch couldn't happen it falls back to the non-optimistic case.
|
|
if (!(res == null ? void 0 : res.bailOptimistic)) {
|
|
mutable.previousTree = state.tree;
|
|
mutable.patchedTree = optimisticTree;
|
|
mutable.mpaNavigation = isNavigatingToNewRootLayout(state.tree, optimisticTree);
|
|
return {
|
|
// Set href.
|
|
canonicalUrl: href,
|
|
// Set pendingPush.
|
|
pushRef: {
|
|
pendingPush,
|
|
mpaNavigation: false
|
|
},
|
|
// All navigation requires scroll and focus management to trigger.
|
|
focusAndScrollRef: {
|
|
apply: true
|
|
},
|
|
// Apply patched cache.
|
|
cache: cache,
|
|
prefetchCache: state.prefetchCache,
|
|
// Apply optimistic tree.
|
|
tree: optimisticTree
|
|
};
|
|
}
|
|
}
|
|
// Below is the not-optimistic case. Data is fetched at the root and suspended there without a suspense boundary.
|
|
// If no in-flight fetch at the top, start it.
|
|
if (!cache.data) {
|
|
cache.data = createRecordFromThenable((0, _appRouter).fetchServerResponse(url, state.tree));
|
|
}
|
|
// Unwrap cache data with `use` to suspend here (in the reducer) until the fetch resolves.
|
|
const [flightData, canonicalUrlOverride] = readRecordValue(cache.data);
|
|
// Handle case when navigating to page in `pages` from `app`
|
|
if (typeof flightData === 'string') {
|
|
return {
|
|
canonicalUrl: flightData,
|
|
// Enable mpaNavigation
|
|
pushRef: {
|
|
pendingPush: true,
|
|
mpaNavigation: true
|
|
},
|
|
// Don't apply scroll and focus management.
|
|
focusAndScrollRef: {
|
|
apply: false
|
|
},
|
|
cache: state.cache,
|
|
prefetchCache: state.prefetchCache,
|
|
tree: state.tree
|
|
};
|
|
}
|
|
// Remove cache.data as it has been resolved at this point.
|
|
cache.data = null;
|
|
// TODO-APP: Currently the Flight data can only have one item but in the future it can have multiple paths.
|
|
const flightDataPath = flightData[0];
|
|
// The one before last item is the router state tree patch
|
|
const [treePatch, subTreeData, head] = flightDataPath.slice(-3);
|
|
// Path without the last segment, router state, and the subTreeData
|
|
const flightSegmentPath = flightDataPath.slice(0, -4);
|
|
// Create new tree based on the flightSegmentPath and router state patch
|
|
const newTree = applyRouterStatePatchToTree(// TODO-APP: remove ''
|
|
[
|
|
'',
|
|
...flightSegmentPath
|
|
], state.tree, treePatch);
|
|
if (newTree === null) {
|
|
throw new Error('SEGMENT MISMATCH');
|
|
}
|
|
const canonicalUrlOverrideHref = canonicalUrlOverride ? createHrefFromUrl(canonicalUrlOverride) : undefined;
|
|
if (canonicalUrlOverrideHref) {
|
|
mutable.canonicalUrlOverride = canonicalUrlOverrideHref;
|
|
}
|
|
mutable.previousTree = state.tree;
|
|
mutable.patchedTree = newTree;
|
|
mutable.mpaNavigation = isNavigatingToNewRootLayout(state.tree, newTree);
|
|
if (flightDataPath.length === 3) {
|
|
cache.subTreeData = subTreeData;
|
|
fillLazyItemsTillLeafWithHead(cache, state.cache, treePatch, head);
|
|
} else {
|
|
// Copy subTreeData for the root node of the cache.
|
|
cache.subTreeData = state.cache.subTreeData;
|
|
// Create a copy of the existing cache with the subTreeData applied.
|
|
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath);
|
|
}
|
|
return {
|
|
// Set href.
|
|
canonicalUrl: canonicalUrlOverrideHref ? canonicalUrlOverrideHref : href,
|
|
// Set pendingPush.
|
|
pushRef: {
|
|
pendingPush,
|
|
mpaNavigation: false
|
|
},
|
|
// All navigation requires scroll and focus management to trigger.
|
|
focusAndScrollRef: {
|
|
apply: true
|
|
},
|
|
// Apply patched cache.
|
|
cache: cache,
|
|
prefetchCache: state.prefetchCache,
|
|
// Apply patched tree.
|
|
tree: newTree
|
|
};
|
|
}
|
|
case ACTION_SERVER_PATCH:
|
|
{
|
|
const { flightData , previousTree , overrideCanonicalUrl , cache , mutable } = action;
|
|
// When a fetch is slow to resolve it could be that you navigated away while the request was happening or before the reducer runs.
|
|
// In that case opt-out of applying the patch given that the data could be stale.
|
|
if (JSON.stringify(previousTree) !== JSON.stringify(state.tree)) {
|
|
// TODO-APP: Handle tree mismatch
|
|
console.log('TREE MISMATCH');
|
|
// Keep everything as-is.
|
|
return state;
|
|
}
|
|
if (mutable.mpaNavigation) {
|
|
return {
|
|
// Set href.
|
|
canonicalUrl: mutable.canonicalUrlOverride ? mutable.canonicalUrlOverride : state.canonicalUrl,
|
|
// TODO-APP: verify mpaNavigation not being set is correct here.
|
|
pushRef: {
|
|
pendingPush: true,
|
|
mpaNavigation: mutable.mpaNavigation
|
|
},
|
|
// All navigation requires scroll and focus management to trigger.
|
|
focusAndScrollRef: {
|
|
apply: false
|
|
},
|
|
// Apply cache.
|
|
cache: state.cache,
|
|
prefetchCache: state.prefetchCache,
|
|
// Apply patched router state.
|
|
tree: state.tree
|
|
};
|
|
}
|
|
// Handle concurrent rendering / strict mode case where the cache and tree were already populated.
|
|
if (mutable.patchedTree) {
|
|
return {
|
|
// Keep href as it was set during navigate / restore
|
|
canonicalUrl: mutable.canonicalUrlOverride ? mutable.canonicalUrlOverride : state.canonicalUrl,
|
|
// Keep pushRef as server-patch only causes cache/tree update.
|
|
pushRef: state.pushRef,
|
|
// Keep focusAndScrollRef as server-patch only causes cache/tree update.
|
|
focusAndScrollRef: state.focusAndScrollRef,
|
|
// Apply patched router state
|
|
tree: mutable.patchedTree,
|
|
prefetchCache: state.prefetchCache,
|
|
// Apply patched cache
|
|
cache: cache
|
|
};
|
|
}
|
|
// Handle case when navigating to page in `pages` from `app`
|
|
if (typeof flightData === 'string') {
|
|
return {
|
|
// Set href.
|
|
canonicalUrl: flightData,
|
|
// Enable mpaNavigation as this is a navigation that the app-router shouldn't handle.
|
|
pushRef: {
|
|
pendingPush: true,
|
|
mpaNavigation: true
|
|
},
|
|
// Don't apply scroll and focus management.
|
|
focusAndScrollRef: {
|
|
apply: false
|
|
},
|
|
// Other state is kept as-is.
|
|
cache: state.cache,
|
|
prefetchCache: state.prefetchCache,
|
|
tree: state.tree
|
|
};
|
|
}
|
|
// TODO-APP: Currently the Flight data can only have one item but in the future it can have multiple paths.
|
|
const flightDataPath = flightData[0];
|
|
// Slices off the last segment (which is at -4) as it doesn't exist in the tree yet
|
|
const flightSegmentPath = flightDataPath.slice(0, -4);
|
|
const [treePatch, subTreeData, head] = flightDataPath.slice(-3);
|
|
const newTree = applyRouterStatePatchToTree(// TODO-APP: remove ''
|
|
[
|
|
'',
|
|
...flightSegmentPath
|
|
], state.tree, treePatch);
|
|
if (newTree === null) {
|
|
throw new Error('SEGMENT MISMATCH');
|
|
}
|
|
const canonicalUrlOverrideHref = overrideCanonicalUrl ? createHrefFromUrl(overrideCanonicalUrl) : undefined;
|
|
if (canonicalUrlOverrideHref) {
|
|
mutable.canonicalUrlOverride = canonicalUrlOverrideHref;
|
|
}
|
|
mutable.patchedTree = newTree;
|
|
mutable.mpaNavigation = isNavigatingToNewRootLayout(state.tree, newTree);
|
|
// Root refresh
|
|
if (flightDataPath.length === 3) {
|
|
cache.subTreeData = subTreeData;
|
|
fillLazyItemsTillLeafWithHead(cache, state.cache, treePatch, head);
|
|
} else {
|
|
// Copy subTreeData for the root node of the cache.
|
|
cache.subTreeData = state.cache.subTreeData;
|
|
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath);
|
|
}
|
|
return {
|
|
// Keep href as it was set during navigate / restore
|
|
canonicalUrl: canonicalUrlOverrideHref ? canonicalUrlOverrideHref : state.canonicalUrl,
|
|
// Keep pushRef as server-patch only causes cache/tree update.
|
|
pushRef: state.pushRef,
|
|
// Keep focusAndScrollRef as server-patch only causes cache/tree update.
|
|
focusAndScrollRef: state.focusAndScrollRef,
|
|
// Apply patched router state
|
|
tree: newTree,
|
|
prefetchCache: state.prefetchCache,
|
|
// Apply patched cache
|
|
cache: cache
|
|
};
|
|
}
|
|
case ACTION_RESTORE:
|
|
{
|
|
const { url , tree } = action;
|
|
const href = createHrefFromUrl(url);
|
|
return {
|
|
// Set canonical url
|
|
canonicalUrl: href,
|
|
pushRef: state.pushRef,
|
|
focusAndScrollRef: state.focusAndScrollRef,
|
|
cache: state.cache,
|
|
prefetchCache: state.prefetchCache,
|
|
// Restore provided tree
|
|
tree: tree
|
|
};
|
|
}
|
|
// TODO-APP: Add test for not scrolling to nearest layout when calling refresh.
|
|
// TODO-APP: Add test for startTransition(() => {router.push('/'); router.refresh();}), that case should scroll.
|
|
case ACTION_REFRESH:
|
|
{
|
|
const { cache , mutable } = action;
|
|
const href = state.canonicalUrl;
|
|
const isForCurrentTree = JSON.stringify(mutable.previousTree) === JSON.stringify(state.tree);
|
|
if (mutable.mpaNavigation && isForCurrentTree) {
|
|
return {
|
|
// Set href.
|
|
canonicalUrl: mutable.canonicalUrlOverride ? mutable.canonicalUrlOverride : state.canonicalUrl,
|
|
// TODO-APP: verify mpaNavigation not being set is correct here.
|
|
pushRef: {
|
|
pendingPush: true,
|
|
mpaNavigation: mutable.mpaNavigation
|
|
},
|
|
// All navigation requires scroll and focus management to trigger.
|
|
focusAndScrollRef: {
|
|
apply: false
|
|
},
|
|
// Apply cache.
|
|
cache: state.cache,
|
|
prefetchCache: state.prefetchCache,
|
|
// Apply patched router state.
|
|
tree: state.tree
|
|
};
|
|
}
|
|
// Handle concurrent rendering / strict mode case where the cache and tree were already populated.
|
|
if (mutable.patchedTree && isForCurrentTree) {
|
|
return {
|
|
// Set href.
|
|
canonicalUrl: mutable.canonicalUrlOverride ? mutable.canonicalUrlOverride : href,
|
|
// set pendingPush (always false in this case).
|
|
pushRef: state.pushRef,
|
|
// Apply focus and scroll.
|
|
// TODO-APP: might need to disable this for Fast Refresh.
|
|
focusAndScrollRef: {
|
|
apply: false
|
|
},
|
|
cache: cache,
|
|
prefetchCache: state.prefetchCache,
|
|
tree: mutable.patchedTree
|
|
};
|
|
}
|
|
if (!cache.data) {
|
|
// Fetch data from the root of the tree.
|
|
cache.data = createRecordFromThenable((0, _appRouter).fetchServerResponse(new URL(href, location.origin), [
|
|
state.tree[0],
|
|
state.tree[1],
|
|
state.tree[2],
|
|
'refetch',
|
|
]));
|
|
}
|
|
const [flightData, canonicalUrlOverride] = readRecordValue(cache.data);
|
|
// Handle case when navigating to page in `pages` from `app`
|
|
if (typeof flightData === 'string') {
|
|
return {
|
|
canonicalUrl: flightData,
|
|
pushRef: {
|
|
pendingPush: true,
|
|
mpaNavigation: true
|
|
},
|
|
focusAndScrollRef: {
|
|
apply: false
|
|
},
|
|
cache: state.cache,
|
|
prefetchCache: state.prefetchCache,
|
|
tree: state.tree
|
|
};
|
|
}
|
|
// Remove cache.data as it has been resolved at this point.
|
|
cache.data = null;
|
|
// TODO-APP: Currently the Flight data can only have one item but in the future it can have multiple paths.
|
|
const flightDataPath = flightData[0];
|
|
// FlightDataPath with more than two items means unexpected Flight data was returned
|
|
if (flightDataPath.length !== 3) {
|
|
// TODO-APP: handle this case better
|
|
console.log('REFRESH FAILED');
|
|
return state;
|
|
}
|
|
// Given the path can only have two items the items are only the router state and subTreeData for the root.
|
|
const [treePatch, subTreeData, head] = flightDataPath;
|
|
const newTree = applyRouterStatePatchToTree(// TODO-APP: remove ''
|
|
[
|
|
''
|
|
], state.tree, treePatch);
|
|
if (newTree === null) {
|
|
throw new Error('SEGMENT MISMATCH');
|
|
}
|
|
const canonicalUrlOverrideHref = canonicalUrlOverride ? createHrefFromUrl(canonicalUrlOverride) : undefined;
|
|
if (canonicalUrlOverride) {
|
|
mutable.canonicalUrlOverride = canonicalUrlOverrideHref;
|
|
}
|
|
mutable.previousTree = state.tree;
|
|
mutable.patchedTree = newTree;
|
|
mutable.mpaNavigation = isNavigatingToNewRootLayout(state.tree, newTree);
|
|
// Set subTreeData for the root node of the cache.
|
|
cache.subTreeData = subTreeData;
|
|
fillLazyItemsTillLeafWithHead(cache, state.cache, treePatch, head);
|
|
return {
|
|
// Set href, this doesn't reuse the state.canonicalUrl as because of concurrent rendering the href might change between dispatching and applying.
|
|
canonicalUrl: canonicalUrlOverrideHref ? canonicalUrlOverrideHref : href,
|
|
// set pendingPush (always false in this case).
|
|
pushRef: state.pushRef,
|
|
// TODO-APP: might need to disable this for Fast Refresh.
|
|
focusAndScrollRef: {
|
|
apply: false
|
|
},
|
|
// Apply patched cache.
|
|
cache: cache,
|
|
prefetchCache: state.prefetchCache,
|
|
// Apply patched router state.
|
|
tree: newTree
|
|
};
|
|
}
|
|
case ACTION_PREFETCH:
|
|
{
|
|
const { url , serverResponse } = action;
|
|
const [flightData, canonicalUrlOverride] = serverResponse;
|
|
if (typeof flightData === 'string') {
|
|
return state;
|
|
}
|
|
const href = createHrefFromUrl(url);
|
|
// TODO-APP: Currently the Flight data can only have one item but in the future it can have multiple paths.
|
|
const flightDataPath = flightData[0];
|
|
// The one before last item is the router state tree patch
|
|
const [treePatch, subTreeData] = flightDataPath.slice(-3);
|
|
// TODO-APP: Verify if `null` can't be returned from user code.
|
|
// If subTreeData is null the prefetch did not provide a component tree.
|
|
if (subTreeData !== null) {
|
|
fillCacheWithPrefetchedSubTreeData(state.cache, flightDataPath);
|
|
}
|
|
const flightSegmentPath = flightDataPath.slice(0, -3);
|
|
const newTree = applyRouterStatePatchToTree(// TODO-APP: remove ''
|
|
[
|
|
'',
|
|
...flightSegmentPath
|
|
], state.tree, treePatch);
|
|
// Patch did not apply correctly
|
|
if (newTree === null) {
|
|
return state;
|
|
}
|
|
// Create new tree based on the flightSegmentPath and router state patch
|
|
state.prefetchCache.set(href, {
|
|
// Path without the last segment, router state, and the subTreeData
|
|
flightSegmentPath,
|
|
// Create new tree based on the flightSegmentPath and router state patch
|
|
tree: newTree,
|
|
canonicalUrlOverride
|
|
});
|
|
return state;
|
|
}
|
|
// This case should never be hit as dispatch is strongly typed.
|
|
default:
|
|
throw new Error('Unknown action');
|
|
}
|
|
}
|
|
function serverReducer(state, _action) {
|
|
return state;
|
|
}
|
|
const reducer = typeof window === 'undefined' ? serverReducer : clientReducer;
|
|
exports.reducer = reducer;
|
|
|
|
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=reducer.js.map
|