Spaces:
Running
Running
| ; | |
| Object.defineProperty(exports, "__esModule", { | |
| value: true | |
| }); | |
| 0 && (module.exports = { | |
| convertServerPatchToFullTree: null, | |
| navigate: null, | |
| navigateToSeededRoute: null | |
| }); | |
| function _export(target, all) { | |
| for(var name in all)Object.defineProperty(target, name, { | |
| enumerable: true, | |
| get: all[name] | |
| }); | |
| } | |
| _export(exports, { | |
| convertServerPatchToFullTree: function() { | |
| return convertServerPatchToFullTree; | |
| }, | |
| navigate: function() { | |
| return navigate; | |
| }, | |
| navigateToSeededRoute: function() { | |
| return navigateToSeededRoute; | |
| } | |
| }); | |
| const _fetchserverresponse = require("../router-reducer/fetch-server-response"); | |
| const _pprnavigations = require("../router-reducer/ppr-navigations"); | |
| const _createhreffromurl = require("../router-reducer/create-href-from-url"); | |
| const _cache = require("./cache"); | |
| const _cachekey = require("./cache-key"); | |
| const _segment = require("../../../shared/lib/segment"); | |
| const _types = require("./types"); | |
| function navigate(url, currentUrl, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, shouldScroll, accumulation) { | |
| const now = Date.now(); | |
| const href = url.href; | |
| // We special case navigations to the exact same URL as the current location. | |
| // It's a common UI pattern for apps to refresh when you click a link to the | |
| // current page. So when this happens, we refresh the dynamic data in the page | |
| // segments. | |
| // | |
| // Note that this does not apply if the any part of the hash or search query | |
| // has changed. This might feel a bit weird but it makes more sense when you | |
| // consider that the way to trigger this behavior is to click the same link | |
| // multiple times. | |
| // | |
| // TODO: We should probably refresh the *entire* route when this case occurs, | |
| // not just the page segments. Essentially treating it the same as a refresh() | |
| // triggered by an action, which is the more explicit way of modeling the UI | |
| // pattern described above. | |
| // | |
| // Also note that this only refreshes the dynamic data, not static/ cached | |
| // data. If the page segment is fully static and prefetched, the request is | |
| // skipped. (This is also how refresh() works.) | |
| const isSamePageNavigation = href === currentUrl.href; | |
| const cacheKey = (0, _cachekey.createCacheKey)(href, nextUrl); | |
| const route = (0, _cache.readRouteCacheEntry)(now, cacheKey); | |
| if (route !== null && route.status === _cache.EntryStatus.Fulfilled) { | |
| // We have a matching prefetch. | |
| const snapshot = readRenderSnapshotFromCache(now, route, route.tree); | |
| const prefetchFlightRouterState = snapshot.flightRouterState; | |
| const prefetchSeedData = snapshot.seedData; | |
| const headSnapshot = readHeadSnapshotFromCache(now, route); | |
| const prefetchHead = headSnapshot.rsc; | |
| const isPrefetchHeadPartial = headSnapshot.isPartial; | |
| // TODO: The "canonicalUrl" stored in the cache doesn't include the hash, | |
| // because hash entries do not vary by hash fragment. However, the one | |
| // we set in the router state *does* include the hash, and it's used to | |
| // sync with the actual browser location. To make this less of a refactor | |
| // hazard, we should always track the hash separately from the rest of | |
| // the URL. | |
| const newCanonicalUrl = route.canonicalUrl + url.hash; | |
| const renderedSearch = route.renderedSearch; | |
| return navigateUsingPrefetchedRouteTree(now, url, currentUrl, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, newCanonicalUrl, renderedSearch, freshnessPolicy, shouldScroll); | |
| } | |
| // There was no matching route tree in the cache. Let's see if we can | |
| // construct an "optimistic" route tree. | |
| // | |
| // Do not construct an optimistic route tree if there was a cache hit, but | |
| // the entry has a rejected status, since it may have been rejected due to a | |
| // rewrite or redirect based on the search params. | |
| // | |
| // TODO: There are multiple reasons a prefetch might be rejected; we should | |
| // track them explicitly and choose what to do here based on that. | |
| if (route === null || route.status !== _cache.EntryStatus.Rejected) { | |
| const optimisticRoute = (0, _cache.requestOptimisticRouteCacheEntry)(now, url, nextUrl); | |
| if (optimisticRoute !== null) { | |
| // We have an optimistic route tree. Proceed with the normal flow. | |
| const snapshot = readRenderSnapshotFromCache(now, optimisticRoute, optimisticRoute.tree); | |
| const prefetchFlightRouterState = snapshot.flightRouterState; | |
| const prefetchSeedData = snapshot.seedData; | |
| const headSnapshot = readHeadSnapshotFromCache(now, optimisticRoute); | |
| const prefetchHead = headSnapshot.rsc; | |
| const isPrefetchHeadPartial = headSnapshot.isPartial; | |
| const newCanonicalUrl = optimisticRoute.canonicalUrl + url.hash; | |
| const newRenderedSearch = optimisticRoute.renderedSearch; | |
| return navigateUsingPrefetchedRouteTree(now, url, currentUrl, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, newCanonicalUrl, newRenderedSearch, freshnessPolicy, shouldScroll); | |
| } | |
| } | |
| // There's no matching prefetch for this route in the cache. | |
| let collectedDebugInfo = accumulation.collectedDebugInfo ?? []; | |
| if (accumulation.collectedDebugInfo === undefined) { | |
| collectedDebugInfo = accumulation.collectedDebugInfo = []; | |
| } | |
| return { | |
| tag: _types.NavigationResultTag.Async, | |
| data: navigateDynamicallyWithNoPrefetch(now, url, currentUrl, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, shouldScroll, collectedDebugInfo) | |
| }; | |
| } | |
| function navigateToSeededRoute(now, url, canonicalUrl, navigationSeed, currentUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, shouldScroll) { | |
| // A version of navigate() that accepts the target route tree as an argument | |
| // rather than reading it from the prefetch cache. | |
| const accumulation = { | |
| scrollableSegments: null, | |
| separateRefreshUrls: null | |
| }; | |
| const isSamePageNavigation = url.href === currentUrl.href; | |
| const task = (0, _pprnavigations.startPPRNavigation)(now, currentUrl, currentCacheNode, currentFlightRouterState, navigationSeed.tree, freshnessPolicy, navigationSeed.data, navigationSeed.head, null, null, false, isSamePageNavigation, accumulation); | |
| if (task !== null) { | |
| (0, _pprnavigations.spawnDynamicRequests)(task, url, nextUrl, freshnessPolicy, accumulation); | |
| return navigationTaskToResult(task, canonicalUrl, navigationSeed.renderedSearch, accumulation.scrollableSegments, shouldScroll, url.hash); | |
| } | |
| // Could not perform a SPA navigation. Revert to a full-page (MPA) navigation. | |
| return { | |
| tag: _types.NavigationResultTag.MPA, | |
| data: canonicalUrl | |
| }; | |
| } | |
| function navigateUsingPrefetchedRouteTree(now, url, currentUrl, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, canonicalUrl, renderedSearch, freshnessPolicy, shouldScroll) { | |
| // Recursively construct a prefetch tree by reading from the Segment Cache. To | |
| // maintain compatibility, we output the same data structures as the old | |
| // prefetching implementation: FlightRouterState and CacheNodeSeedData. | |
| // TODO: Eventually updateCacheNodeOnNavigation (or the equivalent) should | |
| // read from the Segment Cache directly. It's only structured this way for now | |
| // so we can share code with the old prefetching implementation. | |
| const accumulation = { | |
| scrollableSegments: null, | |
| separateRefreshUrls: null | |
| }; | |
| const seedData = null; | |
| const seedHead = null; | |
| const task = (0, _pprnavigations.startPPRNavigation)(now, currentUrl, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, freshnessPolicy, seedData, seedHead, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, accumulation); | |
| if (task !== null) { | |
| (0, _pprnavigations.spawnDynamicRequests)(task, url, nextUrl, freshnessPolicy, accumulation); | |
| return navigationTaskToResult(task, canonicalUrl, renderedSearch, accumulation.scrollableSegments, shouldScroll, url.hash); | |
| } | |
| // Could not perform a SPA navigation. Revert to a full-page (MPA) navigation. | |
| return { | |
| tag: _types.NavigationResultTag.MPA, | |
| data: canonicalUrl | |
| }; | |
| } | |
| function navigationTaskToResult(task, canonicalUrl, renderedSearch, scrollableSegments, shouldScroll, hash) { | |
| return { | |
| tag: _types.NavigationResultTag.Success, | |
| data: { | |
| flightRouterState: task.route, | |
| cacheNode: task.node, | |
| canonicalUrl, | |
| renderedSearch, | |
| scrollableSegments, | |
| shouldScroll, | |
| hash | |
| } | |
| }; | |
| } | |
| function readRenderSnapshotFromCache(now, route, tree) { | |
| let childRouterStates = {}; | |
| let childSeedDatas = {}; | |
| const slots = tree.slots; | |
| if (slots !== null) { | |
| for(const parallelRouteKey in slots){ | |
| const childTree = slots[parallelRouteKey]; | |
| const childResult = readRenderSnapshotFromCache(now, route, childTree); | |
| childRouterStates[parallelRouteKey] = childResult.flightRouterState; | |
| childSeedDatas[parallelRouteKey] = childResult.seedData; | |
| } | |
| } | |
| let rsc = null; | |
| let loading = null; | |
| let isPartial = true; | |
| const segmentEntry = (0, _cache.readSegmentCacheEntry)(now, tree.varyPath); | |
| if (segmentEntry !== null) { | |
| switch(segmentEntry.status){ | |
| case _cache.EntryStatus.Fulfilled: | |
| { | |
| // Happy path: a cache hit | |
| rsc = segmentEntry.rsc; | |
| loading = segmentEntry.loading; | |
| isPartial = segmentEntry.isPartial; | |
| break; | |
| } | |
| case _cache.EntryStatus.Pending: | |
| { | |
| // We haven't received data for this segment yet, but there's already | |
| // an in-progress request. Since it's extremely likely to arrive | |
| // before the dynamic data response, we might as well use it. | |
| const promiseForFulfilledEntry = (0, _cache.waitForSegmentCacheEntry)(segmentEntry); | |
| rsc = promiseForFulfilledEntry.then((entry)=>entry !== null ? entry.rsc : null); | |
| loading = promiseForFulfilledEntry.then((entry)=>entry !== null ? entry.loading : null); | |
| // Because the request is still pending, we typically don't know yet | |
| // whether the response will be partial. We shouldn't skip this segment | |
| // during the dynamic navigation request. Otherwise, we might need to | |
| // do yet another request to fill in the remaining data, creating | |
| // a waterfall. | |
| // | |
| // The one exception is if this segment is being fetched with via | |
| // prefetch={true} (i.e. the "force stale" or "full" strategy). If so, | |
| // we can assume the response will be full. This field is set to `false` | |
| // for such segments. | |
| isPartial = segmentEntry.isPartial; | |
| break; | |
| } | |
| case _cache.EntryStatus.Empty: | |
| case _cache.EntryStatus.Rejected: | |
| break; | |
| default: | |
| segmentEntry; | |
| } | |
| } | |
| // The navigation implementation expects the search params to be | |
| // included in the segment. However, the Segment Cache tracks search | |
| // params separately from the rest of the segment key. So we need to | |
| // add them back here. | |
| // | |
| // See corresponding comment in convertFlightRouterStateToTree. | |
| // | |
| // TODO: What we should do instead is update the navigation diffing | |
| // logic to compare search params explicitly. This is a temporary | |
| // solution until more of the Segment Cache implementation has settled. | |
| const segment = (0, _segment.addSearchParamsIfPageSegment)(tree.segment, Object.fromEntries(new URLSearchParams(route.renderedSearch))); | |
| // We don't need this information in a render snapshot, so this can just be a placeholder. | |
| const hasRuntimePrefetch = false; | |
| return { | |
| flightRouterState: [ | |
| segment, | |
| childRouterStates, | |
| null, | |
| null, | |
| tree.isRootLayout | |
| ], | |
| seedData: [ | |
| rsc, | |
| childSeedDatas, | |
| loading, | |
| isPartial, | |
| hasRuntimePrefetch | |
| ] | |
| }; | |
| } | |
| function readHeadSnapshotFromCache(now, route) { | |
| // Same as readRenderSnapshotFromCache, but for the head | |
| let rsc = null; | |
| let isPartial = true; | |
| const segmentEntry = (0, _cache.readSegmentCacheEntry)(now, route.metadata.varyPath); | |
| if (segmentEntry !== null) { | |
| switch(segmentEntry.status){ | |
| case _cache.EntryStatus.Fulfilled: | |
| { | |
| rsc = segmentEntry.rsc; | |
| isPartial = segmentEntry.isPartial; | |
| break; | |
| } | |
| case _cache.EntryStatus.Pending: | |
| { | |
| const promiseForFulfilledEntry = (0, _cache.waitForSegmentCacheEntry)(segmentEntry); | |
| rsc = promiseForFulfilledEntry.then((entry)=>entry !== null ? entry.rsc : null); | |
| isPartial = segmentEntry.isPartial; | |
| break; | |
| } | |
| case _cache.EntryStatus.Empty: | |
| case _cache.EntryStatus.Rejected: | |
| break; | |
| default: | |
| segmentEntry; | |
| } | |
| } | |
| return { | |
| rsc, | |
| isPartial | |
| }; | |
| } | |
| // Used to request all the dynamic data for a route, rather than just a subset, | |
| // e.g. during a refresh or a revalidation. Typically this gets constructed | |
| // during the normal flow when diffing the route tree, but for an unprefetched | |
| // navigation, where we don't know the structure of the target route, we use | |
| // this instead. | |
| const DynamicRequestTreeForEntireRoute = [ | |
| '', | |
| {}, | |
| null, | |
| 'refetch' | |
| ]; | |
| async function navigateDynamicallyWithNoPrefetch(now, url, currentUrl, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, shouldScroll, collectedDebugInfo) { | |
| // Runs when a navigation happens but there's no cached prefetch we can use. | |
| // Don't bother to wait for a prefetch response; go straight to a full | |
| // navigation that contains both static and dynamic data in a single stream. | |
| // (This is unlike the old navigation implementation, which instead blocks | |
| // the dynamic request until a prefetch request is received.) | |
| // | |
| // To avoid duplication of logic, we're going to pretend that the tree | |
| // returned by the dynamic request is, in fact, a prefetch tree. Then we can | |
| // use the same server response to write the actual data into the CacheNode | |
| // tree. So it's the same flow as the "happy path" (prefetch, then | |
| // navigation), except we use a single server response for both stages. | |
| let dynamicRequestTree; | |
| switch(freshnessPolicy){ | |
| case _pprnavigations.FreshnessPolicy.Default: | |
| case _pprnavigations.FreshnessPolicy.HistoryTraversal: | |
| dynamicRequestTree = currentFlightRouterState; | |
| break; | |
| case _pprnavigations.FreshnessPolicy.Hydration: | |
| case _pprnavigations.FreshnessPolicy.RefreshAll: | |
| case _pprnavigations.FreshnessPolicy.HMRRefresh: | |
| dynamicRequestTree = DynamicRequestTreeForEntireRoute; | |
| break; | |
| default: | |
| freshnessPolicy; | |
| dynamicRequestTree = currentFlightRouterState; | |
| break; | |
| } | |
| const promiseForDynamicServerResponse = (0, _fetchserverresponse.fetchServerResponse)(url, { | |
| flightRouterState: dynamicRequestTree, | |
| nextUrl | |
| }); | |
| const result = await promiseForDynamicServerResponse; | |
| if (typeof result === 'string') { | |
| // This is an MPA navigation. | |
| const newUrl = result; | |
| return { | |
| tag: _types.NavigationResultTag.MPA, | |
| data: newUrl | |
| }; | |
| } | |
| const { flightData, canonicalUrl, renderedSearch, debugInfo: debugInfoFromResponse } = result; | |
| if (debugInfoFromResponse !== null) { | |
| collectedDebugInfo.push(...debugInfoFromResponse); | |
| } | |
| // Since the response format of dynamic requests and prefetches is slightly | |
| // different, we'll need to massage the data a bit. Create FlightRouterState | |
| // tree that simulates what we'd receive as the result of a prefetch. | |
| const navigationSeed = convertServerPatchToFullTree(currentFlightRouterState, flightData, renderedSearch); | |
| return navigateToSeededRoute(now, url, (0, _createhreffromurl.createHrefFromUrl)(canonicalUrl), navigationSeed, currentUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, shouldScroll); | |
| } | |
| function convertServerPatchToFullTree(currentTree, flightData, renderedSearch) { | |
| // During a client navigation or prefetch, the server sends back only a patch | |
| // for the parts of the tree that have changed. | |
| // | |
| // This applies the patch to the base tree to create a full representation of | |
| // the resulting tree. | |
| // | |
| // The return type includes a full FlightRouterState tree and a full | |
| // CacheNodeSeedData tree. (Conceptually these are the same tree, and should | |
| // eventually be unified, but there's still lots of existing code that | |
| // operates on FlightRouterState trees alone without the CacheNodeSeedData.) | |
| // | |
| // TODO: This similar to what apply-router-state-patch-to-tree does. It | |
| // will eventually fully replace it. We should get rid of all the remaining | |
| // places where we iterate over the server patch format. This should also | |
| // eventually replace normalizeFlightData. | |
| let baseTree = currentTree; | |
| let baseData = null; | |
| let head = null; | |
| for (const { segmentPath, tree: treePatch, seedData: dataPatch, head: headPatch } of flightData){ | |
| const result = convertServerPatchToFullTreeImpl(baseTree, baseData, treePatch, dataPatch, segmentPath, 0); | |
| baseTree = result.tree; | |
| baseData = result.data; | |
| // This is the same for all patches per response, so just pick an | |
| // arbitrary one | |
| head = headPatch; | |
| } | |
| return { | |
| tree: baseTree, | |
| data: baseData, | |
| renderedSearch, | |
| head | |
| }; | |
| } | |
| function convertServerPatchToFullTreeImpl(baseRouterState, baseData, treePatch, dataPatch, segmentPath, index) { | |
| if (index === segmentPath.length) { | |
| // We reached the part of the tree that we need to patch. | |
| return { | |
| tree: treePatch, | |
| data: dataPatch | |
| }; | |
| } | |
| // segmentPath represents the parent path of subtree. It's a repeating | |
| // pattern of parallel route key and segment: | |
| // | |
| // [string, Segment, string, Segment, string, Segment, ...] | |
| // | |
| // This path tells us which part of the base tree to apply the tree patch. | |
| // | |
| // NOTE: We receive the FlightRouterState patch in the same request as the | |
| // seed data patch. Therefore we don't need to worry about diffing the segment | |
| // values; we can assume the server sent us a correct result. | |
| const updatedParallelRouteKey = segmentPath[index]; | |
| // const segment: Segment = segmentPath[index + 1] <-- Not used, see note above | |
| const baseTreeChildren = baseRouterState[1]; | |
| const baseSeedDataChildren = baseData !== null ? baseData[1] : null; | |
| const newTreeChildren = {}; | |
| const newSeedDataChildren = {}; | |
| for(const parallelRouteKey in baseTreeChildren){ | |
| const childBaseRouterState = baseTreeChildren[parallelRouteKey]; | |
| const childBaseSeedData = baseSeedDataChildren !== null ? baseSeedDataChildren[parallelRouteKey] ?? null : null; | |
| if (parallelRouteKey === updatedParallelRouteKey) { | |
| const result = convertServerPatchToFullTreeImpl(childBaseRouterState, childBaseSeedData, treePatch, dataPatch, segmentPath, // Advance the index by two and keep cloning until we reach | |
| // the end of the segment path. | |
| index + 2); | |
| newTreeChildren[parallelRouteKey] = result.tree; | |
| newSeedDataChildren[parallelRouteKey] = result.data; | |
| } else { | |
| // This child is not being patched. Copy it over as-is. | |
| newTreeChildren[parallelRouteKey] = childBaseRouterState; | |
| newSeedDataChildren[parallelRouteKey] = childBaseSeedData; | |
| } | |
| } | |
| let clonedTree; | |
| let clonedSeedData; | |
| // Clone all the fields except the children. | |
| // Clone the FlightRouterState tree. Based on equivalent logic in | |
| // apply-router-state-patch-to-tree, but should confirm whether we need to | |
| // copy all of these fields. Not sure the server ever sends, e.g. the | |
| // refetch marker. | |
| clonedTree = [ | |
| baseRouterState[0], | |
| newTreeChildren | |
| ]; | |
| if (2 in baseRouterState) { | |
| clonedTree[2] = baseRouterState[2]; | |
| } | |
| if (3 in baseRouterState) { | |
| clonedTree[3] = baseRouterState[3]; | |
| } | |
| if (4 in baseRouterState) { | |
| clonedTree[4] = baseRouterState[4]; | |
| } | |
| // Clone the CacheNodeSeedData tree. | |
| const isEmptySeedDataPartial = true; | |
| clonedSeedData = [ | |
| null, | |
| newSeedDataChildren, | |
| null, | |
| isEmptySeedDataPartial, | |
| false | |
| ]; | |
| return { | |
| tree: clonedTree, | |
| data: clonedSeedData | |
| }; | |
| } | |
| if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') { | |
| Object.defineProperty(exports.default, '__esModule', { value: true }); | |
| Object.assign(exports.default, exports); | |
| module.exports = exports.default; | |
| } | |
| //# sourceMappingURL=navigation.js.map |