import _extends from "@swc/helpers/src/_extends.mjs"; import { CacheStates } from '../../shared/lib/app-router-context'; import { matchSegment } from './match-segments'; import { fetchServerResponse } from './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; } } export 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: 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: 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: 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: 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: 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: 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 && 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 (!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 (!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); } export const ACTION_REFRESH = 'refresh'; export const ACTION_NAVIGATE = 'navigate'; export const ACTION_RESTORE = 'restore'; export const ACTION_SERVER_PATCH = 'server-patch'; export const ACTION_PREFETCH = '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), ()=>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(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(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; } // we don't run the client reducer on the server, so we use a noop function for better tree shaking export const reducer = typeof window === 'undefined' ? serverReducer : clientReducer; //# sourceMappingURL=reducer.js.map