rag-context-optimizer / frontend /node_modules /next /dist /client /components /segment-cache-impl /navigation.js
| ; | |
| Object.defineProperty(exports, "__esModule", { | |
| value: true | |
| }); | |
| Object.defineProperty(exports, "navigate", { | |
| enumerable: true, | |
| get: function() { | |
| return navigate; | |
| } | |
| }); | |
| 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 _segmentcache = require("../segment-cache"); | |
| function navigate(url, currentCacheNode, currentFlightRouterState, nextUrl, shouldScroll) { | |
| 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 = // TODO: This is not the only place we read from the location, but we should | |
| // consider storing the current URL in the router state instead of reading | |
| // from the location object. In practice I don't think this matters much | |
| // since we keep them in sync anyway, but having two sources of truth can | |
| // lead to subtle bugs and race conditions. | |
| href === window.location.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 prefetchHead = route.head; | |
| const isPrefetchHeadPartial = route.isHeadPartial; | |
| const newCanonicalUrl = route.canonicalUrl; | |
| return navigateUsingPrefetchedRouteTree(now, url, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, newCanonicalUrl, shouldScroll, url.hash); | |
| } | |
| // There was no matching route tree in the cache. Let's see if we can | |
| // construct an "optimistic" route tree. | |
| 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 prefetchHead = optimisticRoute.head; | |
| const isPrefetchHeadPartial = optimisticRoute.isHeadPartial; | |
| const newCanonicalUrl = optimisticRoute.canonicalUrl; | |
| return navigateUsingPrefetchedRouteTree(now, url, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, newCanonicalUrl, shouldScroll, url.hash); | |
| } | |
| // There's no matching prefetch for this route in the cache. | |
| return { | |
| tag: _segmentcache.NavigationResultTag.Async, | |
| data: navigateDynamicallyWithNoPrefetch(now, url, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, shouldScroll, url.hash) | |
| }; | |
| } | |
| function navigateUsingPrefetchedRouteTree(now, url, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, canonicalUrl, shouldScroll, hash) { | |
| // 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 scrollableSegments = []; | |
| const task = (0, _pprnavigations.startPPRNavigation)(now, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, scrollableSegments); | |
| if (task !== null) { | |
| const dynamicRequestTree = task.dynamicRequestTree; | |
| if (dynamicRequestTree !== null) { | |
| const promiseForDynamicServerResponse = (0, _fetchserverresponse.fetchServerResponse)(new URL(canonicalUrl, url.origin), { | |
| flightRouterState: dynamicRequestTree, | |
| nextUrl | |
| }); | |
| (0, _pprnavigations.listenForDynamicRequest)(task, promiseForDynamicServerResponse); | |
| } else { | |
| // The prefetched tree does not contain dynamic holes — it's | |
| // fully static. We can skip the dynamic request. | |
| } | |
| return navigationTaskToResult(task, currentCacheNode, canonicalUrl, scrollableSegments, shouldScroll, hash); | |
| } | |
| // The server sent back an empty tree patch. There's nothing to update, except | |
| // possibly the URL. | |
| return { | |
| tag: _segmentcache.NavigationResultTag.NoOp, | |
| data: { | |
| canonicalUrl, | |
| shouldScroll | |
| } | |
| }; | |
| } | |
| function navigationTaskToResult(task, currentCacheNode, canonicalUrl, scrollableSegments, shouldScroll, hash) { | |
| const flightRouterState = task.route; | |
| if (flightRouterState === null) { | |
| // When no router state is provided, it signals that we should perform an | |
| // MPA navigation. | |
| return { | |
| tag: _segmentcache.NavigationResultTag.MPA, | |
| data: canonicalUrl | |
| }; | |
| } | |
| const newCacheNode = task.node; | |
| return { | |
| tag: _segmentcache.NavigationResultTag.Success, | |
| data: { | |
| flightRouterState, | |
| cacheNode: newCacheNode !== null ? newCacheNode : currentCacheNode, | |
| canonicalUrl, | |
| 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, route, tree.cacheKey); | |
| 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); | |
| // Since we don't know yet whether the segment is partial or fully | |
| // static, we must assume it's partial; we can't skip the | |
| // dynamic request. | |
| isPartial = true; | |
| 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))); | |
| return { | |
| flightRouterState: [ | |
| segment, | |
| childRouterStates, | |
| null, | |
| null, | |
| tree.isRootLayout | |
| ], | |
| seedData: [ | |
| segment, | |
| rsc, | |
| childSeedDatas, | |
| loading, | |
| isPartial | |
| ] | |
| }; | |
| } | |
| async function navigateDynamicallyWithNoPrefetch(now, url, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, shouldScroll, hash) { | |
| // 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. | |
| const promiseForDynamicServerResponse = (0, _fetchserverresponse.fetchServerResponse)(url, { | |
| flightRouterState: currentFlightRouterState, | |
| nextUrl | |
| }); | |
| const { flightData, canonicalUrl: canonicalUrlOverride } = await promiseForDynamicServerResponse; | |
| if (typeof flightData === 'string') { | |
| // This is an MPA navigation. | |
| const newUrl = flightData; | |
| return { | |
| tag: _segmentcache.NavigationResultTag.MPA, | |
| data: newUrl | |
| }; | |
| } | |
| // 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 prefetchFlightRouterState = simulatePrefetchTreeUsingDynamicTreePatch(currentFlightRouterState, flightData); | |
| // In our simulated prefetch payload, we pretend that there's no seed data | |
| // nor a prefetch head. | |
| const prefetchSeedData = null; | |
| const prefetchHead = null; | |
| const isPrefetchHeadPartial = true; | |
| const canonicalUrl = (0, _createhreffromurl.createHrefFromUrl)(canonicalUrlOverride ? canonicalUrlOverride : url); | |
| // Now we proceed exactly as we would for normal navigation. | |
| const scrollableSegments = []; | |
| const task = (0, _pprnavigations.startPPRNavigation)(now, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, scrollableSegments); | |
| if (task !== null) { | |
| // In this case, we've already sent the dynamic request, so we don't | |
| // actually use the request tree created by `startPPRNavigation`, | |
| // except to check if it contains dynamic holes. | |
| // | |
| // This is almost always true, but it could be false if all the segment data | |
| // was present in the cache, but the route tree was not. E.g. navigating | |
| // to a URL that was not prefetched but rewrites to a different URL | |
| // that was. | |
| const hasDynamicHoles = task.dynamicRequestTree !== null; | |
| if (hasDynamicHoles) { | |
| (0, _pprnavigations.listenForDynamicRequest)(task, promiseForDynamicServerResponse); | |
| } else { | |
| // The prefetched tree does not contain dynamic holes — it's | |
| // fully static. We don't need to process the server response further. | |
| } | |
| return navigationTaskToResult(task, currentCacheNode, canonicalUrl, scrollableSegments, shouldScroll, hash); | |
| } | |
| // The server sent back an empty tree patch. There's nothing to update, except | |
| // possibly the URL. | |
| return { | |
| tag: _segmentcache.NavigationResultTag.NoOp, | |
| data: { | |
| canonicalUrl, | |
| shouldScroll | |
| } | |
| }; | |
| } | |
| function simulatePrefetchTreeUsingDynamicTreePatch(currentTree, flightData) { | |
| // Takes the current FlightRouterState and applies the router state patch | |
| // received from the server, to create a full FlightRouterState tree that we | |
| // can pretend was returned by a prefetch. | |
| // | |
| // (It sounds similar to what applyRouterStatePatch does, but it doesn't need | |
| // to handle stuff like interception routes or diffing since that will be | |
| // handled later.) | |
| let baseTree = currentTree; | |
| for (const { segmentPath, tree: treePatch } of flightData){ | |
| // If the server sends us multiple tree patches, we only need to clone the | |
| // base tree when applying the first patch. After the first patch, we can | |
| // apply the remaining patches in place without copying. | |
| const canMutateInPlace = baseTree !== currentTree; | |
| baseTree = simulatePrefetchTreeUsingDynamicTreePatchImpl(baseTree, treePatch, segmentPath, canMutateInPlace, 0); | |
| } | |
| return baseTree; | |
| } | |
| function simulatePrefetchTreeUsingDynamicTreePatchImpl(baseRouterState, patch, segmentPath, canMutateInPlace, index) { | |
| if (index === segmentPath.length) { | |
| // We reached the part of the tree that we need to patch. | |
| return patch; | |
| } | |
| // 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: In the case of a fully dynamic request with no prefetch, we receive | |
| // the FlightRouterState patch in the same request as the dynamic data. | |
| // 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 baseChildren = baseRouterState[1]; | |
| const newChildren = {}; | |
| for(const parallelRouteKey in baseChildren){ | |
| if (parallelRouteKey === updatedParallelRouteKey) { | |
| const childBaseRouterState = baseChildren[parallelRouteKey]; | |
| newChildren[parallelRouteKey] = simulatePrefetchTreeUsingDynamicTreePatchImpl(childBaseRouterState, patch, segmentPath, canMutateInPlace, // Advance the index by two and keep cloning until we reach | |
| // the end of the segment path. | |
| index + 2); | |
| } else { | |
| // This child is not being patched. Copy it over as-is. | |
| newChildren[parallelRouteKey] = baseChildren[parallelRouteKey]; | |
| } | |
| } | |
| if (canMutateInPlace) { | |
| // We can mutate the base tree in place, because the base tree is already | |
| // a clone. | |
| baseRouterState[1] = newChildren; | |
| return baseRouterState; | |
| } | |
| // Clone all the fields except the children. | |
| // | |
| // 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. | |
| const clone = [ | |
| baseRouterState[0], | |
| newChildren | |
| ]; | |
| if (2 in baseRouterState) { | |
| clone[2] = baseRouterState[2]; | |
| } | |
| if (3 in baseRouterState) { | |
| clone[3] = baseRouterState[3]; | |
| } | |
| if (4 in baseRouterState) { | |
| clone[4] = baseRouterState[4]; | |
| } | |
| return clone; | |
| } | |
| 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 |