Spaces:
Running
Running
| ; | |
| Object.defineProperty(exports, "__esModule", { | |
| value: true | |
| }); | |
| 0 && (module.exports = { | |
| EntryStatus: null, | |
| attemptToFulfillDynamicSegmentFromBFCache: null, | |
| attemptToUpgradeSegmentFromBFCache: null, | |
| canNewFetchStrategyProvideMoreContent: null, | |
| convertReusedFlightRouterStateToRouteTree: null, | |
| convertRootFlightRouterStateToRouteTree: null, | |
| convertRouteTreeToFlightRouterState: null, | |
| createDetachedSegmentCacheEntry: null, | |
| createMetadataRouteTree: null, | |
| deprecated_requestOptimisticRouteCacheEntry: null, | |
| fetchInlinedSegmentsOnCacheMiss: null, | |
| fetchRouteOnCacheMiss: null, | |
| fetchSegmentOnCacheMiss: null, | |
| fetchSegmentPrefetchesUsingDynamicRequest: null, | |
| fulfillRouteCacheEntry: null, | |
| getCurrentRouteCacheVersion: null, | |
| getCurrentSegmentCacheVersion: null, | |
| getStaleAt: null, | |
| getStaleTimeMs: null, | |
| invalidateEntirePrefetchCache: null, | |
| invalidateRouteCacheEntries: null, | |
| invalidateSegmentCacheEntries: null, | |
| markRouteEntryAsDynamicRewrite: null, | |
| overwriteRevalidatingSegmentCacheEntry: null, | |
| pingInvalidationListeners: null, | |
| processRuntimePrefetchStream: null, | |
| readOrCreateRevalidatingSegmentEntry: null, | |
| readOrCreateRouteCacheEntry: null, | |
| readOrCreateSegmentCacheEntry: null, | |
| readRouteCacheEntry: null, | |
| readSegmentCacheEntry: null, | |
| stripIsPartialByte: null, | |
| upgradeToPendingSegment: null, | |
| upsertSegmentEntry: null, | |
| waitForSegmentCacheEntry: null, | |
| writeDynamicRenderResponseIntoCache: null, | |
| writeRouteIntoCache: null, | |
| writeStaticStageResponseIntoCache: null | |
| }); | |
| function _export(target, all) { | |
| for(var name in all)Object.defineProperty(target, name, { | |
| enumerable: true, | |
| get: all[name] | |
| }); | |
| } | |
| _export(exports, { | |
| EntryStatus: function() { | |
| return EntryStatus; | |
| }, | |
| attemptToFulfillDynamicSegmentFromBFCache: function() { | |
| return attemptToFulfillDynamicSegmentFromBFCache; | |
| }, | |
| attemptToUpgradeSegmentFromBFCache: function() { | |
| return attemptToUpgradeSegmentFromBFCache; | |
| }, | |
| canNewFetchStrategyProvideMoreContent: function() { | |
| return canNewFetchStrategyProvideMoreContent; | |
| }, | |
| convertReusedFlightRouterStateToRouteTree: function() { | |
| return convertReusedFlightRouterStateToRouteTree; | |
| }, | |
| convertRootFlightRouterStateToRouteTree: function() { | |
| return convertRootFlightRouterStateToRouteTree; | |
| }, | |
| convertRouteTreeToFlightRouterState: function() { | |
| return convertRouteTreeToFlightRouterState; | |
| }, | |
| createDetachedSegmentCacheEntry: function() { | |
| return createDetachedSegmentCacheEntry; | |
| }, | |
| createMetadataRouteTree: function() { | |
| return createMetadataRouteTree; | |
| }, | |
| deprecated_requestOptimisticRouteCacheEntry: function() { | |
| return deprecated_requestOptimisticRouteCacheEntry; | |
| }, | |
| fetchInlinedSegmentsOnCacheMiss: function() { | |
| return fetchInlinedSegmentsOnCacheMiss; | |
| }, | |
| fetchRouteOnCacheMiss: function() { | |
| return fetchRouteOnCacheMiss; | |
| }, | |
| fetchSegmentOnCacheMiss: function() { | |
| return fetchSegmentOnCacheMiss; | |
| }, | |
| fetchSegmentPrefetchesUsingDynamicRequest: function() { | |
| return fetchSegmentPrefetchesUsingDynamicRequest; | |
| }, | |
| fulfillRouteCacheEntry: function() { | |
| return fulfillRouteCacheEntry; | |
| }, | |
| getCurrentRouteCacheVersion: function() { | |
| return getCurrentRouteCacheVersion; | |
| }, | |
| getCurrentSegmentCacheVersion: function() { | |
| return getCurrentSegmentCacheVersion; | |
| }, | |
| getStaleAt: function() { | |
| return getStaleAt; | |
| }, | |
| getStaleTimeMs: function() { | |
| return getStaleTimeMs; | |
| }, | |
| invalidateEntirePrefetchCache: function() { | |
| return invalidateEntirePrefetchCache; | |
| }, | |
| invalidateRouteCacheEntries: function() { | |
| return invalidateRouteCacheEntries; | |
| }, | |
| invalidateSegmentCacheEntries: function() { | |
| return invalidateSegmentCacheEntries; | |
| }, | |
| markRouteEntryAsDynamicRewrite: function() { | |
| return markRouteEntryAsDynamicRewrite; | |
| }, | |
| overwriteRevalidatingSegmentCacheEntry: function() { | |
| return overwriteRevalidatingSegmentCacheEntry; | |
| }, | |
| pingInvalidationListeners: function() { | |
| return pingInvalidationListeners; | |
| }, | |
| processRuntimePrefetchStream: function() { | |
| return processRuntimePrefetchStream; | |
| }, | |
| readOrCreateRevalidatingSegmentEntry: function() { | |
| return readOrCreateRevalidatingSegmentEntry; | |
| }, | |
| readOrCreateRouteCacheEntry: function() { | |
| return readOrCreateRouteCacheEntry; | |
| }, | |
| readOrCreateSegmentCacheEntry: function() { | |
| return readOrCreateSegmentCacheEntry; | |
| }, | |
| readRouteCacheEntry: function() { | |
| return readRouteCacheEntry; | |
| }, | |
| readSegmentCacheEntry: function() { | |
| return readSegmentCacheEntry; | |
| }, | |
| stripIsPartialByte: function() { | |
| return stripIsPartialByte; | |
| }, | |
| upgradeToPendingSegment: function() { | |
| return upgradeToPendingSegment; | |
| }, | |
| upsertSegmentEntry: function() { | |
| return upsertSegmentEntry; | |
| }, | |
| waitForSegmentCacheEntry: function() { | |
| return waitForSegmentCacheEntry; | |
| }, | |
| writeDynamicRenderResponseIntoCache: function() { | |
| return writeDynamicRenderResponseIntoCache; | |
| }, | |
| writeRouteIntoCache: function() { | |
| return writeRouteIntoCache; | |
| }, | |
| writeStaticStageResponseIntoCache: function() { | |
| return writeStaticStageResponseIntoCache; | |
| } | |
| }); | |
| const _varyparamsdecoding = require("../../../shared/lib/segment-cache/vary-params-decoding"); | |
| const _approuterheaders = require("../app-router-headers"); | |
| const _fetchserverresponse = require("../router-reducer/fetch-server-response"); | |
| const _scheduler = require("./scheduler"); | |
| const _varypath = require("./vary-path"); | |
| const _createhreffromurl = require("../router-reducer/create-href-from-url"); | |
| const _cachekey = require("./cache-key"); | |
| const _routeparams = require("../../route-params"); | |
| const _cachemap = require("./cache-map"); | |
| const _segmentvalueencoding = require("../../../shared/lib/segment-cache/segment-value-encoding"); | |
| const _flightdatahelpers = require("../../flight-data-helpers"); | |
| const _navigatereducer = require("../router-reducer/reducers/navigate-reducer"); | |
| const _links = require("../links"); | |
| const _segment = require("../../../shared/lib/segment"); | |
| const _types = require("./types"); | |
| const _promisewithresolvers = require("../../../shared/lib/promise-with-resolvers"); | |
| const _bfcache = require("./bfcache"); | |
| const _optimisticroutes = require("./optimistic-routes"); | |
| const _navigation = require("./navigation"); | |
| const _navigationbuildid = require("../../navigation-build-id"); | |
| const _constants = require("../../../lib/constants"); | |
| function getStaleTimeMs(staleTimeSeconds) { | |
| return Math.max(staleTimeSeconds, 30) * 1000; | |
| } | |
| var EntryStatus = /*#__PURE__*/ function(EntryStatus) { | |
| EntryStatus[EntryStatus["Empty"] = 0] = "Empty"; | |
| EntryStatus[EntryStatus["Pending"] = 1] = "Pending"; | |
| EntryStatus[EntryStatus["Fulfilled"] = 2] = "Fulfilled"; | |
| EntryStatus[EntryStatus["Rejected"] = 3] = "Rejected"; | |
| return EntryStatus; | |
| }({}); | |
| const isOutputExportMode = process.env.NODE_ENV === 'production' && process.env.__NEXT_CONFIG_OUTPUT === 'export'; | |
| const MetadataOnlyRequestTree = [ | |
| '', | |
| {}, | |
| null, | |
| 'metadata-only' | |
| ]; | |
| let routeCacheMap = (0, _cachemap.createCacheMap)(); | |
| let segmentCacheMap = (0, _cachemap.createCacheMap)(); | |
| // All invalidation listeners for the whole cache are tracked in single set. | |
| // Since we don't yet support tag or path-based invalidation, there's no point | |
| // tracking them any more granularly than this. Once we add granular | |
| // invalidation, that may change, though generally the model is to just notify | |
| // the listeners and allow the caller to poll the prefetch cache with a new | |
| // prefetch task if desired. | |
| let invalidationListeners = null; | |
| // Incrementing counters used to track cache invalidations. Route and segment | |
| // caches have separate versions so they can be invalidated independently. | |
| // Invalidation does not eagerly evict anything from the cache; entries are | |
| // lazily evicted when read. | |
| let currentRouteCacheVersion = 0; | |
| let currentSegmentCacheVersion = 0; | |
| function getCurrentRouteCacheVersion() { | |
| return currentRouteCacheVersion; | |
| } | |
| function getCurrentSegmentCacheVersion() { | |
| return currentSegmentCacheVersion; | |
| } | |
| function invalidateEntirePrefetchCache(nextUrl, tree) { | |
| currentRouteCacheVersion++; | |
| currentSegmentCacheVersion++; | |
| (0, _links.pingVisibleLinks)(nextUrl, tree); | |
| pingInvalidationListeners(nextUrl, tree); | |
| } | |
| function invalidateRouteCacheEntries(nextUrl, tree) { | |
| currentRouteCacheVersion++; | |
| (0, _links.pingVisibleLinks)(nextUrl, tree); | |
| pingInvalidationListeners(nextUrl, tree); | |
| } | |
| function invalidateSegmentCacheEntries(nextUrl, tree) { | |
| currentSegmentCacheVersion++; | |
| (0, _links.pingVisibleLinks)(nextUrl, tree); | |
| pingInvalidationListeners(nextUrl, tree); | |
| } | |
| function attachInvalidationListener(task) { | |
| // This function is called whenever a prefetch task reads a cache entry. If | |
| // the task has an onInvalidate function associated with it — i.e. the one | |
| // optionally passed to router.prefetch(onInvalidate) — then we attach that | |
| // listener to the every cache entry that the task reads. Then, if an entry | |
| // is invalidated, we call the function. | |
| if (task.onInvalidate !== null) { | |
| if (invalidationListeners === null) { | |
| invalidationListeners = new Set([ | |
| task | |
| ]); | |
| } else { | |
| invalidationListeners.add(task); | |
| } | |
| } | |
| } | |
| function notifyInvalidationListener(task) { | |
| const onInvalidate = task.onInvalidate; | |
| if (onInvalidate !== null) { | |
| // Clear the callback from the task object to guarantee it's not called more | |
| // than once. | |
| task.onInvalidate = null; | |
| // This is a user-space function, so we must wrap in try/catch. | |
| try { | |
| onInvalidate(); | |
| } catch (error) { | |
| if (typeof reportError === 'function') { | |
| reportError(error); | |
| } else { | |
| console.error(error); | |
| } | |
| } | |
| } | |
| } | |
| function pingInvalidationListeners(nextUrl, tree) { | |
| // The rough equivalent of pingVisibleLinks, but for onInvalidate callbacks. | |
| // This is called when the Next-Url or the base tree changes, since those | |
| // may affect the result of a prefetch task. It's also called after a | |
| // cache invalidation. | |
| if (invalidationListeners !== null) { | |
| const tasks = invalidationListeners; | |
| invalidationListeners = null; | |
| for (const task of tasks){ | |
| if ((0, _scheduler.isPrefetchTaskDirty)(task, nextUrl, tree)) { | |
| notifyInvalidationListener(task); | |
| } | |
| } | |
| } | |
| } | |
| function readRouteCacheEntry(now, key) { | |
| const varyPath = (0, _varypath.getRouteVaryPath)(key.pathname, key.search, key.nextUrl); | |
| const isRevalidation = false; | |
| const existingEntry = (0, _cachemap.getFromCacheMap)(now, getCurrentRouteCacheVersion(), routeCacheMap, varyPath, isRevalidation); | |
| if (existingEntry !== null) { | |
| return existingEntry; | |
| } | |
| // No cache hit. Attempt to construct from template using the new | |
| // optimistic routing mechanism (pattern-based matching). | |
| if (process.env.__NEXT_OPTIMISTIC_ROUTING) { | |
| return (0, _optimisticroutes.matchKnownRoute)(key.pathname, key.search); | |
| } | |
| return null; | |
| } | |
| function readSegmentCacheEntry(now, varyPath) { | |
| const isRevalidation = false; | |
| return (0, _cachemap.getFromCacheMap)(now, getCurrentSegmentCacheVersion(), segmentCacheMap, varyPath, isRevalidation); | |
| } | |
| function readRevalidatingSegmentCacheEntry(now, varyPath) { | |
| const isRevalidation = true; | |
| return (0, _cachemap.getFromCacheMap)(now, getCurrentSegmentCacheVersion(), segmentCacheMap, varyPath, isRevalidation); | |
| } | |
| function waitForSegmentCacheEntry(pendingEntry) { | |
| // Because the entry is pending, there's already a in-progress request. | |
| // Attach a promise to the entry that will resolve when the server responds. | |
| let promiseWithResolvers = pendingEntry.promise; | |
| if (promiseWithResolvers === null) { | |
| promiseWithResolvers = pendingEntry.promise = (0, _promisewithresolvers.createPromiseWithResolvers)(); | |
| } else { | |
| // There's already a promise we can use | |
| } | |
| return promiseWithResolvers.promise; | |
| } | |
| function createDetachedRouteCacheEntry() { | |
| return { | |
| canonicalUrl: null, | |
| status: 0, | |
| blockedTasks: null, | |
| tree: null, | |
| metadata: null, | |
| // This is initialized to true because we don't know yet whether the route | |
| // could be intercepted. It's only set to false once we receive a response | |
| // from the server. | |
| couldBeIntercepted: true, | |
| // Similarly, we don't yet know if the route supports PPR. | |
| supportsPerSegmentPrefetching: false, | |
| renderedSearch: null, | |
| // Map-related fields | |
| ref: null, | |
| size: 0, | |
| // Since this is an empty entry, there's no reason to ever evict it. It will | |
| // be updated when the data is populated. | |
| staleAt: Infinity, | |
| version: getCurrentRouteCacheVersion() | |
| }; | |
| } | |
| function readOrCreateRouteCacheEntry(now, task, key) { | |
| attachInvalidationListener(task); | |
| const existingEntry = readRouteCacheEntry(now, key); | |
| if (existingEntry !== null) { | |
| return existingEntry; | |
| } | |
| // Create a pending entry and add it to the cache. | |
| const pendingEntry = createDetachedRouteCacheEntry(); | |
| const varyPath = (0, _varypath.getRouteVaryPath)(key.pathname, key.search, key.nextUrl); | |
| const isRevalidation = false; | |
| (0, _cachemap.setInCacheMap)(routeCacheMap, varyPath, pendingEntry, isRevalidation); | |
| return pendingEntry; | |
| } | |
| function deprecated_requestOptimisticRouteCacheEntry(now, requestedUrl, nextUrl) { | |
| // This function is called during a navigation when there was no matching | |
| // route tree in the prefetch cache. Before de-opting to a blocking, | |
| // unprefetched navigation, we will first attempt to construct an "optimistic" | |
| // route tree by checking the cache for similar routes. | |
| // | |
| // Check if there's a route with the same pathname, but with different | |
| // search params. We can then base our optimistic route tree on this entry. | |
| // | |
| // Conceptually, we are simulating what would happen if we did perform a | |
| // prefetch the requested URL, under the assumption that the server will | |
| // not redirect or rewrite the request in a different manner than the | |
| // base route tree. This assumption might not hold, in which case we'll have | |
| // to recover when we perform the dynamic navigation request. However, this | |
| // is what would happen if a route were dynamically rewritten/redirected | |
| // in between the prefetch and the navigation. So the logic needs to exist | |
| // to handle this case regardless. | |
| // Look for a route with the same pathname, but with an empty search string. | |
| // TODO: There's nothing inherently special about the empty search string; | |
| // it's chosen somewhat arbitrarily, with the rationale that it's the most | |
| // likely one to exist. But we should update this to match _any_ search | |
| // string. The plan is to generalize this logic alongside other improvements | |
| // related to "fallback" cache entries. | |
| const requestedSearch = requestedUrl.search; | |
| if (requestedSearch === '') { | |
| // The caller would have already checked if a route with an empty search | |
| // string is in the cache. So we can bail out here. | |
| return null; | |
| } | |
| const urlWithoutSearchParams = new URL(requestedUrl); | |
| urlWithoutSearchParams.search = ''; | |
| const routeWithNoSearchParams = readRouteCacheEntry(now, (0, _cachekey.createCacheKey)(urlWithoutSearchParams.href, nextUrl)); | |
| if (routeWithNoSearchParams === null || routeWithNoSearchParams.status !== 2) { | |
| // Bail out of constructing an optimistic route tree. This will result in | |
| // a blocking, unprefetched navigation. | |
| return null; | |
| } | |
| // Now we have a base route tree we can "patch" with our optimistic values. | |
| // Optimistically assume that redirects for the requested pathname do | |
| // not vary on the search string. Therefore, if the base route was | |
| // redirected to a different search string, then the optimistic route | |
| // should be redirected to the same search string. Otherwise, we use | |
| // the requested search string. | |
| const canonicalUrlForRouteWithNoSearchParams = new URL(routeWithNoSearchParams.canonicalUrl, requestedUrl.origin); | |
| const optimisticCanonicalSearch = canonicalUrlForRouteWithNoSearchParams.search !== '' ? canonicalUrlForRouteWithNoSearchParams.search : requestedSearch; | |
| // Similarly, optimistically assume that rewrites for the requested | |
| // pathname do not vary on the search string. Therefore, if the base | |
| // route was rewritten to a different search string, then the optimistic | |
| // route should be rewritten to the same search string. Otherwise, we use | |
| // the requested search string. | |
| const optimisticRenderedSearch = routeWithNoSearchParams.renderedSearch !== '' ? routeWithNoSearchParams.renderedSearch : requestedSearch; | |
| const optimisticUrl = new URL(routeWithNoSearchParams.canonicalUrl, location.origin); | |
| optimisticUrl.search = optimisticCanonicalSearch; | |
| const optimisticCanonicalUrl = (0, _createhreffromurl.createHrefFromUrl)(optimisticUrl); | |
| const optimisticRouteTree = deprecated_createOptimisticRouteTree(routeWithNoSearchParams.tree, optimisticRenderedSearch); | |
| const optimisticMetadataTree = deprecated_createOptimisticRouteTree(routeWithNoSearchParams.metadata, optimisticRenderedSearch); | |
| // Clone the base route tree, and override the relevant fields with our | |
| // optimistic values. | |
| const optimisticEntry = { | |
| canonicalUrl: optimisticCanonicalUrl, | |
| status: 2, | |
| // This isn't cloned because it's instance-specific | |
| blockedTasks: null, | |
| tree: optimisticRouteTree, | |
| metadata: optimisticMetadataTree, | |
| couldBeIntercepted: routeWithNoSearchParams.couldBeIntercepted, | |
| supportsPerSegmentPrefetching: routeWithNoSearchParams.supportsPerSegmentPrefetching, | |
| hasDynamicRewrite: routeWithNoSearchParams.hasDynamicRewrite, | |
| // Override the rendered search with the optimistic value. | |
| renderedSearch: optimisticRenderedSearch, | |
| // Map-related fields | |
| ref: null, | |
| size: 0, | |
| staleAt: routeWithNoSearchParams.staleAt, | |
| version: routeWithNoSearchParams.version | |
| }; | |
| // Do not insert this entry into the cache. It only exists so we can | |
| // perform the current navigation. Just return it to the caller. | |
| return optimisticEntry; | |
| } | |
| function deprecated_createOptimisticRouteTree(tree, newRenderedSearch) { | |
| // Create a new route tree that identical to the original one except for | |
| // the rendered search string, which is contained in the vary path. | |
| let clonedSlots = null; | |
| const originalSlots = tree.slots; | |
| if (originalSlots !== null) { | |
| clonedSlots = {}; | |
| for(const parallelRouteKey in originalSlots){ | |
| const childTree = originalSlots[parallelRouteKey]; | |
| clonedSlots[parallelRouteKey] = deprecated_createOptimisticRouteTree(childTree, newRenderedSearch); | |
| } | |
| } | |
| // We only need to clone the vary path if the route is a page. | |
| if (tree.isPage) { | |
| return { | |
| requestKey: tree.requestKey, | |
| segment: tree.segment, | |
| refreshState: tree.refreshState, | |
| varyPath: (0, _varypath.clonePageVaryPathWithNewSearchParams)(tree.varyPath, newRenderedSearch), | |
| isPage: true, | |
| slots: clonedSlots, | |
| prefetchHints: tree.prefetchHints | |
| }; | |
| } | |
| return { | |
| requestKey: tree.requestKey, | |
| segment: tree.segment, | |
| refreshState: tree.refreshState, | |
| varyPath: tree.varyPath, | |
| isPage: false, | |
| slots: clonedSlots, | |
| prefetchHints: tree.prefetchHints | |
| }; | |
| } | |
| function readOrCreateSegmentCacheEntry(now, fetchStrategy, tree) { | |
| const existingEntry = readSegmentCacheEntry(now, tree.varyPath); | |
| if (existingEntry !== null) { | |
| return existingEntry; | |
| } | |
| // Create a pending entry and add it to the cache. The stale time is set to a | |
| // default value; the actual stale time will be set when the entry is | |
| // fulfilled with data from the server response. | |
| const varyPathForRequest = (0, _varypath.getSegmentVaryPathForRequest)(fetchStrategy, tree); | |
| const pendingEntry = createDetachedSegmentCacheEntry(now); | |
| const isRevalidation = false; | |
| (0, _cachemap.setInCacheMap)(segmentCacheMap, varyPathForRequest, pendingEntry, isRevalidation); | |
| return pendingEntry; | |
| } | |
| function readOrCreateRevalidatingSegmentEntry(now, fetchStrategy, tree) { | |
| // This function is called when we've already confirmed that a particular | |
| // segment is cached, but we want to perform another request anyway in case it | |
| // returns more complete and/or fresher data than we already have. The logic | |
| // for deciding whether to replace the existing entry is handled elsewhere; | |
| // this function just handles retrieving a cache entry that we can use to | |
| // track the revalidation. | |
| // | |
| // The reason revalidations are stored in the cache is because we need to be | |
| // able to dedupe multiple revalidation requests. The reason they have to be | |
| // handled specially is because we shouldn't overwrite a "normal" entry if | |
| // one exists at the same keypath. So, for each internal cache location, there | |
| // is a special "revalidation" slot that is used solely for this purpose. | |
| // | |
| // You can think of it as if all the revalidation entries were stored in a | |
| // separate cache map from the canonical entries, and then transfered to the | |
| // canonical cache map once the request is complete — this isn't how it's | |
| // actually implemented, since it's more efficient to store them in the same | |
| // data structure as the normal entries, but that's how it's modeled | |
| // conceptually. | |
| // TODO: Once we implement Fallback behavior for params, where an entry is | |
| // re-keyed based on response information, we'll need to account for the | |
| // possibility that the keypath of the previous entry is more generic than | |
| // the keypath of the revalidating entry. In other words, the server could | |
| // return a less generic entry upon revalidation. For now, though, this isn't | |
| // a concern because the keypath is based solely on the prefetch strategy, | |
| // not on data contained in the response. | |
| const existingEntry = readRevalidatingSegmentCacheEntry(now, tree.varyPath); | |
| if (existingEntry !== null) { | |
| return existingEntry; | |
| } | |
| // Create a pending entry and add it to the cache. The stale time is set to a | |
| // default value; the actual stale time will be set when the entry is | |
| // fulfilled with data from the server response. | |
| const varyPathForRequest = (0, _varypath.getSegmentVaryPathForRequest)(fetchStrategy, tree); | |
| const pendingEntry = createDetachedSegmentCacheEntry(now); | |
| const isRevalidation = true; | |
| (0, _cachemap.setInCacheMap)(segmentCacheMap, varyPathForRequest, pendingEntry, isRevalidation); | |
| return pendingEntry; | |
| } | |
| function overwriteRevalidatingSegmentCacheEntry(now, fetchStrategy, tree) { | |
| // This function is called when we've already decided to replace an existing | |
| // revalidation entry. Create a new entry and write it into the cache, | |
| // overwriting the previous value. The stale time is set to a default value; | |
| // the actual stale time will be set when the entry is fulfilled with data | |
| // from the server response. | |
| const varyPathForRequest = (0, _varypath.getSegmentVaryPathForRequest)(fetchStrategy, tree); | |
| const pendingEntry = createDetachedSegmentCacheEntry(now); | |
| const isRevalidation = true; | |
| (0, _cachemap.setInCacheMap)(segmentCacheMap, varyPathForRequest, pendingEntry, isRevalidation); | |
| return pendingEntry; | |
| } | |
| function upsertSegmentEntry(now, varyPath, candidateEntry) { | |
| // We have a new entry that has not yet been inserted into the cache. Before | |
| // we do so, we need to confirm whether it takes precedence over the existing | |
| // entry (if one exists). | |
| // TODO: We should not upsert an entry if its key was invalidated in the time | |
| // since the request was made. We can do that by passing the "owner" entry to | |
| // this function and confirming it's the same as `existingEntry`. | |
| if ((0, _cachemap.isValueExpired)(now, getCurrentSegmentCacheVersion(), candidateEntry)) { | |
| // The entry is expired. We cannot upsert it. | |
| return null; | |
| } | |
| const existingEntry = readSegmentCacheEntry(now, varyPath); | |
| if (existingEntry !== null) { | |
| // Don't replace a more specific segment with a less-specific one. A case where this | |
| // might happen is if the existing segment was fetched via | |
| // `<Link prefetch={true}>`. | |
| if (// We fetched the new segment using a different, less specific fetch strategy | |
| // than the segment we already have in the cache, so it can't have more content. | |
| candidateEntry.fetchStrategy !== existingEntry.fetchStrategy && !canNewFetchStrategyProvideMoreContent(existingEntry.fetchStrategy, candidateEntry.fetchStrategy) || // The existing entry isn't partial, but the new one is. | |
| // (TODO: can this be true if `candidateEntry.fetchStrategy >= existingEntry.fetchStrategy`?) | |
| !existingEntry.isPartial && candidateEntry.isPartial) { | |
| // We're going to leave revalidating entry in the cache so that it doesn't | |
| // get revalidated again unnecessarily. Downgrade the Fulfilled entry to | |
| // Rejected and null out the data so it can be garbage collected. We leave | |
| // `staleAt` intact to prevent subsequent revalidation attempts only until | |
| // the entry expires. | |
| const rejectedEntry = candidateEntry; | |
| rejectedEntry.status = 3; | |
| rejectedEntry.rsc = null; | |
| return null; | |
| } | |
| // Evict the existing entry from the cache. | |
| (0, _cachemap.deleteFromCacheMap)(existingEntry); | |
| } | |
| const isRevalidation = false; | |
| (0, _cachemap.setInCacheMap)(segmentCacheMap, varyPath, candidateEntry, isRevalidation); | |
| return candidateEntry; | |
| } | |
| function createDetachedSegmentCacheEntry(now) { | |
| // Default stale time for pending segment cache entries. The actual stale time | |
| // is set when the entry is fulfilled with data from the server response. | |
| const staleAt = now + 30 * 1000; | |
| const emptyEntry = { | |
| status: 0, | |
| // Default to assuming the fetch strategy will be PPR. This will be updated | |
| // when a fetch is actually initiated. | |
| fetchStrategy: _types.FetchStrategy.PPR, | |
| rsc: null, | |
| isPartial: true, | |
| promise: null, | |
| // Map-related fields | |
| ref: null, | |
| size: 0, | |
| staleAt, | |
| version: 0 | |
| }; | |
| return emptyEntry; | |
| } | |
| function upgradeToPendingSegment(emptyEntry, fetchStrategy) { | |
| const pendingEntry = emptyEntry; | |
| pendingEntry.status = 1; | |
| pendingEntry.fetchStrategy = fetchStrategy; | |
| if (fetchStrategy === _types.FetchStrategy.Full) { | |
| // We can assume the response will contain the full segment data. Set this | |
| // to false so we know it's OK to omit this segment from any navigation | |
| // requests that may happen while the data is still pending. | |
| pendingEntry.isPartial = false; | |
| } | |
| // Set the version here, since this is right before the request is initiated. | |
| // The next time the segment cache version is incremented, the entry will | |
| // effectively be evicted. This happens before initiating the request, rather | |
| // than when receiving the response, because it's guaranteed to happen | |
| // before the data is read on the server. | |
| pendingEntry.version = getCurrentSegmentCacheVersion(); | |
| return pendingEntry; | |
| } | |
| function attemptToFulfillDynamicSegmentFromBFCache(now, segment, tree) { | |
| // Attempts to fulfill an empty segment cache entry using data from the | |
| // bfcache. This is only valid during a Full prefetch (i.e. one that includes | |
| // dynamic data), because the bfcache stores data from navigations which | |
| // always include dynamic data. | |
| // We always use the canonical vary path when checking the bfcache. This is | |
| // the same operation we'd use to access the cache during a | |
| // regular navigation. | |
| const varyPath = tree.varyPath; | |
| // Read from the BFCache without expiring it (pass -1). We check freshness | |
| // ourselves using navigatedAt, because the BFCache's staleAt may have been | |
| // overridden by a per-page unstable_dynamicStaleTime and can't be used to | |
| // derive the original request time. | |
| const bfcacheEntry = (0, _bfcache.readFromBFCache)(varyPath); | |
| if (bfcacheEntry !== null) { | |
| // The stale time for dynamic prefetches (default: 5 mins) is different | |
| // from the stale time for regular navigations (default: 0 secs). Use | |
| // navigatedAt to compute the correct expiry for prefetch purposes. | |
| const dynamicPrefetchStaleAt = bfcacheEntry.navigatedAt + _navigatereducer.STATIC_STALETIME_MS; | |
| if (now > dynamicPrefetchStaleAt) { | |
| return null; | |
| } | |
| const pendingSegment = upgradeToPendingSegment(segment, _types.FetchStrategy.Full); | |
| const isPartial = false; | |
| return fulfillSegmentCacheEntry(pendingSegment, bfcacheEntry.rsc, dynamicPrefetchStaleAt, isPartial); | |
| } | |
| return null; | |
| } | |
| function attemptToUpgradeSegmentFromBFCache(now, tree) { | |
| const varyPath = tree.varyPath; | |
| const bfcacheEntry = (0, _bfcache.readFromBFCache)(varyPath); | |
| if (bfcacheEntry !== null) { | |
| const dynamicPrefetchStaleAt = bfcacheEntry.navigatedAt + _navigatereducer.STATIC_STALETIME_MS; | |
| if (now > dynamicPrefetchStaleAt) { | |
| return null; | |
| } | |
| const pendingSegment = upgradeToPendingSegment(createDetachedSegmentCacheEntry(now), _types.FetchStrategy.Full); | |
| const isPartial = false; | |
| const newEntry = fulfillSegmentCacheEntry(pendingSegment, bfcacheEntry.rsc, dynamicPrefetchStaleAt, isPartial); | |
| const segmentVaryPath = (0, _varypath.getSegmentVaryPathForRequest)(_types.FetchStrategy.Full, tree); | |
| const upserted = upsertSegmentEntry(now, segmentVaryPath, newEntry); | |
| if (upserted !== null && upserted.status === 2) { | |
| return upserted; | |
| } | |
| } | |
| return null; | |
| } | |
| function pingBlockedTasks(entry) { | |
| const blockedTasks = entry.blockedTasks; | |
| if (blockedTasks !== null) { | |
| for (const task of blockedTasks){ | |
| (0, _scheduler.pingPrefetchTask)(task); | |
| } | |
| entry.blockedTasks = null; | |
| } | |
| } | |
| function createMetadataRouteTree(metadataVaryPath) { | |
| // The Head is not actually part of the route tree, but other than that, it's | |
| // fetched and cached like a segment. Some functions expect a RouteTree | |
| // object, so rather than fork the logic in all those places, we use this | |
| // "fake" one. | |
| const metadata = { | |
| requestKey: _segmentvalueencoding.HEAD_REQUEST_KEY, | |
| segment: _segmentvalueencoding.HEAD_REQUEST_KEY, | |
| refreshState: null, | |
| varyPath: metadataVaryPath, | |
| // The metadata isn't really a "page" (though it isn't really a "segment" | |
| // either) but for the purposes of how this field is used, it behaves like | |
| // one. If this logic ever gets more complex we can change this to an enum. | |
| isPage: true, | |
| slots: null, | |
| prefetchHints: 0 | |
| }; | |
| return metadata; | |
| } | |
| function fulfillRouteCacheEntry(now, entry, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching) { | |
| // Get the rendered search from the vary path | |
| const renderedSearch = (0, _varypath.getRenderedSearchFromVaryPath)(metadataVaryPath) ?? ''; | |
| const fulfilledEntry = entry; | |
| fulfilledEntry.status = 2; | |
| fulfilledEntry.tree = tree; | |
| fulfilledEntry.metadata = createMetadataRouteTree(metadataVaryPath); | |
| // Route structure is essentially static — it only changes on deploy. | |
| // Always use the static stale time. | |
| // NOTE: An exception is rewrites/redirects in middleware or proxy, which can | |
| // change routes dynamically. We have other strategies for handling those. | |
| fulfilledEntry.staleAt = now + _navigatereducer.STATIC_STALETIME_MS; | |
| fulfilledEntry.couldBeIntercepted = couldBeIntercepted; | |
| fulfilledEntry.canonicalUrl = canonicalUrl; | |
| fulfilledEntry.renderedSearch = renderedSearch; | |
| fulfilledEntry.supportsPerSegmentPrefetching = supportsPerSegmentPrefetching; | |
| fulfilledEntry.hasDynamicRewrite = false; | |
| pingBlockedTasks(entry); | |
| return fulfilledEntry; | |
| } | |
| function writeRouteIntoCache(now, pathname, nextUrl, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching) { | |
| const pendingEntry = createDetachedRouteCacheEntry(); | |
| const fulfilledEntry = fulfillRouteCacheEntry(now, pendingEntry, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching); | |
| const renderedSearch = fulfilledEntry.renderedSearch; | |
| const varyPath = (0, _varypath.getFulfilledRouteVaryPath)(pathname, renderedSearch, nextUrl, couldBeIntercepted); | |
| const isRevalidation = false; | |
| (0, _cachemap.setInCacheMap)(routeCacheMap, varyPath, fulfilledEntry, isRevalidation); | |
| return fulfilledEntry; | |
| } | |
| function markRouteEntryAsDynamicRewrite(entry) { | |
| entry.hasDynamicRewrite = true; | |
| // Note: The caller is responsible for also calling invalidateRouteCacheEntries | |
| // to invalidate other entries that may have been derived from this template | |
| // before we knew it had a dynamic rewrite. | |
| } | |
| function fulfillSegmentCacheEntry(segmentCacheEntry, rsc, staleAt, isPartial) { | |
| const fulfilledEntry = segmentCacheEntry; | |
| fulfilledEntry.status = 2; | |
| fulfilledEntry.rsc = rsc; | |
| fulfilledEntry.staleAt = staleAt; | |
| fulfilledEntry.isPartial = isPartial; | |
| // Resolve any listeners that were waiting for this data. | |
| if (segmentCacheEntry.promise !== null) { | |
| segmentCacheEntry.promise.resolve(fulfilledEntry); | |
| // Free the promise for garbage collection. | |
| fulfilledEntry.promise = null; | |
| } | |
| return fulfilledEntry; | |
| } | |
| function rejectRouteCacheEntry(entry, staleAt) { | |
| const rejectedEntry = entry; | |
| rejectedEntry.status = 3; | |
| rejectedEntry.staleAt = staleAt; | |
| pingBlockedTasks(entry); | |
| } | |
| function rejectSegmentCacheEntry(entry, staleAt) { | |
| const rejectedEntry = entry; | |
| rejectedEntry.status = 3; | |
| rejectedEntry.staleAt = staleAt; | |
| if (entry.promise !== null) { | |
| // NOTE: We don't currently propagate the reason the prefetch was canceled | |
| // but we could by accepting a `reason` argument. | |
| entry.promise.resolve(null); | |
| entry.promise = null; | |
| } | |
| } | |
| function convertRootTreePrefetchToRouteTree(rootTree, renderedPathname, renderedSearch, acc) { | |
| // Remove trailing and leading slashes | |
| const pathnameParts = renderedPathname.split('/').filter((p)=>p !== ''); | |
| const index = 0; | |
| const rootSegment = _segmentvalueencoding.ROOT_SEGMENT_REQUEST_KEY; | |
| return convertTreePrefetchToRouteTree(rootTree.tree, rootSegment, null, _segmentvalueencoding.ROOT_SEGMENT_REQUEST_KEY, pathnameParts, index, renderedSearch, acc); | |
| } | |
| function convertTreePrefetchToRouteTree(prefetch, segment, partialVaryPath, requestKey, pathnameParts, pathnamePartsIndex, renderedSearch, acc) { | |
| // Converts the route tree sent by the server into the format used by the | |
| // cache. The cached version of the tree includes additional fields, such as a | |
| // cache key for each segment. Since this is frequently accessed, we compute | |
| // it once instead of on every access. This same cache key is also used to | |
| // request the segment from the server. | |
| let slots = null; | |
| let isPage; | |
| let varyPath; | |
| const prefetchSlots = prefetch.slots; | |
| if (prefetchSlots !== null) { | |
| isPage = false; | |
| varyPath = (0, _varypath.finalizeLayoutVaryPath)(requestKey, partialVaryPath); | |
| slots = {}; | |
| for(let parallelRouteKey in prefetchSlots){ | |
| const childPrefetch = prefetchSlots[parallelRouteKey]; | |
| const childSegmentName = childPrefetch.name; | |
| const childParam = childPrefetch.param; | |
| let childDoesAppearInURL; | |
| let childSegment; | |
| let childPartialVaryPath; | |
| if (childParam !== null) { | |
| // This segment is parameterized. Get the param from the pathname. | |
| const childParamValue = (0, _routeparams.parseDynamicParamFromURLPart)(childParam.type, pathnameParts, pathnamePartsIndex); | |
| // Assign a cache key to the segment, based on the param value. In the | |
| // pre-Segment Cache implementation, the server computes this and sends | |
| // it in the body of the response. In the Segment Cache implementation, | |
| // the server sends an empty string and we fill it in here. | |
| // TODO: We're intentionally not adding the search param to page | |
| // segments here; it's tracked separately and added back during a read. | |
| // This would clearer if we waited to construct the segment until it's | |
| // read from the cache, since that's effectively what we're | |
| // doing anyway. | |
| const childParamKey = // The server omits this field from the prefetch response when | |
| // cacheComponents is enabled. | |
| childParam.key !== null ? childParam.key : (0, _routeparams.getCacheKeyForDynamicParam)(childParamValue, ''); | |
| childPartialVaryPath = (0, _varypath.appendLayoutVaryPath)(partialVaryPath, childParamKey, childSegmentName); | |
| childSegment = [ | |
| childSegmentName, | |
| childParamKey, | |
| childParam.type, | |
| childParam.siblings | |
| ]; | |
| childDoesAppearInURL = true; | |
| } else { | |
| // This segment does not have a param. Inherit the partial vary path of | |
| // the parent. | |
| childPartialVaryPath = partialVaryPath; | |
| childSegment = childSegmentName; | |
| childDoesAppearInURL = (0, _routeparams.doesStaticSegmentAppearInURL)(childSegmentName); | |
| } | |
| // Only increment the index if the segment appears in the URL. If it's a | |
| // "virtual" segment, like a route group, it remains the same. | |
| const childPathnamePartsIndex = childDoesAppearInURL ? pathnamePartsIndex + 1 : pathnamePartsIndex; | |
| const childRequestKeyPart = (0, _segmentvalueencoding.createSegmentRequestKeyPart)(childSegment); | |
| const childRequestKey = (0, _segmentvalueencoding.appendSegmentRequestKeyPart)(requestKey, parallelRouteKey, childRequestKeyPart); | |
| slots[parallelRouteKey] = convertTreePrefetchToRouteTree(childPrefetch, childSegment, childPartialVaryPath, childRequestKey, pathnameParts, childPathnamePartsIndex, renderedSearch, acc); | |
| } | |
| } else { | |
| if (requestKey.endsWith(_segment.PAGE_SEGMENT_KEY)) { | |
| // This is a page segment. | |
| isPage = true; | |
| varyPath = (0, _varypath.finalizePageVaryPath)(requestKey, renderedSearch, partialVaryPath); | |
| // The metadata "segment" is not part the route tree, but it has the same | |
| // conceptual params as a page segment. Write the vary path into the | |
| // accumulator object. If there are multiple parallel pages, we use the | |
| // first one. Which page we choose is arbitrary as long as it's | |
| // consistently the same one every time every time. See | |
| // finalizeMetadataVaryPath for more details. | |
| if (acc.metadataVaryPath === null) { | |
| acc.metadataVaryPath = (0, _varypath.finalizeMetadataVaryPath)(requestKey, renderedSearch, partialVaryPath); | |
| } | |
| } else { | |
| // This is a layout segment. | |
| isPage = false; | |
| varyPath = (0, _varypath.finalizeLayoutVaryPath)(requestKey, partialVaryPath); | |
| } | |
| } | |
| return { | |
| requestKey, | |
| segment, | |
| refreshState: null, | |
| // TODO: Cheating the type system here a bit because TypeScript can't tell | |
| // that the type of isPage and varyPath are consistent. The fix would be to | |
| // create separate constructors and call the appropriate one from each of | |
| // the branches above. Just seems a bit overkill only for one field so I'll | |
| // leave it as-is for now. If isPage were wrong it would break the behavior | |
| // and we'd catch it quickly, anyway. | |
| varyPath: varyPath, | |
| isPage: isPage, | |
| slots, | |
| prefetchHints: prefetch.prefetchHints | |
| }; | |
| } | |
| function convertRootFlightRouterStateToRouteTree(flightRouterState, renderedSearch, acc) { | |
| return convertFlightRouterStateToRouteTree(flightRouterState, _segmentvalueencoding.ROOT_SEGMENT_REQUEST_KEY, null, renderedSearch, acc); | |
| } | |
| function convertReusedFlightRouterStateToRouteTree(parentRouteTree, parallelRouteKey, flightRouterState, renderedSearch, acc) { | |
| // Create a RouteTree for a FlightRouterState that was reused from an older | |
| // route. This happens during a navigation when a parallel route slot does not | |
| // match the target route; we reuse whatever slot was already active. | |
| // Unlike a FlightRouterState, the RouteTree type contains backreferences to | |
| // the parent segments. Append the vary path to the parent's vary path. | |
| const parentPartialVaryPath = parentRouteTree.isPage ? (0, _varypath.getPartialPageVaryPath)(parentRouteTree.varyPath) : (0, _varypath.getPartialLayoutVaryPath)(parentRouteTree.varyPath); | |
| const segment = flightRouterState[0]; | |
| // And the request key. | |
| const parentRequestKey = parentRouteTree.requestKey; | |
| const requestKeyPart = (0, _segmentvalueencoding.createSegmentRequestKeyPart)(segment); | |
| const requestKey = (0, _segmentvalueencoding.appendSegmentRequestKeyPart)(parentRequestKey, parallelRouteKey, requestKeyPart); | |
| return convertFlightRouterStateToRouteTree(flightRouterState, requestKey, parentPartialVaryPath, renderedSearch, acc); | |
| } | |
| function convertFlightRouterStateToRouteTree(flightRouterState, requestKey, parentPartialVaryPath, parentRenderedSearch, acc) { | |
| const originalSegment = flightRouterState[0]; | |
| // If the FlightRouterState has a refresh state, then this segment is part of | |
| // an inactive parallel route. It has a different rendered search query than | |
| // the outer parent route. In order to construct the inactive route correctly, | |
| // we must restore the query that was originally used to render it. | |
| const compressedRefreshState = flightRouterState[2] ?? null; | |
| const refreshState = compressedRefreshState !== null ? { | |
| canonicalUrl: compressedRefreshState[0], | |
| renderedSearch: compressedRefreshState[1] | |
| } : null; | |
| const renderedSearch = refreshState !== null ? refreshState.renderedSearch : parentRenderedSearch; | |
| let segment; | |
| let partialVaryPath; | |
| let isPage; | |
| let varyPath; | |
| if (Array.isArray(originalSegment)) { | |
| isPage = false; | |
| const paramCacheKey = originalSegment[1]; | |
| const paramName = originalSegment[0]; | |
| partialVaryPath = (0, _varypath.appendLayoutVaryPath)(parentPartialVaryPath, paramCacheKey, paramName); | |
| varyPath = (0, _varypath.finalizeLayoutVaryPath)(requestKey, partialVaryPath); | |
| segment = originalSegment; | |
| } else { | |
| // This segment does not have a param. Inherit the partial vary path of | |
| // the parent. | |
| partialVaryPath = parentPartialVaryPath; | |
| if (requestKey.endsWith(_segment.PAGE_SEGMENT_KEY)) { | |
| // This is a page segment. | |
| isPage = true; | |
| // The navigation implementation expects the search params to be included | |
| // in the segment. However, in the case of a static response, the search | |
| // params are omitted. So the client needs to add them back in when reading | |
| // from the Segment Cache. | |
| // | |
| // For consistency, we'll do this for dynamic responses, too. | |
| // | |
| // TODO: We should move search params out of FlightRouterState and handle | |
| // them entirely on the client, similar to our plan for dynamic params. | |
| segment = _segment.PAGE_SEGMENT_KEY; | |
| varyPath = (0, _varypath.finalizePageVaryPath)(requestKey, renderedSearch, partialVaryPath); | |
| // The metadata "segment" is not part the route tree, but it has the same | |
| // conceptual params as a page segment. Write the vary path into the | |
| // accumulator object. If there are multiple parallel pages, we use the | |
| // first one. Which page we choose is arbitrary as long as it's | |
| // consistently the same one every time every time. See | |
| // finalizeMetadataVaryPath for more details. | |
| if (acc.metadataVaryPath === null) { | |
| acc.metadataVaryPath = (0, _varypath.finalizeMetadataVaryPath)(requestKey, renderedSearch, partialVaryPath); | |
| } | |
| } else { | |
| // This is a layout segment. | |
| isPage = false; | |
| segment = originalSegment; | |
| varyPath = (0, _varypath.finalizeLayoutVaryPath)(requestKey, partialVaryPath); | |
| } | |
| } | |
| let slots = null; | |
| const parallelRoutes = flightRouterState[1]; | |
| for(let parallelRouteKey in parallelRoutes){ | |
| const childRouterState = parallelRoutes[parallelRouteKey]; | |
| const childSegment = childRouterState[0]; | |
| // TODO: Eventually, the param values will not be included in the response | |
| // from the server. We'll instead fill them in on the client by parsing | |
| // the URL. This is where we'll do that. | |
| const childRequestKeyPart = (0, _segmentvalueencoding.createSegmentRequestKeyPart)(childSegment); | |
| const childRequestKey = (0, _segmentvalueencoding.appendSegmentRequestKeyPart)(requestKey, parallelRouteKey, childRequestKeyPart); | |
| const childTree = convertFlightRouterStateToRouteTree(childRouterState, childRequestKey, partialVaryPath, renderedSearch, acc); | |
| if (slots === null) { | |
| slots = { | |
| [parallelRouteKey]: childTree | |
| }; | |
| } else { | |
| slots[parallelRouteKey] = childTree; | |
| } | |
| } | |
| return { | |
| requestKey, | |
| segment, | |
| refreshState, | |
| // TODO: Cheating the type system here a bit because TypeScript can't tell | |
| // that the type of isPage and varyPath are consistent. The fix would be to | |
| // create separate constructors and call the appropriate one from each of | |
| // the branches above. Just seems a bit overkill only for one field so I'll | |
| // leave it as-is for now. If isPage were wrong it would break the behavior | |
| // and we'd catch it quickly, anyway. | |
| varyPath: varyPath, | |
| isPage: isPage, | |
| slots, | |
| prefetchHints: flightRouterState[4] ?? 0 | |
| }; | |
| } | |
| function convertRouteTreeToFlightRouterState(routeTree) { | |
| const parallelRoutes = {}; | |
| if (routeTree.slots !== null) { | |
| for(const parallelRouteKey in routeTree.slots){ | |
| parallelRoutes[parallelRouteKey] = convertRouteTreeToFlightRouterState(routeTree.slots[parallelRouteKey]); | |
| } | |
| } | |
| const flightRouterState = [ | |
| routeTree.segment, | |
| parallelRoutes, | |
| null, | |
| null | |
| ]; | |
| return flightRouterState; | |
| } | |
| async function fetchRouteOnCacheMiss(entry, key) { | |
| // This function is allowed to use async/await because it contains the actual | |
| // fetch that gets issued on a cache miss. Notice it writes the result to the | |
| // cache entry directly, rather than return data that is then written by | |
| // the caller. | |
| const pathname = key.pathname; | |
| const search = key.search; | |
| const nextUrl = key.nextUrl; | |
| const segmentPath = '/_tree'; | |
| const headers = { | |
| [_approuterheaders.RSC_HEADER]: '1', | |
| [_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER]: '1', | |
| [_approuterheaders.NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: segmentPath | |
| }; | |
| if (nextUrl !== null) { | |
| headers[_approuterheaders.NEXT_URL] = nextUrl; | |
| } | |
| // Tell the server to perform a static pre-render for the Instant Navigation | |
| // Testing API. Static pre-renders don't normally happen during development. | |
| addInstantPrefetchHeaderIfLocked(headers); | |
| try { | |
| const url = new URL(pathname + search, location.origin); | |
| let response; | |
| let urlAfterRedirects; | |
| if (isOutputExportMode) { | |
| // In output: "export" mode, we can't use headers to request a particular | |
| // segment. Instead, we encode the extra request information into the URL. | |
| // This is not part of the "public" interface of the app; it's an internal | |
| // Next.js implementation detail that the app developer should not need to | |
| // concern themselves with. | |
| // | |
| // For example, to request a segment: | |
| // | |
| // Path passed to <Link>: /path/to/page | |
| // Path passed to fetch: /path/to/page/__next-segments/_tree | |
| // | |
| // (This is not the exact protocol, just an illustration.) | |
| // | |
| // Before we do that, though, we need to account for redirects. Even in | |
| // output: "export" mode, a proxy might redirect the page to a different | |
| // location, but we shouldn't assume or expect that they also redirect all | |
| // the segment files, too. | |
| // | |
| // To check whether the page is redirected, previously we perform a range | |
| // request of 64 bytes of the HTML document to check if the target page | |
| // is part of this app (by checking if build id matches). Only if the target | |
| // page is part of this app do we determine the final canonical URL. | |
| // | |
| // However, as mentioned in https://github.com/vercel/next.js/pull/85903, | |
| // some popular static hosting providers (like Cloudflare Pages or Render.com) | |
| // do not support range requests, in the worst case, the entire HTML instead | |
| // of 64 bytes could be returned, which is wasteful. | |
| // | |
| // So instead, we drops the check for build id here, and simply perform | |
| // a HEAD request to rejects 1xx/4xx/5xx responses, and then determine the | |
| // final URL after redirects. | |
| // | |
| // NOTE: We could embed the route tree into the HTML document, to avoid | |
| // a second request. We're not doing that currently because it would make | |
| // the HTML document larger and affect normal page loads. | |
| const headResponse = await fetch(url, { | |
| method: 'HEAD' | |
| }); | |
| if (headResponse.status < 200 || headResponse.status >= 400) { | |
| // The target page responded w/o a successful status code | |
| // Could be a WAF serving a 403, or a 5xx from a backend | |
| // | |
| // Note that we can't use headResponse.ok here, because | |
| // Response#ok returns `false` with 3xx responses. | |
| rejectRouteCacheEntry(entry, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| urlAfterRedirects = headResponse.redirected ? new URL(headResponse.url) : url; | |
| response = await fetchPrefetchResponse(addSegmentPathToUrlInOutputExportMode(urlAfterRedirects, segmentPath), headers); | |
| } else { | |
| // "Server" mode. We can use request headers instead of the pathname. | |
| // TODO: The eventual plan is to get rid of our custom request headers and | |
| // encode everything into the URL, using a similar strategy to the | |
| // "output: export" block above. | |
| response = await fetchPrefetchResponse(url, headers); | |
| urlAfterRedirects = response !== null && response.redirected ? new URL(response.url) : url; | |
| } | |
| if (!response || !response.ok || // 204 is a Cache miss. Though theoretically this shouldn't happen when | |
| // PPR is enabled, because we always respond to route tree requests, even | |
| // if it needs to be blockingly generated on demand. | |
| response.status === 204 || !response.body) { | |
| // Server responded with an error, or with a miss. We should still cache | |
| // the response, but we can try again after 10 seconds. | |
| rejectRouteCacheEntry(entry, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| // TODO: The canonical URL is the href without the origin. I think | |
| // historically the reason for this is because the initial canonical URL | |
| // gets passed as a prop to the top-level React component, which means it | |
| // needs to be computed during SSR. If it were to include the origin, it | |
| // would need to always be same as location.origin on the client, to prevent | |
| // a hydration mismatch. To sidestep this complexity, we omit the origin. | |
| // | |
| // However, since this is neither a native URL object nor a fully qualified | |
| // URL string, we need to be careful about how we use it. To prevent subtle | |
| // mistakes, we should create a special type for it, instead of just string. | |
| // Or, we should just use a (readonly) URL object instead. The type of the | |
| // prop that we pass to seed the initial state does not need to be the same | |
| // type as the state itself. | |
| const canonicalUrl = (0, _createhreffromurl.createHrefFromUrl)(urlAfterRedirects); | |
| // Check whether the response varies based on the Next-Url header. | |
| const varyHeader = response.headers.get('vary'); | |
| const couldBeIntercepted = varyHeader !== null && varyHeader.includes(_approuterheaders.NEXT_URL); | |
| // TODO: The `closed` promise was originally used to track when a streaming | |
| // network connection closes, so the scheduler could limit concurrent | |
| // connections. Now that prefetch responses are buffered, `closed` is | |
| // resolved immediately after buffering — before the outer function even | |
| // returns. This mechanism is only still meaningful for dynamic (Full) | |
| // prefetches, which use incremental streaming. Consider removing the | |
| // `closed` plumbing for buffered prefetch paths. | |
| const closed = (0, _promisewithresolvers.createPromiseWithResolvers)(); | |
| // This checks whether the response was served from the per-segment cache, | |
| // rather than the old prefetching flow. If it fails, it implies that PPR | |
| // is disabled on this route. | |
| const routeIsPPREnabled = response.headers.get(_approuterheaders.NEXT_DID_POSTPONE_HEADER) === '2' || // In output: "export" mode, we can't rely on response headers. But if we | |
| // receive a well-formed response, we can assume it's a static response, | |
| // because all data is static in this mode. | |
| isOutputExportMode; | |
| if (routeIsPPREnabled) { | |
| const { stream: prefetchStream, size: responseSize } = await createNonTaskyPrefetchResponseStream(response.body); | |
| closed.resolve(); | |
| (0, _cachemap.setSizeInCacheMap)(entry, responseSize); | |
| const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream, headers, { | |
| allowPartialStream: true | |
| }); | |
| if ((response.headers.get(_constants.NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? serverData.buildId) !== (0, _navigationbuildid.getNavigationBuildId)()) { | |
| // The server build does not match the client. Treat as a 404. During | |
| // an actual navigation, the router will trigger an MPA navigation. | |
| // TODO: We should cache the fact that this is an MPA navigation. | |
| rejectRouteCacheEntry(entry, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| // Get the params that were used to render the target page. These may | |
| // be different from the params in the request URL, if the page | |
| // was rewritten. | |
| const renderedPathname = (0, _routeparams.getRenderedPathname)(response); | |
| const renderedSearch = (0, _routeparams.getRenderedSearch)(response); | |
| // Convert the server-sent data into the RouteTree format used by the | |
| // client cache. | |
| // | |
| // During this traversal, we accumulate additional data into this | |
| // "accumulator" object. | |
| const acc = { | |
| metadataVaryPath: null | |
| }; | |
| const routeTree = convertRootTreePrefetchToRouteTree(serverData, renderedPathname, renderedSearch, acc); | |
| const metadataVaryPath = acc.metadataVaryPath; | |
| if (metadataVaryPath === null) { | |
| rejectRouteCacheEntry(entry, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| (0, _optimisticroutes.discoverKnownRoute)(Date.now(), pathname, nextUrl, entry, routeTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, routeIsPPREnabled, false // hasDynamicRewrite | |
| ); | |
| } else { | |
| // PPR is not enabled for this route. The server responds with a | |
| // different format (FlightRouterState) that we need to convert. | |
| // TODO: We will unify the responses eventually. I'm keeping the types | |
| // separate for now because FlightRouterState has so many | |
| // overloaded concerns. | |
| const { stream: prefetchStream, size: responseSize } = await createNonTaskyPrefetchResponseStream(response.body); | |
| closed.resolve(); | |
| (0, _cachemap.setSizeInCacheMap)(entry, responseSize); | |
| const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream, headers, { | |
| allowPartialStream: true | |
| }); | |
| if ((response.headers.get(_constants.NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? serverData.b) !== (0, _navigationbuildid.getNavigationBuildId)()) { | |
| // The server build does not match the client. Treat as a 404. During | |
| // an actual navigation, the router will trigger an MPA navigation. | |
| // TODO: We should cache the fact that this is an MPA navigation. | |
| rejectRouteCacheEntry(entry, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| // Read head vary params synchronously. Individual segments carry their | |
| // own thenables in CacheNodeSeedData. | |
| const headVaryParamsThenable = serverData.h; | |
| const headVaryParams = headVaryParamsThenable !== null ? (0, _varyparamsdecoding.readVaryParams)(headVaryParamsThenable) : null; | |
| writeDynamicTreeResponseIntoCache(Date.now(), // The non-PPR response format is what we'd get if we prefetched these segments | |
| // using the LoadingBoundary fetch strategy, so mark their cache entries accordingly. | |
| _types.FetchStrategy.LoadingBoundary, response, serverData, entry, couldBeIntercepted, canonicalUrl, routeIsPPREnabled, headVaryParams, pathname, nextUrl); | |
| } | |
| if (!couldBeIntercepted) { | |
| // This route will never be intercepted. So we can use this entry for all | |
| // requests to this route, regardless of the Next-Url header. This works | |
| // because when reading the cache we always check for a valid | |
| // non-intercepted entry first. | |
| // Re-key the entry. The `set` implementation handles removing it from | |
| // its previous position in the cache. We don't need to do anything to | |
| // update the LRU, because the entry is already in it. | |
| // TODO: Treat this as an upsert — should check if an entry already | |
| // exists at the new keypath, and if so, whether we should keep that | |
| // one instead. | |
| const fulfilledVaryPath = (0, _varypath.getFulfilledRouteVaryPath)(pathname, search, nextUrl, couldBeIntercepted); | |
| const isRevalidation = false; | |
| (0, _cachemap.setInCacheMap)(routeCacheMap, fulfilledVaryPath, entry, isRevalidation); | |
| } | |
| // Return a promise that resolves when the network connection closes, so | |
| // the scheduler can track the number of concurrent network connections. | |
| return { | |
| value: null, | |
| closed: closed.promise | |
| }; | |
| } catch (error) { | |
| // Either the connection itself failed, or something bad happened while | |
| // decoding the response. | |
| rejectRouteCacheEntry(entry, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| } | |
| async function fetchSegmentOnCacheMiss(route, segmentCacheEntry, routeKey, tree) { | |
| // This function is allowed to use async/await because it contains the actual | |
| // fetch that gets issued on a cache miss. Notice it writes the result to the | |
| // cache entry directly, rather than return data that is then written by | |
| // the caller. | |
| // | |
| // Segment fetches are non-blocking so we don't need to ping the scheduler | |
| // on completion. | |
| // Use the canonical URL to request the segment, not the original URL. These | |
| // are usually the same, but the canonical URL will be different if the route | |
| // tree response was redirected. To avoid an extra waterfall on every segment | |
| // request, we pass the redirected URL instead of the original one. | |
| const url = new URL(route.canonicalUrl, location.origin); | |
| const nextUrl = routeKey.nextUrl; | |
| const requestKey = tree.requestKey; | |
| const normalizedRequestKey = requestKey === _segmentvalueencoding.ROOT_SEGMENT_REQUEST_KEY ? // handling of these requests, we encode the root segment path as | |
| // `_index` instead of as an empty string. This should be treated as | |
| // an implementation detail and not as a stable part of the protocol. | |
| // It just needs to match the equivalent logic that happens when | |
| // prerendering the responses. It should not leak outside of Next.js. | |
| '/_index' : requestKey; | |
| const headers = { | |
| [_approuterheaders.RSC_HEADER]: '1', | |
| [_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER]: '1', | |
| [_approuterheaders.NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: normalizedRequestKey | |
| }; | |
| if (nextUrl !== null) { | |
| headers[_approuterheaders.NEXT_URL] = nextUrl; | |
| } | |
| // Tell the server to perform a static pre-render for the Instant Navigation | |
| // Testing API. Static pre-renders don't normally happen during development. | |
| addInstantPrefetchHeaderIfLocked(headers); | |
| const requestUrl = isOutputExportMode ? addSegmentPathToUrlInOutputExportMode(url, normalizedRequestKey) : url; | |
| try { | |
| const response = await fetchPrefetchResponse(requestUrl, headers); | |
| if (!response || !response.ok || response.status === 204 || // Cache miss | |
| // This checks whether the response was served from the per-segment cache, | |
| // rather than the old prefetching flow. If it fails, it implies that PPR | |
| // is disabled on this route. Theoretically this should never happen | |
| // because we only issue requests for segments once we've verified that | |
| // the route supports PPR. | |
| response.headers.get(_approuterheaders.NEXT_DID_POSTPONE_HEADER) !== '2' && // In output: "export" mode, we can't rely on response headers. But if | |
| // we receive a well-formed response, we can assume it's a static | |
| // response, because all data is static in this mode. | |
| !isOutputExportMode || !response.body) { | |
| // Server responded with an error, or with a miss. We should still cache | |
| // the response, but we can try again after 10 seconds. | |
| rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| // See TODO in fetchRouteOnCacheMiss about removing `closed` for | |
| // buffered prefetch paths. | |
| const closed = (0, _promisewithresolvers.createPromiseWithResolvers)(); | |
| const { stream: prefetchStream, size: responseSize } = await createNonTaskyPrefetchResponseStream(response.body); | |
| closed.resolve(); | |
| (0, _cachemap.setSizeInCacheMap)(segmentCacheEntry, responseSize); | |
| const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream, headers, { | |
| allowPartialStream: true | |
| }); | |
| if ((response.headers.get(_constants.NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? serverData.buildId) !== (0, _navigationbuildid.getNavigationBuildId)()) { | |
| // The server build does not match the client. Treat as a 404. During | |
| // an actual navigation, the router will trigger an MPA navigation. | |
| rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| const now = Date.now(); | |
| const staleAt = now + getStaleTimeMs(serverData.staleTime); | |
| const fulfilledEntry = fulfillSegmentCacheEntry(segmentCacheEntry, serverData.rsc, staleAt, serverData.isPartial); | |
| // If the server tells us which params the segment varies by, we can re-key | |
| // the entry to a more generic vary path. This allows the entry to be reused | |
| // across different param values for params that the segment doesn't | |
| // actually depend on. | |
| const varyParams = serverData.varyParams; | |
| const fulfilledVaryPath = process.env.__NEXT_VARY_PARAMS && varyParams !== null ? (0, _varypath.getFulfilledSegmentVaryPath)(tree.varyPath, varyParams) : (0, _varypath.getSegmentVaryPathForRequest)(segmentCacheEntry.fetchStrategy, tree); | |
| // Re-key and upsert the entry at the fulfilled vary path. This ensures | |
| // the entry is stored at the most generic path possible based on which | |
| // params the segment actually depends on. | |
| upsertSegmentEntry(now, fulfilledVaryPath, fulfilledEntry); | |
| return { | |
| value: fulfilledEntry, | |
| // Return a promise that resolves when the network connection closes, so | |
| // the scheduler can track the number of concurrent network connections. | |
| closed: closed.promise | |
| }; | |
| } catch (error) { | |
| // Either the connection itself failed, or something bad happened while | |
| // decoding the response. | |
| rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| } | |
| async function fetchInlinedSegmentsOnCacheMiss(route, routeKey, tree, spawnedEntries) { | |
| // When prefetch inlining is enabled, all segment data for a route is bundled | |
| // into a single /_inlined response instead of individual per-segment | |
| // requests. This function fetches that response and walks the tree to fill | |
| // all segment cache entries at once. | |
| const url = new URL(route.canonicalUrl, location.origin); | |
| const nextUrl = routeKey.nextUrl; | |
| const headers = { | |
| [_approuterheaders.RSC_HEADER]: '1', | |
| [_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER]: '1', | |
| [_approuterheaders.NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: '/' + _segment.PAGE_SEGMENT_KEY | |
| }; | |
| if (nextUrl !== null) { | |
| headers[_approuterheaders.NEXT_URL] = nextUrl; | |
| } | |
| addInstantPrefetchHeaderIfLocked(headers); | |
| try { | |
| const response = await fetchPrefetchResponse(url, headers); | |
| if (!response || !response.ok || response.status === 204 || response.headers.get(_approuterheaders.NEXT_DID_POSTPONE_HEADER) !== '2' && !isOutputExportMode || !response.body) { | |
| rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| // See TODO in fetchRouteOnCacheMiss about removing `closed` for | |
| // buffered prefetch paths. | |
| const closed = (0, _promisewithresolvers.createPromiseWithResolvers)(); | |
| const { stream: prefetchStream } = await createNonTaskyPrefetchResponseStream(response.body); | |
| closed.resolve(); | |
| const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream, headers, { | |
| allowPartialStream: true | |
| }); | |
| if ((response.headers.get(_constants.NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? serverData.tree.segment.buildId) !== (0, _navigationbuildid.getNavigationBuildId)()) { | |
| rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| const now = Date.now(); | |
| // Walk the inlined tree in parallel with the RouteTree and fill | |
| // segment cache entries. | |
| fillInlinedSegmentEntries(now, route, tree, serverData.tree, spawnedEntries); | |
| // Fill the head entry. | |
| const headStaleAt = now + getStaleTimeMs(serverData.head.staleTime); | |
| const headKey = route.metadata.requestKey; | |
| const ownedHeadEntry = spawnedEntries.get(headKey); | |
| if (ownedHeadEntry !== undefined) { | |
| fulfillSegmentCacheEntry(ownedHeadEntry, serverData.head.rsc, headStaleAt, serverData.head.isPartial); | |
| } else { | |
| // The head was already cached. Try to upsert if the entry is empty. | |
| const existingEntry = readOrCreateSegmentCacheEntry(now, _types.FetchStrategy.PPR, route.metadata); | |
| if (existingEntry.status === 0) { | |
| fulfillSegmentCacheEntry(upgradeToPendingSegment(existingEntry, _types.FetchStrategy.PPR), serverData.head.rsc, headStaleAt, serverData.head.isPartial); | |
| } | |
| } | |
| // Reject any remaining entries that were not fulfilled by the response. | |
| rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000); | |
| return { | |
| value: null, | |
| closed: closed.promise | |
| }; | |
| } catch (error) { | |
| rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| } | |
| function fillInlinedSegmentEntries(now, route, tree, inlinedNode, spawnedEntries) { | |
| // Check if the spawned entries map has an entry for this segment's key. | |
| const segment = inlinedNode.segment; | |
| const staleAt = now + getStaleTimeMs(segment.staleTime); | |
| const ownedEntry = spawnedEntries.get(tree.requestKey); | |
| if (ownedEntry !== undefined) { | |
| // We own this entry. Fulfill it directly. | |
| fulfillSegmentCacheEntry(ownedEntry, segment.rsc, staleAt, segment.isPartial); | |
| } else { | |
| // Not owned by us — this is extra data from the inlined response for a | |
| // segment that was already cached. Try to upsert if the entry is empty. | |
| const existingEntry = readOrCreateSegmentCacheEntry(now, _types.FetchStrategy.PPR, tree); | |
| if (existingEntry.status === 0) { | |
| fulfillSegmentCacheEntry(upgradeToPendingSegment(existingEntry, _types.FetchStrategy.PPR), segment.rsc, staleAt, segment.isPartial); | |
| } | |
| } | |
| // Recurse into children. | |
| if (tree.slots !== null && inlinedNode.slots !== null) { | |
| for(const parallelRouteKey in tree.slots){ | |
| const childTree = tree.slots[parallelRouteKey]; | |
| const childInlinedNode = inlinedNode.slots[parallelRouteKey]; | |
| if (childInlinedNode !== undefined) { | |
| fillInlinedSegmentEntries(now, route, childTree, childInlinedNode, spawnedEntries); | |
| } | |
| } | |
| } | |
| } | |
| async function fetchSegmentPrefetchesUsingDynamicRequest(task, route, fetchStrategy, dynamicRequestTree, spawnedEntries) { | |
| const key = task.key; | |
| const url = new URL(route.canonicalUrl, location.origin); | |
| const nextUrl = key.nextUrl; | |
| if (spawnedEntries.size === 1 && spawnedEntries.has(route.metadata.requestKey)) { | |
| // The only thing pending is the head. Instruct the server to | |
| // skip over everything else. | |
| dynamicRequestTree = MetadataOnlyRequestTree; | |
| } | |
| const headers = { | |
| [_approuterheaders.RSC_HEADER]: '1', | |
| [_approuterheaders.NEXT_ROUTER_STATE_TREE_HEADER]: (0, _flightdatahelpers.prepareFlightRouterStateForRequest)(dynamicRequestTree) | |
| }; | |
| if (nextUrl !== null) { | |
| headers[_approuterheaders.NEXT_URL] = nextUrl; | |
| } | |
| switch(fetchStrategy){ | |
| case _types.FetchStrategy.Full: | |
| { | |
| break; | |
| } | |
| case _types.FetchStrategy.PPRRuntime: | |
| { | |
| headers[_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER] = '2'; | |
| break; | |
| } | |
| case _types.FetchStrategy.LoadingBoundary: | |
| { | |
| headers[_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER] = '1'; | |
| break; | |
| } | |
| default: | |
| { | |
| fetchStrategy; | |
| } | |
| } | |
| try { | |
| const response = await fetchPrefetchResponse(url, headers); | |
| if (!response || !response.ok || !response.body) { | |
| // Server responded with an error, or with a miss. We should still cache | |
| // the response, but we can try again after 10 seconds. | |
| rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| const renderedSearch = (0, _routeparams.getRenderedSearch)(response); | |
| if (renderedSearch !== route.renderedSearch) { | |
| // The search params that were used to render the target page are | |
| // different from the search params in the request URL. This only happens | |
| // when there's a dynamic rewrite in between the tree prefetch and the | |
| // data prefetch. | |
| // TODO: For now, since this is an edge case, we reject the prefetch, but | |
| // the proper way to handle this is to evict the stale route tree entry | |
| // then fill the cache with the new response. | |
| rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| // Track when the network connection closes. Only meaningful for Full | |
| // (dynamic) prefetches which use incremental streaming. For buffered | |
| // paths, this is resolved immediately — see TODO in fetchRouteOnCacheMiss. | |
| const closed = (0, _promisewithresolvers.createPromiseWithResolvers)(); | |
| let fulfilledEntries = null; | |
| let prefetchStream; | |
| let bufferedResponseSize = null; | |
| if (fetchStrategy === _types.FetchStrategy.Full) { | |
| // Full prefetches are dynamic responses stored in the prefetch cache. | |
| // They don't carry vary params or other cache metadata, so there's no | |
| // need to buffer them. Use the incremental version to allow data to be | |
| // processed as it arrives. | |
| prefetchStream = createIncrementalPrefetchResponseStream(response.body, closed.resolve, function onResponseSizeUpdate(totalBytesReceivedSoFar) { | |
| // When processing a dynamic response, we don't know how large each | |
| // individual segment is, so approximate by assigning each segment | |
| // the average of the total response size. | |
| if (fulfilledEntries === null) { | |
| // Haven't received enough data yet to know which segments | |
| // were included. | |
| return; | |
| } | |
| const averageSize = totalBytesReceivedSoFar / fulfilledEntries.length; | |
| for (const entry of fulfilledEntries){ | |
| (0, _cachemap.setSizeInCacheMap)(entry, averageSize); | |
| } | |
| }); | |
| } else { | |
| const { stream, size } = await createNonTaskyPrefetchResponseStream(response.body); | |
| closed.resolve(); | |
| prefetchStream = stream; | |
| bufferedResponseSize = size; | |
| } | |
| const [serverData, cacheData] = await Promise.all([ | |
| (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream, headers, { | |
| allowPartialStream: true | |
| }), | |
| response.cacheData | |
| ]); | |
| // Read head vary params synchronously. Individual segments carry their | |
| // own thenables in CacheNodeSeedData. | |
| const headVaryParamsThenable = serverData.h; | |
| const headVaryParams = headVaryParamsThenable !== null ? (0, _varyparamsdecoding.readVaryParams)(headVaryParamsThenable) : null; | |
| const now = Date.now(); | |
| const staleAt = await getStaleAt(now, serverData.s, response); | |
| // PPRRuntime prefetches are partial when the server marks the response | |
| // as '~' (Partial). Full/LoadingBoundary prefetches are always complete. | |
| const isResponsePartial = fetchStrategy === _types.FetchStrategy.PPRRuntime && (cacheData?.isResponsePartial ?? false); | |
| // Aside from writing the data into the cache, this function also returns | |
| // the entries that were fulfilled, so we can streamingly update their sizes | |
| // in the LRU as more data comes in. | |
| const buildId = response.headers.get(_constants.NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? serverData.b; | |
| const flightDatas = (0, _flightdatahelpers.normalizeFlightData)(serverData.f); | |
| if (typeof flightDatas === 'string') { | |
| rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| const navigationSeed = (0, _navigation.convertServerPatchToFullTree)(now, dynamicRequestTree, flightDatas, renderedSearch, // Not needed for prefetch responses; pass unknown to use the default. | |
| _bfcache.UnknownDynamicStaleTime); | |
| fulfilledEntries = writeDynamicRenderResponseIntoCache(now, fetchStrategy, flightDatas, buildId, isResponsePartial, headVaryParams, staleAt, navigationSeed, spawnedEntries); | |
| // For buffered responses, update LRU sizes now that we know which | |
| // entries were fulfilled. | |
| if (bufferedResponseSize !== null && fulfilledEntries !== null && fulfilledEntries.length > 0) { | |
| const averageSize = bufferedResponseSize / fulfilledEntries.length; | |
| for (const entry of fulfilledEntries){ | |
| (0, _cachemap.setSizeInCacheMap)(entry, averageSize); | |
| } | |
| } | |
| // Return a promise that resolves when the network connection closes, so | |
| // the scheduler can track the number of concurrent network connections. | |
| return { | |
| value: null, | |
| closed: closed.promise | |
| }; | |
| } catch (error) { | |
| rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000); | |
| return null; | |
| } | |
| } | |
| function writeDynamicTreeResponseIntoCache(now, fetchStrategy, response, serverData, entry, couldBeIntercepted, canonicalUrl, routeIsPPREnabled, headVaryParams, originalPathname, nextUrl) { | |
| const renderedSearch = (0, _routeparams.getRenderedSearch)(response); | |
| const normalizedFlightDataResult = (0, _flightdatahelpers.normalizeFlightData)(serverData.f); | |
| if (// A string result means navigating to this route will result in an | |
| // MPA navigation. | |
| typeof normalizedFlightDataResult === 'string' || normalizedFlightDataResult.length !== 1) { | |
| rejectRouteCacheEntry(entry, now + 10 * 1000); | |
| return; | |
| } | |
| const flightData = normalizedFlightDataResult[0]; | |
| if (!flightData.isRootRender) { | |
| // Unexpected response format. | |
| rejectRouteCacheEntry(entry, now + 10 * 1000); | |
| return; | |
| } | |
| const flightRouterState = flightData.tree; | |
| // If the response was postponed, segments may contain dynamic holes. | |
| // The head has its own partiality flag (flightDataEntry.isHeadPartial) | |
| // which is handled separately in writeDynamicRenderResponseIntoCache. | |
| const isResponsePartial = response.headers.get(_approuterheaders.NEXT_DID_POSTPONE_HEADER) === '1'; | |
| // Convert the server-sent data into the RouteTree format used by the | |
| // client cache. | |
| // | |
| // During this traversal, we accumulate additional data into this | |
| // "accumulator" object. | |
| const acc = { | |
| metadataVaryPath: null | |
| }; | |
| const routeTree = convertRootFlightRouterStateToRouteTree(flightRouterState, renderedSearch, acc); | |
| const metadataVaryPath = acc.metadataVaryPath; | |
| if (metadataVaryPath === null) { | |
| rejectRouteCacheEntry(entry, now + 10 * 1000); | |
| return; | |
| } | |
| (0, _optimisticroutes.discoverKnownRoute)(now, originalPathname, nextUrl, entry, routeTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, routeIsPPREnabled, false // hasDynamicRewrite | |
| ); | |
| // If the server sent segment data as part of the response, we should write | |
| // it into the cache to prevent a second, redundant prefetch request. | |
| // TODO: This is a leftover branch from before Client Segment Cache was | |
| // enabled everywhere. Tree prefetches should never include segment data. We | |
| // can delete it. Leaving for a subsequent PR. | |
| const navigationSeed = (0, _navigation.convertServerPatchToFullTree)(now, flightRouterState, normalizedFlightDataResult, renderedSearch, _bfcache.UnknownDynamicStaleTime); | |
| const buildId = response.headers.get(_constants.NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? serverData.b; | |
| writeDynamicRenderResponseIntoCache(now, fetchStrategy, normalizedFlightDataResult, buildId, isResponsePartial, headVaryParams, getStaleAtFromHeader(now, response), navigationSeed, null); | |
| } | |
| function rejectSegmentEntriesIfStillPending(entries, staleAt) { | |
| const fulfilledEntries = []; | |
| for (const entry of entries.values()){ | |
| if (entry.status === 1) { | |
| rejectSegmentCacheEntry(entry, staleAt); | |
| } else if (entry.status === 2) { | |
| fulfilledEntries.push(entry); | |
| } | |
| } | |
| return fulfilledEntries; | |
| } | |
| function writeDynamicRenderResponseIntoCache(now, fetchStrategy, flightDatas, buildId, isResponsePartial, headVaryParams, staleAt, navigationSeed, spawnedEntries) { | |
| if (buildId && buildId !== (0, _navigationbuildid.getNavigationBuildId)()) { | |
| // The server build does not match the client. Treat as a 404. During | |
| // an actual navigation, the router will trigger an MPA navigation. | |
| if (spawnedEntries !== null) { | |
| rejectSegmentEntriesIfStillPending(spawnedEntries, now + 10 * 1000); | |
| } | |
| return null; | |
| } | |
| const routeTree = navigationSeed.routeTree; | |
| const metadataTree = navigationSeed.metadataVaryPath !== null ? createMetadataRouteTree(navigationSeed.metadataVaryPath) : null; | |
| for (const flightDataEntry of flightDatas){ | |
| const seedData = flightDataEntry.seedData; | |
| if (seedData !== null) { | |
| // The data sent by the server represents only a subtree of the app. We | |
| // need to find the part of the task tree that matches the response. | |
| // | |
| // segmentPath represents the parent path of subtree. It's a repeating | |
| // pattern of parallel route key and segment: | |
| // | |
| // [string, Segment, string, Segment, string, Segment, ...] | |
| const segmentPath = flightDataEntry.segmentPath; | |
| let tree = routeTree; | |
| for(let i = 0; i < segmentPath.length; i += 2){ | |
| const parallelRouteKey = segmentPath[i]; | |
| if (tree?.slots?.[parallelRouteKey] !== undefined) { | |
| tree = tree.slots[parallelRouteKey]; | |
| } else { | |
| if (spawnedEntries !== null) { | |
| rejectSegmentEntriesIfStillPending(spawnedEntries, now + 10 * 1000); | |
| } | |
| return null; | |
| } | |
| } | |
| writeSeedDataIntoCache(now, fetchStrategy, tree, staleAt, seedData, isResponsePartial, spawnedEntries); | |
| } | |
| const head = flightDataEntry.head; | |
| if (head !== null && metadataTree !== null) { | |
| // When Cache Components is enabled, the server conservatively marks | |
| // the head as partial during static generation (isPossiblyPartialHead | |
| // in app-render.tsx), even for fully static pages where the head is | |
| // actually complete. When the response is non-partial, we override | |
| // this since the server confirmed no dynamic content exists. | |
| // | |
| // Without Cache Components, the server always sends the correct | |
| // isHeadPartial value, so no override is needed. | |
| const isHeadPartial = !isResponsePartial && process.env.__NEXT_CACHE_COMPONENTS ? false : flightDataEntry.isHeadPartial; | |
| fulfillEntrySpawnedByRuntimePrefetch(now, fetchStrategy, head, isHeadPartial, staleAt, // For head entries, use the head-specific vary params passed as | |
| // parameter. | |
| headVaryParams, metadataTree, spawnedEntries); | |
| } | |
| } | |
| // Any entry that's still pending was intentionally not rendered by the | |
| // server, because it was inside the loading boundary. Mark them as rejected | |
| // so we know not to fetch them again. | |
| // TODO: If PPR is enabled on some routes but not others, then it's possible | |
| // that a different page is able to do a per-segment prefetch of one of the | |
| // segments we're marking as rejected here. We should mark on the segment | |
| // somehow that the reason for the rejection is because of a non-PPR prefetch. | |
| // That way a per-segment prefetch knows to disregard the rejection. | |
| if (spawnedEntries !== null) { | |
| const fulfilledEntries = rejectSegmentEntriesIfStillPending(spawnedEntries, now + 10 * 1000); | |
| return fulfilledEntries; | |
| } | |
| return null; | |
| } | |
| function writeSeedDataIntoCache(now, fetchStrategy, tree, staleAt, seedData, isResponsePartial, entriesOwnedByCurrentTask) { | |
| // This function is used to write the result of a runtime server request | |
| // (CacheNodeSeedData) into the prefetch cache. | |
| const rsc = seedData[0]; | |
| const isPartial = rsc === null || isResponsePartial; | |
| const varyParamsThenable = seedData[4]; | |
| // Each segment carries its own vary params thenable in the seed data. The | |
| // thenable resolves to the set of params the segment accessed during render. | |
| // A null thenable means tracking was not enabled (not a prerender). | |
| const varyParams = varyParamsThenable !== null ? (0, _varyparamsdecoding.readVaryParams)(varyParamsThenable) : null; | |
| fulfillEntrySpawnedByRuntimePrefetch(now, fetchStrategy, rsc, isPartial, staleAt, varyParams, tree, entriesOwnedByCurrentTask); | |
| // Recursively write the child data into the cache. | |
| const slots = tree.slots; | |
| if (slots !== null) { | |
| const seedDataChildren = seedData[1]; | |
| for(const parallelRouteKey in slots){ | |
| const childTree = slots[parallelRouteKey]; | |
| const childSeedData = seedDataChildren[parallelRouteKey]; | |
| if (childSeedData !== null && childSeedData !== undefined) { | |
| writeSeedDataIntoCache(now, fetchStrategy, childTree, staleAt, childSeedData, isResponsePartial, entriesOwnedByCurrentTask); | |
| } | |
| } | |
| } | |
| } | |
| function fulfillEntrySpawnedByRuntimePrefetch(now, fetchStrategy, rsc, isPartial, staleAt, segmentVaryParams, tree, entriesOwnedByCurrentTask) { | |
| // We should only write into cache entries that are owned by us. Or create | |
| // a new one and write into that. We must never write over an entry that was | |
| // created by a different task, because that causes data races. | |
| const ownedEntry = entriesOwnedByCurrentTask !== null ? entriesOwnedByCurrentTask.get(tree.requestKey) : undefined; | |
| if (ownedEntry !== undefined) { | |
| const fulfilledEntry = fulfillSegmentCacheEntry(ownedEntry, rsc, staleAt, isPartial); | |
| // Re-key the entry based on which params the segment actually depends on. | |
| if (process.env.__NEXT_VARY_PARAMS && segmentVaryParams !== null) { | |
| const fulfilledVaryPath = (0, _varypath.getFulfilledSegmentVaryPath)(tree.varyPath, segmentVaryParams); | |
| const isRevalidation = false; | |
| (0, _cachemap.setInCacheMap)(segmentCacheMap, fulfilledVaryPath, fulfilledEntry, isRevalidation); | |
| } | |
| } else { | |
| // There's no matching entry. Attempt to create a new one. | |
| const possiblyNewEntry = readOrCreateSegmentCacheEntry(now, fetchStrategy, tree); | |
| if (possiblyNewEntry.status === 0) { | |
| // Confirmed this is a new entry. We can fulfill it. | |
| const newEntry = possiblyNewEntry; | |
| const fulfilledEntry = fulfillSegmentCacheEntry(upgradeToPendingSegment(newEntry, fetchStrategy), rsc, staleAt, isPartial); | |
| // Re-key the entry based on which params the segment actually depends on. | |
| if (process.env.__NEXT_VARY_PARAMS && segmentVaryParams !== null) { | |
| const fulfilledVaryPath = (0, _varypath.getFulfilledSegmentVaryPath)(tree.varyPath, segmentVaryParams); | |
| const isRevalidation = false; | |
| (0, _cachemap.setInCacheMap)(segmentCacheMap, fulfilledVaryPath, fulfilledEntry, isRevalidation); | |
| } | |
| } else { | |
| // There was already an entry in the cache. But we may be able to | |
| // replace it with the new one from the server. | |
| const newEntry = fulfillSegmentCacheEntry(upgradeToPendingSegment(createDetachedSegmentCacheEntry(now), fetchStrategy), rsc, staleAt, isPartial); | |
| // Use the fulfilled vary path if available, otherwise fall back to | |
| // the request vary path. | |
| const varyPath = process.env.__NEXT_VARY_PARAMS && segmentVaryParams !== null ? (0, _varypath.getFulfilledSegmentVaryPath)(tree.varyPath, segmentVaryParams) : (0, _varypath.getSegmentVaryPathForRequest)(fetchStrategy, tree); | |
| upsertSegmentEntry(now, varyPath, newEntry); | |
| } | |
| } | |
| } | |
| async function fetchPrefetchResponse(url, headers) { | |
| const fetchPriority = 'low'; | |
| // When issuing a prefetch request, don't immediately decode the response; we | |
| // use the lower level `createFromResponse` API instead because we need to do | |
| // some extra processing of the response stream. See | |
| // `createNonTaskyPrefetchResponseStream` for more details. | |
| const shouldImmediatelyDecode = false; | |
| const response = await (0, _fetchserverresponse.createFetch)(url, headers, fetchPriority, shouldImmediatelyDecode); | |
| if (!response.ok) { | |
| return null; | |
| } | |
| // Check the content type | |
| if (isOutputExportMode) { | |
| // In output: "export" mode, we relaxed about the content type, since it's | |
| // not Next.js that's serving the response. If the status is OK, assume the | |
| // response is valid. If it's not a valid response, the Flight client won't | |
| // be able to decode it, and we'll treat it as a miss. | |
| } else { | |
| const contentType = response.headers.get('content-type'); | |
| const isFlightResponse = contentType && contentType.startsWith(_approuterheaders.RSC_CONTENT_TYPE_HEADER); | |
| if (!isFlightResponse) { | |
| return null; | |
| } | |
| } | |
| return response; | |
| } | |
| async function createNonTaskyPrefetchResponseStream(body) { | |
| // Buffer the entire response before passing it to the Flight client. This | |
| // ensures that when Flight processes the stream, all model data is available | |
| // synchronously. This is important for readVaryParams, which synchronously | |
| // checks the thenable status — if data arrived in multiple network chunks, | |
| // the thenables might not yet be fulfilled. | |
| // | |
| // TODO: There are too many intermediate stream transformations in the | |
| // prefetch response pipeline (e.g. stripIsPartialByte, this function). | |
| // These could all be consolidated into a single transformation. Refactor | |
| // once the cached navigations experiment lands. | |
| // | |
| // Read the entire response from the network. | |
| const reader = body.getReader(); | |
| const chunks = []; | |
| let size = 0; | |
| while(true){ | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| chunks.push(value); | |
| size += value.byteLength; | |
| } | |
| // Concatenate into a single chunk so that Flight's processBinaryChunk | |
| // processes all rows synchronously in one call. Multiple chunks would not | |
| // be sufficient: even though reader.read() resolves as a microtask for | |
| // already-enqueued data, the `await` continuation from | |
| // createFromReadableStream can interleave between chunks. If the root | |
| // model row isn't the first row (e.g. outlined values come first), the | |
| // PromiseResolveThenableJob from `await` can cause the root to initialize | |
| // eagerly, scheduling the continuation before remaining chunks (including | |
| // promise value rows) are processed. A single chunk avoids this. | |
| let buffer; | |
| if (chunks.length === 1) { | |
| buffer = chunks[0]; | |
| } else if (chunks.length > 1) { | |
| buffer = new Uint8Array(size); | |
| let offset = 0; | |
| for (const chunk of chunks){ | |
| buffer.set(chunk, offset); | |
| offset += chunk.byteLength; | |
| } | |
| } else { | |
| buffer = new Uint8Array(0); | |
| } | |
| const stream = new ReadableStream({ | |
| start (controller) { | |
| controller.enqueue(buffer); | |
| controller.close(); | |
| } | |
| }); | |
| return { | |
| stream, | |
| size | |
| }; | |
| } | |
| /** | |
| * Creates a streaming (non-buffered) prefetch response stream for dynamic/Full | |
| * prefetches. These are essentially dynamic responses that get stored in the | |
| * prefetch cache — they don't carry vary params or other cache metadata that | |
| * requires synchronous thenable resolution, so there's no need to buffer them. | |
| * They should continue to stream so consumers can process data as it arrives. | |
| */ function createIncrementalPrefetchResponseStream(originalFlightStream, onStreamClose, onResponseSizeUpdate) { | |
| // While processing the original stream, we incrementally update the size | |
| // of the cache entry in the LRU. | |
| let totalByteLength = 0; | |
| const reader = originalFlightStream.getReader(); | |
| return new ReadableStream({ | |
| async pull (controller) { | |
| while(true){ | |
| const { done, value } = await reader.read(); | |
| if (!done) { | |
| // Pass to the target stream and keep consuming the Flight response | |
| // from the server. | |
| controller.enqueue(value); | |
| // Incrementally update the size of the cache entry in the LRU. | |
| totalByteLength += value.byteLength; | |
| onResponseSizeUpdate(totalByteLength); | |
| continue; | |
| } | |
| controller.close(); | |
| onStreamClose(); | |
| return; | |
| } | |
| } | |
| }); | |
| } | |
| function addSegmentPathToUrlInOutputExportMode(url, segmentPath) { | |
| if (isOutputExportMode) { | |
| // In output: "export" mode, we cannot use a header to encode the segment | |
| // path. Instead, we append it to the end of the pathname. | |
| const staticUrl = new URL(url); | |
| const routeDir = staticUrl.pathname.endsWith('/') ? staticUrl.pathname.slice(0, -1) : staticUrl.pathname; | |
| const staticExportFilename = (0, _segmentvalueencoding.convertSegmentPathToStaticExportFilename)(segmentPath); | |
| staticUrl.pathname = `${routeDir}/${staticExportFilename}`; | |
| return staticUrl; | |
| } | |
| return url; | |
| } | |
| function canNewFetchStrategyProvideMoreContent(currentStrategy, newStrategy) { | |
| return currentStrategy < newStrategy; | |
| } | |
| /** | |
| * Adds the instant prefetch header if the navigation lock is active. | |
| * Uses a lazy require to ensure dead code elimination. | |
| */ function addInstantPrefetchHeaderIfLocked(headers) { | |
| if (process.env.__NEXT_EXPOSE_TESTING_API) { | |
| const { isNavigationLocked } = require('./navigation-testing-lock'); | |
| if (isNavigationLocked()) { | |
| headers[_approuterheaders.NEXT_INSTANT_PREFETCH_HEADER] = '1'; | |
| } | |
| } | |
| } | |
| function getStaleAtFromHeader(now, response) { | |
| const staleTimeSeconds = parseInt(response.headers.get(_approuterheaders.NEXT_ROUTER_STALE_TIME_HEADER) ?? '', 10); | |
| const staleTimeMs = !isNaN(staleTimeSeconds) ? getStaleTimeMs(staleTimeSeconds) : _navigatereducer.STATIC_STALETIME_MS; | |
| return now + staleTimeMs; | |
| } | |
| async function getStaleAt(now, staleTimeIterable, response) { | |
| if (staleTimeIterable !== undefined) { | |
| // Iterate the async iterable and take the last yielded value. The server | |
| // yields updated staleTime values during the render; the last one is the | |
| // final staleTime. | |
| let staleTimeSeconds; | |
| for await (const value of staleTimeIterable){ | |
| staleTimeSeconds = value; | |
| } | |
| if (staleTimeSeconds !== undefined) { | |
| const staleTimeMs = isNaN(staleTimeSeconds) ? _navigatereducer.STATIC_STALETIME_MS : getStaleTimeMs(staleTimeSeconds); | |
| return now + staleTimeMs; | |
| } | |
| } | |
| if (response !== undefined) { | |
| return getStaleAtFromHeader(now, response); | |
| } | |
| return now + _navigatereducer.STATIC_STALETIME_MS; | |
| } | |
| function writeStaticStageResponseIntoCache(now, flightData, buildId, headVaryParamsThenable, staleAt, baseTree, renderedSearch, isResponsePartial) { | |
| const fetchStrategy = isResponsePartial ? _types.FetchStrategy.PPR : _types.FetchStrategy.Full; | |
| const headVaryParams = headVaryParamsThenable !== null ? (0, _varyparamsdecoding.readVaryParams)(headVaryParamsThenable) : null; | |
| const flightDatas = (0, _flightdatahelpers.normalizeFlightData)(flightData); | |
| if (typeof flightDatas === 'string') { | |
| return; | |
| } | |
| const navigationSeed = (0, _navigation.convertServerPatchToFullTree)(now, baseTree, flightDatas, renderedSearch, _bfcache.UnknownDynamicStaleTime); | |
| writeDynamicRenderResponseIntoCache(now, fetchStrategy, flightDatas, buildId, isResponsePartial, headVaryParams, staleAt, navigationSeed, null // spawnedEntries — no pre-created entries; will create or upsert | |
| ); | |
| } | |
| async function processRuntimePrefetchStream(now, runtimePrefetchStream, baseTree, renderedSearch) { | |
| const { stream, isPartial } = await stripIsPartialByte(runtimePrefetchStream); | |
| const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(stream, undefined, { | |
| allowPartialStream: true | |
| }); | |
| const headVaryParamsThenable = serverData.h; | |
| const headVaryParams = headVaryParamsThenable !== null ? (0, _varyparamsdecoding.readVaryParams)(headVaryParamsThenable) : null; | |
| const staleAt = await getStaleAt(now, serverData.s); | |
| const flightDatas = (0, _flightdatahelpers.normalizeFlightData)(serverData.f); | |
| if (typeof flightDatas === 'string') { | |
| return null; | |
| } | |
| const navigationSeed = (0, _navigation.convertServerPatchToFullTree)(now, baseTree, flightDatas, renderedSearch, _bfcache.UnknownDynamicStaleTime); | |
| return { | |
| flightDatas, | |
| navigationSeed, | |
| buildId: serverData.b, | |
| isResponsePartial: isPartial, | |
| headVaryParams, | |
| staleAt | |
| }; | |
| } | |
| async function stripIsPartialByte(stream) { | |
| // When there is no recognized marker byte, the fallback depends on whether | |
| // Cached Navigations is enabled. When enabled, dynamic navigation responses | |
| // don't have a marker but may contain dynamic holes, so they are treated as | |
| // partial. When disabled, unmarked responses are treated as non-partial. | |
| const defaultIsPartial = !!process.env.__NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS; | |
| const reader = stream.getReader(); | |
| const { done, value } = await reader.read(); | |
| if (done || !value || value.byteLength === 0) { | |
| return { | |
| stream: new ReadableStream({ | |
| start: (c)=>c.close() | |
| }), | |
| isPartial: defaultIsPartial | |
| }; | |
| } | |
| const firstByte = value[0]; | |
| const hasMarker = firstByte === 0x23 || firstByte === 0x7e; | |
| const isPartial = hasMarker ? firstByte === 0x7e : defaultIsPartial; | |
| const remainder = hasMarker ? value.byteLength > 1 ? value.subarray(1) : null : value; | |
| return { | |
| isPartial, | |
| stream: new ReadableStream({ | |
| start (controller) { | |
| if (remainder) { | |
| controller.enqueue(remainder); | |
| } | |
| }, | |
| async pull (controller) { | |
| const result = await reader.read(); | |
| if (result.done) { | |
| controller.close(); | |
| } else { | |
| controller.enqueue(result.value); | |
| } | |
| } | |
| }) | |
| }; | |
| } | |
| 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=cache.js.map |