"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { EntryStatus: null, canNewFetchStrategyProvideMoreContent: null, convertRouteTreeToFlightRouterState: null, createDetachedSegmentCacheEntry: null, fetchRouteOnCacheMiss: null, fetchSegmentOnCacheMiss: null, fetchSegmentPrefetchesUsingDynamicRequest: null, getCurrentCacheVersion: null, getSegmentKeypathForTask: null, pingInvalidationListeners: null, readExactRouteCacheEntry: null, readOrCreateRevalidatingSegmentEntry: null, readOrCreateRouteCacheEntry: null, readOrCreateSegmentCacheEntry: null, readRouteCacheEntry: null, readSegmentCacheEntry: null, requestOptimisticRouteCacheEntry: null, resetRevalidatingSegmentEntry: null, revalidateEntireCache: null, upgradeToPendingSegment: null, upsertSegmentEntry: null, waitForSegmentCacheEntry: 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; }, canNewFetchStrategyProvideMoreContent: function() { return canNewFetchStrategyProvideMoreContent; }, convertRouteTreeToFlightRouterState: function() { return convertRouteTreeToFlightRouterState; }, createDetachedSegmentCacheEntry: function() { return createDetachedSegmentCacheEntry; }, fetchRouteOnCacheMiss: function() { return fetchRouteOnCacheMiss; }, fetchSegmentOnCacheMiss: function() { return fetchSegmentOnCacheMiss; }, fetchSegmentPrefetchesUsingDynamicRequest: function() { return fetchSegmentPrefetchesUsingDynamicRequest; }, getCurrentCacheVersion: function() { return getCurrentCacheVersion; }, getSegmentKeypathForTask: function() { return getSegmentKeypathForTask; }, pingInvalidationListeners: function() { return pingInvalidationListeners; }, readExactRouteCacheEntry: function() { return readExactRouteCacheEntry; }, readOrCreateRevalidatingSegmentEntry: function() { return readOrCreateRevalidatingSegmentEntry; }, readOrCreateRouteCacheEntry: function() { return readOrCreateRouteCacheEntry; }, readOrCreateSegmentCacheEntry: function() { return readOrCreateSegmentCacheEntry; }, readRouteCacheEntry: function() { return readRouteCacheEntry; }, readSegmentCacheEntry: function() { return readSegmentCacheEntry; }, requestOptimisticRouteCacheEntry: function() { return requestOptimisticRouteCacheEntry; }, resetRevalidatingSegmentEntry: function() { return resetRevalidatingSegmentEntry; }, revalidateEntireCache: function() { return revalidateEntireCache; }, upgradeToPendingSegment: function() { return upgradeToPendingSegment; }, upsertSegmentEntry: function() { return upsertSegmentEntry; }, waitForSegmentCacheEntry: function() { return waitForSegmentCacheEntry; } }); const _types = require("../../../server/app-render/types"); const _approuterheaders = require("../app-router-headers"); const _fetchserverresponse = require("../router-reducer/fetch-server-response"); const _scheduler = require("./scheduler"); const _appbuildid = require("../../app-build-id"); const _createhreffromurl = require("../router-reducer/create-href-from-url"); const _cachekey = require("./cache-key"); const _routeparams = require("../../route-params"); const _tuplemap = require("./tuple-map"); const _lru = require("./lru"); const _segmentvalueencoding = require("../../../shared/lib/segment-cache/segment-value-encoding"); const _flightdatahelpers = require("../../flight-data-helpers"); const _prefetchcacheutils = require("../router-reducer/prefetch-cache-utils"); const _links = require("../links"); const _segment = require("../../../shared/lib/segment"); const _outputexportprefetchencoding = require("../../../shared/lib/segment-cache/output-export-prefetch-encoding"); const _segmentcache = require("../segment-cache"); const _promisewithresolvers = require("../../../shared/lib/promise-with-resolvers"); 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'; /** * Ensures a minimum stale time of 30s to avoid issues where the server sends a too * short-lived stale time, which would prevent anything from being prefetched. */ function getStaleTimeMs(staleTimeSeconds) { return Math.max(staleTimeSeconds, 30) * 1000; } let routeCacheMap = (0, _tuplemap.createTupleMap)(); // We use an LRU for memory management. We must update this whenever we add or // remove a new cache entry, or when an entry changes size. // TODO: I chose the max size somewhat arbitrarily. Consider setting this based // on navigator.deviceMemory, or some other heuristic. We should make this // customizable via the Next.js config, too. const maxRouteLruSize = 10 * 1024 * 1024 // 10 MB ; let routeCacheLru = (0, _lru.createLRU)(maxRouteLruSize, onRouteLRUEviction); let segmentCacheMap = (0, _tuplemap.createTupleMap)(); // NOTE: Segments and Route entries are managed by separate LRUs. We could // combine them into a single LRU, but because they are separate types, we'd // need to wrap each one in an extra LRU node (to maintain monomorphism, at the // cost of additional memory). const maxSegmentLruSize = 50 * 1024 * 1024 // 50 MB ; let segmentCacheLru = (0, _lru.createLRU)(maxSegmentLruSize, onSegmentLRUEviction); // 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 counter used to track cache invalidations. let currentCacheVersion = 0; function getCurrentCacheVersion() { return currentCacheVersion; } function revalidateEntireCache(nextUrl, tree) { currentCacheVersion++; // Clearing the cache also effectively rejects any pending requests, because // when the response is received, it gets written into a cache entry that is // no longer reachable. // TODO: There's an exception to this case that we don't currently handle // correctly: background revalidations. See note in `upsertSegmentEntry`. routeCacheMap = (0, _tuplemap.createTupleMap)(); routeCacheLru = (0, _lru.createLRU)(maxRouteLruSize, onRouteLRUEviction); segmentCacheMap = (0, _tuplemap.createTupleMap)(); segmentCacheLru = (0, _lru.createLRU)(maxSegmentLruSize, onSegmentLRUEviction); // Prefetch all the currently visible links again, to re-fill the cache. (0, _links.pingVisibleLinks)(nextUrl, tree); // Similarly, notify all invalidation listeners (i.e. those passed to // `router.prefetch(onInvalidate)`), so they can trigger a new prefetch // if needed. 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 readExactRouteCacheEntry(now, href, nextUrl) { const keypath = nextUrl === null ? [ href ] : [ href, nextUrl ]; const existingEntry = routeCacheMap.get(keypath); if (existingEntry !== null) { // Check if the entry is stale if (existingEntry.staleAt > now) { // Reuse the existing entry. // Since this is an access, move the entry to the front of the LRU. routeCacheLru.put(existingEntry); return existingEntry; } else { // Evict the stale entry from the cache. deleteRouteFromCache(existingEntry, keypath); } } return null; } function readRouteCacheEntry(now, key) { // First check if there's a non-intercepted entry. Most routes cannot be // intercepted, so this is the common case. const nonInterceptedEntry = readExactRouteCacheEntry(now, key.href, null); if (nonInterceptedEntry !== null && !nonInterceptedEntry.couldBeIntercepted) { // Found a match, and the route cannot be intercepted. We can reuse it. return nonInterceptedEntry; } // There was no match. Check again but include the Next-Url this time. return readExactRouteCacheEntry(now, key.href, key.nextUrl); } function getSegmentKeypathForTask(task, route, cacheKey) { // When a prefetch includes dynamic data, the search params are included // in the result, so we must include the search string in the segment // cache key. (Note that this is true even if the search string is empty.) // // If we're fetching using PPR, we do not need to include the search params in // the cache key, because the search params are treated as dynamic data. The // cache entry is valid for all possible search param values. const isDynamicTask = task.fetchStrategy === _segmentcache.FetchStrategy.Full || task.fetchStrategy === _segmentcache.FetchStrategy.PPRRuntime || !route.isPPREnabled; return isDynamicTask && cacheKey.endsWith('/' + _segment.PAGE_SEGMENT_KEY) ? [ cacheKey, route.renderedSearch ] : [ cacheKey ]; } function readSegmentCacheEntry(now, route, cacheKey) { if (!cacheKey.endsWith('/' + _segment.PAGE_SEGMENT_KEY)) { // Fast path. Search params only exist on page segments. return readExactSegmentCacheEntry(now, [ cacheKey ]); } const renderedSearch = route.renderedSearch; if (renderedSearch !== null) { // Page segments may or may not contain search params. If they were prefetched // using a dynamic request, then we will have an entry with search params. // Check for that case first. const entryWithSearchParams = readExactSegmentCacheEntry(now, [ cacheKey, renderedSearch ]); if (entryWithSearchParams !== null) { return entryWithSearchParams; } } // If we did not find an entry with the given search params, check for a // "fallback" entry, where the search params are treated as dynamic data. This // is the common case because PPR/static prerenders always treat search params // as dynamic. // // See corresponding logic in `getSegmentKeypathForTask`. const entryWithoutSearchParams = readExactSegmentCacheEntry(now, [ cacheKey ]); return entryWithoutSearchParams; } function readExactSegmentCacheEntry(now, keypath) { const existingEntry = segmentCacheMap.get(keypath); if (existingEntry !== null) { // Check if the entry is stale if (existingEntry.staleAt > now) { // Reuse the existing entry. // Since this is an access, move the entry to the front of the LRU. segmentCacheLru.put(existingEntry); return existingEntry; } else { // This is a stale entry. const revalidatingEntry = existingEntry.revalidating; if (revalidatingEntry !== null) { // There's a revalidation in progress. Upsert it. const upsertedEntry = upsertSegmentEntry(now, keypath, revalidatingEntry); if (upsertedEntry !== null && upsertedEntry.staleAt > now) { // We can use the upserted revalidation entry. return upsertedEntry; } } else { // Evict the stale entry from the cache. deleteSegmentFromCache(existingEntry, keypath); } } } return null; } function readRevalidatingSegmentCacheEntry(now, owner) { const existingRevalidation = owner.revalidating; if (existingRevalidation !== null) { if (existingRevalidation.staleAt > now) { // There's already a revalidation in progress. Or a previous revalidation // failed and it has not yet expired. return existingRevalidation; } else { // Clear the stale revalidation from its owner. clearRevalidatingSegmentFromOwner(owner); } } return null; } 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 readOrCreateRouteCacheEntry(now, task) { attachInvalidationListener(task); const key = task.key; const existingEntry = readRouteCacheEntry(now, key); if (existingEntry !== null) { return existingEntry; } // Create a pending entry and add it to the cache. const pendingEntry = { canonicalUrl: null, status: 0, blockedTasks: null, tree: null, head: null, isHeadPartial: true, // 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, // 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. isPPREnabled: false, renderedSearch: null, TODO_metadataStatus: 0, TODO_isHeadDynamic: false, // LRU-related fields keypath: null, next: null, prev: null, size: 0 }; const keypath = key.nextUrl === null ? [ key.href ] : [ key.href, key.nextUrl ]; routeCacheMap.set(keypath, pendingEntry); // Stash the keypath on the entry so we know how to remove it from the map // if it gets evicted from the LRU. pendingEntry.keypath = keypath; routeCacheLru.put(pendingEntry); return pendingEntry; } function 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 routeWithNoSearchParams = readRouteCacheEntry(now, (0, _cachekey.createCacheKey)(requestedUrl.origin + requestedUrl.pathname, nextUrl)); if (routeWithNoSearchParams === null || routeWithNoSearchParams.status !== 2 || // There's no point constructing an optimistic route tree if the metadata // isn't fully available, because we'll have to do a blocking // navigation anyway. routeWithNoSearchParams.isHeadPartial || // We cannot reuse this route if it has dynamic metadata. // TODO: Move the metadata out of the route cache entry so the route // tree is reusable separately from the metadata. Then we can remove // these checks. routeWithNoSearchParams.TODO_metadataStatus !== 0 || routeWithNoSearchParams.TODO_isHeadDynamic) { // 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); // 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: routeWithNoSearchParams.tree, head: routeWithNoSearchParams.head, isHeadPartial: routeWithNoSearchParams.isHeadPartial, staleAt: routeWithNoSearchParams.staleAt, couldBeIntercepted: routeWithNoSearchParams.couldBeIntercepted, isPPREnabled: routeWithNoSearchParams.isPPREnabled, // Override the rendered search with the optimistic value. renderedSearch: optimisticRenderedSearch, TODO_metadataStatus: routeWithNoSearchParams.TODO_metadataStatus, TODO_isHeadDynamic: routeWithNoSearchParams.TODO_isHeadDynamic, // LRU-related fields keypath: null, next: null, prev: null, size: 0 }; // 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 readOrCreateSegmentCacheEntry(now, task, route, cacheKey) { const keypath = getSegmentKeypathForTask(task, route, cacheKey); const existingEntry = readExactSegmentCacheEntry(now, keypath); if (existingEntry !== null) { return existingEntry; } // Create a pending entry and add it to the cache. const pendingEntry = createDetachedSegmentCacheEntry(route.staleAt); segmentCacheMap.set(keypath, pendingEntry); // Stash the keypath on the entry so we know how to remove it from the map // if it gets evicted from the LRU. pendingEntry.keypath = keypath; segmentCacheLru.put(pendingEntry); return pendingEntry; } function readOrCreateRevalidatingSegmentEntry(now, prevEntry) { const existingRevalidation = readRevalidatingSegmentCacheEntry(now, prevEntry); if (existingRevalidation !== null) { return existingRevalidation; } const pendingEntry = createDetachedSegmentCacheEntry(prevEntry.staleAt); // Background revalidations are not stored directly in the cache map or LRU; // they're stashed on the entry that they will (potentially) replace. // // Note that we don't actually ever clear this field, except when the entry // expires. When the revalidation finishes, one of two things will happen: // // 1) the revalidation is successful, `prevEntry` is removed from the cache // and garbage collected (so there's no point clearing any of its fields) // 2) the revalidation fails, and we'll use the `revalidating` field to // prevent subsequent revalidation attempts, until it expires. prevEntry.revalidating = pendingEntry; return pendingEntry; } function upsertSegmentEntry(now, keypath, 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`. const existingEntry = readExactSegmentCacheEntry(now, keypath); 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 // ``. 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 the entry on the owner's `revalidating` field // 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.loading = null; rejectedEntry.rsc = null; return null; } // Evict the existing entry from the cache. deleteSegmentFromCache(existingEntry, keypath); } segmentCacheMap.set(keypath, candidateEntry); // Stash the keypath on the entry so we know how to remove it from the map // if it gets evicted from the LRU. candidateEntry.keypath = keypath; segmentCacheLru.put(candidateEntry); return candidateEntry; } function createDetachedSegmentCacheEntry(staleAt) { const emptyEntry = { status: 0, // Default to assuming the fetch strategy will be PPR. This will be updated // when a fetch is actually initiated. fetchStrategy: _segmentcache.FetchStrategy.PPR, revalidating: null, rsc: null, loading: null, staleAt, isPartial: true, promise: null, // LRU-related fields keypath: null, next: null, prev: null, size: 0 }; return emptyEntry; } function upgradeToPendingSegment(emptyEntry, fetchStrategy) { const pendingEntry = emptyEntry; pendingEntry.status = 1; pendingEntry.fetchStrategy = fetchStrategy; return pendingEntry; } function deleteRouteFromCache(entry, keypath) { pingBlockedTasks(entry); routeCacheMap.delete(keypath); routeCacheLru.delete(entry); } function deleteSegmentFromCache(entry, keypath) { cancelEntryListeners(entry); segmentCacheMap.delete(keypath); segmentCacheLru.delete(entry); clearRevalidatingSegmentFromOwner(entry); } function clearRevalidatingSegmentFromOwner(owner) { // Revalidating segments are not stored in the cache directly; they're // stored as a field on the entry that they will (potentially) replace. So // to dispose of an existing revalidation, we just need to null out the field // on the owner. const revalidatingSegment = owner.revalidating; if (revalidatingSegment !== null) { cancelEntryListeners(revalidatingSegment); owner.revalidating = null; } } function resetRevalidatingSegmentEntry(owner) { clearRevalidatingSegmentFromOwner(owner); const emptyEntry = createDetachedSegmentCacheEntry(owner.staleAt); owner.revalidating = emptyEntry; return emptyEntry; } function onRouteLRUEviction(entry) { // The LRU evicted this entry. Remove it from the map. const keypath = entry.keypath; if (keypath !== null) { entry.keypath = null; pingBlockedTasks(entry); routeCacheMap.delete(keypath); } } function onSegmentLRUEviction(entry) { // The LRU evicted this entry. Remove it from the map. const keypath = entry.keypath; if (keypath !== null) { entry.keypath = null; cancelEntryListeners(entry); segmentCacheMap.delete(keypath); } } function cancelEntryListeners(entry) { if (entry.status === 1 && entry.promise !== null) { // There were listeners for this entry. Resolve them with `null` to indicate // that the prefetch failed. It's up to the listener to decide how to handle // this case. // 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 pingBlockedTasks(entry) { const blockedTasks = entry.blockedTasks; if (blockedTasks !== null) { for (const task of blockedTasks){ (0, _scheduler.pingPrefetchTask)(task); } entry.blockedTasks = null; } } function fulfillRouteCacheEntry(entry, tree, head, isHeadPartial, staleAt, couldBeIntercepted, canonicalUrl, renderedSearch, isPPREnabled, isHeadDynamic) { const fulfilledEntry = entry; fulfilledEntry.status = 2; fulfilledEntry.tree = tree; fulfilledEntry.head = head; fulfilledEntry.isHeadPartial = isHeadPartial; fulfilledEntry.staleAt = staleAt; fulfilledEntry.couldBeIntercepted = couldBeIntercepted; fulfilledEntry.canonicalUrl = canonicalUrl; fulfilledEntry.renderedSearch = renderedSearch; fulfilledEntry.isPPREnabled = isPPREnabled; fulfilledEntry.TODO_isHeadDynamic = isHeadDynamic; pingBlockedTasks(entry); return fulfilledEntry; } function fulfillSegmentCacheEntry(segmentCacheEntry, rsc, loading, staleAt, isPartial) { const fulfilledEntry = segmentCacheEntry; fulfilledEntry.status = 2; fulfilledEntry.rsc = rsc; fulfilledEntry.loading = loading; 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) { // Remove trailing and leading slashes const pathnameParts = renderedPathname.split('/').filter((p)=>p !== ''); const index = 0; const rootSegment = _segmentvalueencoding.ROOT_SEGMENT_CACHE_KEY; return convertTreePrefetchToRouteTree(rootTree.tree, rootSegment, null, _segmentvalueencoding.ROOT_SEGMENT_REQUEST_KEY, _segmentvalueencoding.ROOT_SEGMENT_CACHE_KEY, pathnameParts, index); } function convertTreePrefetchToRouteTree(prefetch, segment, param, requestKey, cacheKey, pathnameParts, pathnamePartsIndex) { // 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; const prefetchSlots = prefetch.slots; if (prefetchSlots !== null) { slots = {}; for(let parallelRouteKey in prefetchSlots){ const childPrefetch = prefetchSlots[parallelRouteKey]; const childParamName = childPrefetch.name; const childParamType = childPrefetch.paramType; const childServerSentParamKey = childPrefetch.paramKey; let childDoesAppearInURL; let childParam = null; let childSegment; if (childParamType !== null) { // This segment is parameterized. Get the param from the pathname. const childParamValue = (0, _routeparams.parseDynamicParamFromURLPart)(childParamType, 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 renderedSearch = ''; const childParamKey = // The server omits this field from the prefetch response when // clientParamParsing is enabled. The flag only exists while we're // testing the feature, in case there's a bug and we need to revert. // TODO: Remove once clientParamParsing is enabled everywhere. childServerSentParamKey !== null ? childServerSentParamKey : (0, _routeparams.getCacheKeyForDynamicParam)(childParamValue, renderedSearch); childParam = { name: childParamName, value: childParamValue, type: childParamType }; childSegment = [ childParamName, childParamKey, childParamType ]; childDoesAppearInURL = true; } else { childSegment = childParamName; childDoesAppearInURL = (0, _routeparams.doesStaticSegmentAppearInURL)(childParamName); } // 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); const childCacheKey = (0, _segmentvalueencoding.appendSegmentCacheKeyPart)(cacheKey, parallelRouteKey, (0, _segmentvalueencoding.createSegmentCacheKeyPart)(childRequestKeyPart, childSegment)); slots[parallelRouteKey] = convertTreePrefetchToRouteTree(childPrefetch, childSegment, childParam, childRequestKey, childCacheKey, pathnameParts, childPathnamePartsIndex); } } return { cacheKey, requestKey, segment, param, slots, isRootLayout: prefetch.isRootLayout, // This field is only relevant to dynamic routes. For a PPR/static route, // there's always some partial loading state we can fetch. hasLoadingBoundary: _types.HasLoadingBoundary.SegmentHasLoadingBoundary }; } function convertRootFlightRouterStateToRouteTree(flightRouterState) { return convertFlightRouterStateToRouteTree(flightRouterState, _segmentvalueencoding.ROOT_SEGMENT_CACHE_KEY, _segmentvalueencoding.ROOT_SEGMENT_REQUEST_KEY); } function convertFlightRouterStateToRouteTree(flightRouterState, cacheKey, requestKey) { 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 childCacheKey = (0, _segmentvalueencoding.appendSegmentCacheKeyPart)(cacheKey, parallelRouteKey, (0, _segmentvalueencoding.createSegmentCacheKeyPart)(childRequestKeyPart, childSegment)); const childTree = convertFlightRouterStateToRouteTree(childRouterState, childCacheKey, childRequestKey); if (slots === null) { slots = { [parallelRouteKey]: childTree }; } else { slots[parallelRouteKey] = childTree; } } const originalSegment = flightRouterState[0]; let segment; let param = null; if (Array.isArray(originalSegment)) { const paramCacheKey = originalSegment[1]; const paramType = originalSegment[2]; const paramValue = (0, _routeparams.getParamValueFromCacheKey)(paramCacheKey, paramType); param = { name: originalSegment[0], value: paramValue === undefined ? null : paramValue, type: originalSegment[2] }; segment = originalSegment; } else { // 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 = typeof originalSegment === 'string' && originalSegment.startsWith(_segment.PAGE_SEGMENT_KEY) ? _segment.PAGE_SEGMENT_KEY : originalSegment; } return { cacheKey, requestKey, segment, param, slots, isRootLayout: flightRouterState[4] === true, hasLoadingBoundary: flightRouterState[5] !== undefined ? flightRouterState[5] : _types.HasLoadingBoundary.SubtreeHasNoLoadingBoundary }; } 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, routeTree.isRootLayout ]; return flightRouterState; } async function fetchRouteOnCacheMiss(entry, task) { // 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 key = task.key; const href = key.href; 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; } try { 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 : /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, we perform a range request of // the first N bytes of the HTML document. The canonical URL is determined // from the response. // // Then we can use the canonical URL to request the route tree. // // 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 url = new URL(href); const htmlResponse = await fetch(href, { headers: { Range: _outputexportprefetchencoding.DOC_PREFETCH_RANGE_HEADER_VALUE } }); const partialHtml = await htmlResponse.text(); if (!(0, _outputexportprefetchencoding.doesExportedHtmlMatchBuildId)(partialHtml, (0, _appbuildid.getAppBuildId)())) { // The target page is not part of this app, or it belongs to a // different build. rejectRouteCacheEntry(entry, Date.now() + 10 * 1000); return null; } urlAfterRedirects = htmlResponse.redirected ? new URL(htmlResponse.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. const url = new URL(href); 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); // Track when the network connection closes. 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; // Regardless of the type of response, we will never receive dynamic // metadata as part of this prefetch request. const isHeadDynamic = false; if (routeIsPPREnabled) { const prefetchStream = createPrefetchResponseStream(response.body, closed.resolve, function onResponseSizeUpdate(size) { routeCacheLru.updateSize(entry, size); }); const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream); if (serverData.buildId !== (0, _appbuildid.getAppBuildId)()) { // The server build does not match the client. Treat as a 404. During // an actual navigation, the router will trigger an MPA navigation. // TODO: Consider moving the build ID to a response header so we can check // it before decoding the response, and so there's one way of checking // across all response types. // 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); const routeTree = convertRootTreePrefetchToRouteTree(serverData, renderedPathname); const staleTimeMs = getStaleTimeMs(serverData.staleTime); fulfillRouteCacheEntry(entry, routeTree, serverData.head, serverData.isHeadPartial, Date.now() + staleTimeMs, couldBeIntercepted, canonicalUrl, renderedSearch, routeIsPPREnabled, isHeadDynamic); } 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 prefetchStream = createPrefetchResponseStream(response.body, closed.resolve, function onResponseSizeUpdate(size) { routeCacheLru.updateSize(entry, size); }); const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream); if (serverData.b !== (0, _appbuildid.getAppBuildId)()) { // The server build does not match the client. Treat as a 404. During // an actual navigation, the router will trigger an MPA navigation. // TODO: Consider moving the build ID to a response header so we can check // it before decoding the response, and so there's one way of checking // across all response types. // TODO: We should cache the fact that this is an MPA navigation. rejectRouteCacheEntry(entry, Date.now() + 10 * 1000); return null; } writeDynamicTreeResponseIntoCache(Date.now(), task, // 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. _segmentcache.FetchStrategy.LoadingBoundary, response, serverData, entry, couldBeIntercepted, canonicalUrl, routeIsPPREnabled); } if (!couldBeIntercepted && nextUrl !== null) { // 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. Since we're in an async task, we must first confirm // that the entry hasn't been concurrently modified by a different task. const currentKeypath = [ href, nextUrl ]; const expectedEntry = routeCacheMap.get(currentKeypath); if (expectedEntry === entry) { routeCacheMap.delete(currentKeypath); const newKeypath = [ href ]; routeCacheMap.set(newKeypath, entry); // We don't need to update the LRU because the entry is already in it. // But since we changed the keypath, we do need to update that, so we // know how to remove it from the map if it gets evicted from the LRU. entry.keypath = newKeypath; } else { // Something else modified this entry already. Since the re-keying is // just a performance optimization, we can safely skip it. } } // 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, routeKey.href); 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; } 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; } // Track when the network connection closes. const closed = (0, _promisewithresolvers.createPromiseWithResolvers)(); // Wrap the original stream in a new stream that never closes. That way the // Flight client doesn't error if there's a hanging promise. const prefetchStream = createPrefetchResponseStream(response.body, closed.resolve, function onResponseSizeUpdate(size) { segmentCacheLru.updateSize(segmentCacheEntry, size); }); const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream); if (serverData.buildId !== (0, _appbuildid.getAppBuildId)()) { // The server build does not match the client. Treat as a 404. During // an actual navigation, the router will trigger an MPA navigation. // TODO: Consider moving the build ID to a response header so we can check // it before decoding the response, and so there's one way of checking // across all response types. rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000); return null; } return { value: fulfillSegmentCacheEntry(segmentCacheEntry, serverData.rsc, serverData.loading, // TODO: The server does not currently provide per-segment stale time. // So we use the stale time of the route. route.staleAt, serverData.isPartial), // 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 fetchSegmentPrefetchesUsingDynamicRequest(task, route, fetchStrategy, dynamicRequestTree, spawnedEntries) { const url = new URL(route.canonicalUrl, task.key.href); const nextUrl = task.key.nextUrl; const headers = { [_approuterheaders.RSC_HEADER]: '1', [_approuterheaders.NEXT_ROUTER_STATE_TREE_HEADER]: encodeURIComponent(JSON.stringify(dynamicRequestTree)) }; if (nextUrl !== null) { headers[_approuterheaders.NEXT_URL] = nextUrl; } switch(fetchStrategy){ case _segmentcache.FetchStrategy.Full: { break; } case _segmentcache.FetchStrategy.PPRRuntime: { headers[_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER] = '2'; break; } case _segmentcache.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. const closed = (0, _promisewithresolvers.createPromiseWithResolvers)(); let fulfilledEntries = null; const prefetchStream = createPrefetchResponseStream(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 assiging 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){ segmentCacheLru.updateSize(entry, averageSize); } }); const serverData = await (0, _fetchserverresponse.createFromNextReadableStream)(prefetchStream); const isResponsePartial = fetchStrategy === _segmentcache.FetchStrategy.PPRRuntime ? !!response.headers.get(_approuterheaders.NEXT_DID_POSTPONE_HEADER) : // (even if we did set the prefetch header, we only use this codepath for non-PPR-enabled routes) 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. fulfilledEntries = writeDynamicRenderResponseIntoCache(Date.now(), task, fetchStrategy, response, serverData, isResponsePartial, route, spawnedEntries); // 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, task, fetchStrategy, response, serverData, entry, couldBeIntercepted, canonicalUrl, routeIsPPREnabled) { // Get the URL that was used to render the target page. This may be different // from the URL in the request URL, if the page was rewritten. 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; // TODO: Extract to function const staleTimeHeaderSeconds = response.headers.get(_approuterheaders.NEXT_ROUTER_STALE_TIME_HEADER); const staleTimeMs = staleTimeHeaderSeconds !== null ? getStaleTimeMs(parseInt(staleTimeHeaderSeconds, 10)) : _prefetchcacheutils.STATIC_STALETIME_MS; // If the response contains dynamic holes, then we must conservatively assume // that any individual segment might contain dynamic holes, and also the // head. If it did not contain dynamic holes, then we can assume every segment // and the head is completely static. const isResponsePartial = response.headers.get(_approuterheaders.NEXT_DID_POSTPONE_HEADER) === '1'; // Since this is a dynamic response, we must conservatively assume that the // head responded with dynamic data. const isHeadDynamic = true; const fulfilledEntry = fulfillRouteCacheEntry(entry, convertRootFlightRouterStateToRouteTree(flightRouterState), flightData.head, flightData.isHeadPartial, now + staleTimeMs, couldBeIntercepted, canonicalUrl, renderedSearch, routeIsPPREnabled, isHeadDynamic); // 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: When `clientSegmentCache` is enabled, the server does not include // segment data when responding to a route tree prefetch request. However, // when `clientSegmentCache` is set to "client-only", and PPR is enabled (or // the page is fully static), the normal check is bypassed and the server // responds with the full page. This is a temporary situation until we can // remove the "client-only" option. Then, we can delete this function call. writeDynamicRenderResponseIntoCache(now, task, fetchStrategy, response, serverData, isResponsePartial, fulfilledEntry, 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, task, fetchStrategy, response, serverData, isResponsePartial, route, spawnedEntries) { if (serverData.b !== (0, _appbuildid.getAppBuildId)()) { // The server build does not match the client. Treat as a 404. During // an actual navigation, the router will trigger an MPA navigation. // TODO: Consider moving the build ID to a response header so we can check // it before decoding the response, and so there's one way of checking // across all response types. if (spawnedEntries !== null) { rejectSegmentEntriesIfStillPending(spawnedEntries, now + 10 * 1000); } return null; } const flightDatas = (0, _flightdatahelpers.normalizeFlightData)(serverData.f); if (typeof flightDatas === 'string') { // This means navigating to this route will result in an MPA navigation. // TODO: We should cache this, too, so that the MPA navigation is immediate. return null; } const staleTimeHeaderSeconds = response.headers.get(_approuterheaders.NEXT_ROUTER_STALE_TIME_HEADER); const staleTimeMs = staleTimeHeaderSeconds !== null ? getStaleTimeMs(parseInt(staleTimeHeaderSeconds, 10)) : _prefetchcacheutils.STATIC_STALETIME_MS; const staleAt = now + staleTimeMs; for (const flightData of flightDatas){ const seedData = flightData.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 = flightData.segmentPath; let requestKey = _segmentvalueencoding.ROOT_SEGMENT_REQUEST_KEY; let cacheKey = _segmentvalueencoding.ROOT_SEGMENT_CACHE_KEY; for(let i = 0; i < segmentPath.length; i += 2){ const parallelRouteKey = segmentPath[i]; const segment = segmentPath[i + 1]; const requestKeyPart = (0, _segmentvalueencoding.createSegmentRequestKeyPart)(segment); requestKey = (0, _segmentvalueencoding.appendSegmentRequestKeyPart)(requestKey, parallelRouteKey, requestKeyPart); cacheKey = (0, _segmentvalueencoding.appendSegmentCacheKeyPart)(cacheKey, parallelRouteKey, (0, _segmentvalueencoding.createSegmentCacheKeyPart)(requestKeyPart, segment)); } writeSeedDataIntoCache(now, task, fetchStrategy, route, staleAt, seedData, isResponsePartial, cacheKey, requestKey, spawnedEntries); } // During a dynamic request, the server sends back new head data for the // page. Overwrite the existing head with the new one. Note that we're // intentionally not taking into account whether the existing head is // already complete, even though the incoming head might not have finished // streaming in yet. This is to prioritize consistency of the head with // the segment data (though it's still not a guarantee, since some of the // segment data may be reused from a previous request). route.head = flightData.head; route.isHeadPartial = flightData.isHeadPartial; route.TODO_isHeadDynamic = true; // TODO: Currently the stale time of the route tree represents the // stale time of both the route tree *and* all the segment data. So we // can't just overwrite this field; we have to use whichever value is // lower. In the future, though, the plan is to track segment lifetimes // separately from the route tree lifetime. if (staleAt < route.staleAt) { route.staleAt = staleAt; } } // 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, task, fetchStrategy, route, staleAt, seedData, isResponsePartial, cacheKey, requestKey, entriesOwnedByCurrentTask) { // This function is used to write the result of a dynamic server request // (CacheNodeSeedData) into the prefetch cache. It's used in cases where we // want to treat a dynamic response as if it were static. The two examples // where this happens are (which implicitly opts // dynamic data into being static) and when prefetching a PPR-disabled route const rsc = seedData[1]; const loading = seedData[3]; const isPartial = rsc === null || isResponsePartial; // 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(cacheKey) : undefined; if (ownedEntry !== undefined) { fulfillSegmentCacheEntry(ownedEntry, rsc, loading, staleAt, isPartial); } else { // There's no matching entry. Attempt to create a new one. const possiblyNewEntry = readOrCreateSegmentCacheEntry(now, task, route, cacheKey); if (possiblyNewEntry.status === 0) { // Confirmed this is a new entry. We can fulfill it. const newEntry = possiblyNewEntry; fulfillSegmentCacheEntry(upgradeToPendingSegment(newEntry, fetchStrategy), rsc, loading, staleAt, isPartial); } 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(staleAt), fetchStrategy), rsc, loading, staleAt, isPartial); upsertSegmentEntry(now, getSegmentKeypathForTask(task, route, cacheKey), newEntry); } } // Recursively write the child data into the cache. const seedDataChildren = seedData[2]; if (seedDataChildren !== null) { for(const parallelRouteKey in seedDataChildren){ const childSeedData = seedDataChildren[parallelRouteKey]; if (childSeedData !== null) { const childSegment = childSeedData[0]; const childRequestKeyPart = (0, _segmentvalueencoding.createSegmentRequestKeyPart)(childSegment); const childRequestKey = (0, _segmentvalueencoding.appendSegmentRequestKeyPart)(requestKey, parallelRouteKey, childRequestKeyPart); const childCacheKey = (0, _segmentvalueencoding.appendSegmentCacheKeyPart)(cacheKey, parallelRouteKey, (0, _segmentvalueencoding.createSegmentCacheKeyPart)(childRequestKeyPart, childSegment)); writeSeedDataIntoCache(now, task, fetchStrategy, route, staleAt, childSeedData, isResponsePartial, childCacheKey, childRequestKey, entriesOwnedByCurrentTask); } } } } async function fetchPrefetchResponse(url, headers) { const fetchPriority = 'low'; const response = await (0, _fetchserverresponse.createFetch)(url, headers, fetchPriority); 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; } function createPrefetchResponseStream(originalFlightStream, onStreamClose, onResponseSizeUpdate) { // When PPR is enabled, prefetch streams may contain references that never // resolve, because that's how we encode dynamic data access. In the decoded // object returned by the Flight client, these are reified into hanging // promises that suspend during render, which is effectively what we want. // The UI resolves when it switches to the dynamic data stream // (via useDeferredValue(dynamic, static)). // // However, the Flight implementation currently errors if the server closes // the response before all the references are resolved. As a cheat to work // around this, we wrap the original stream in a new stream that never closes, // and therefore doesn't error. // // While processing the original stream, we also 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. // NOTE: Since prefetch responses are delivered in a single chunk, // it's not really necessary to do this streamingly, but I'm doing it // anyway in case this changes in the future. totalByteLength += value.byteLength; onResponseSizeUpdate(totalByteLength); continue; } // The server stream has closed. Exit, but intentionally do not close // the target stream. We do notify the caller, though. 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.substring(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; } 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