UNPKG

next

Version:

The React Framework

900 lines 96.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { cancelPrefetchTask: null, isPrefetchTaskDirty: null, pingPrefetchScheduler: null, pingPrefetchTask: null, reschedulePrefetchTask: null, schedulePrefetchTask: null, startRevalidationCooldown: null, subtreeHasSpeculativePrefetch: null }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { cancelPrefetchTask: function() { return cancelPrefetchTask; }, isPrefetchTaskDirty: function() { return isPrefetchTaskDirty; }, pingPrefetchScheduler: function() { return pingPrefetchScheduler; }, pingPrefetchTask: function() { return pingPrefetchTask; }, reschedulePrefetchTask: function() { return reschedulePrefetchTask; }, schedulePrefetchTask: function() { return schedulePrefetchTask; }, startRevalidationCooldown: function() { return startRevalidationCooldown; }, subtreeHasSpeculativePrefetch: function() { return subtreeHasSpeculativePrefetch; } }); const _approutertypes = require("../../../shared/lib/app-router-types"); const _matchsegments = require("../match-segments"); const _cache = require("./cache"); const _cachekey = require("./cache-key"); const _routeparams = require("../../route-params"); const _types = require("./types"); const _segment = require("../../../shared/lib/segment"); const _lru = require("./lru"); const scheduleMicrotask = typeof queueMicrotask === 'function' ? queueMicrotask : (fn)=>Promise.resolve().then(fn).catch((error)=>setTimeout(()=>{ throw error; })); const taskHeap = []; let inProgressRequests = 0; let sortIdCounter = 0; let didScheduleMicrotask = false; // The most recently hovered (or touched, etc) link, i.e. the most recent task // scheduled at Intent priority. There's only ever a single task at Intent // priority at a time. We reserve special network bandwidth for this task only. let mostRecentlyHoveredLink = null; // CDN cache propagation delay after revalidation (in milliseconds) const REVALIDATION_COOLDOWN_MS = 300; // Timeout handle for the revalidation cooldown. When non-null, prefetch // requests are blocked to allow CDN cache propagation. let revalidationCooldownTimeoutHandle = null; function startRevalidationCooldown() { // Clear any existing timeout in case multiple revalidations happen // in quick succession. if (revalidationCooldownTimeoutHandle !== null) { clearTimeout(revalidationCooldownTimeoutHandle); } // Schedule the cooldown to expire after the delay. revalidationCooldownTimeoutHandle = setTimeout(()=>{ revalidationCooldownTimeoutHandle = null; // Retry the prefetch queue now that the cooldown has expired. pingPrefetchScheduler(); }, REVALIDATION_COOLDOWN_MS); } function schedulePrefetchTask(key, treeAtTimeOfPrefetch, fetchStrategy, priority, onInvalidate, navigationLockPrefetch) { // Spawn a new prefetch task const task = { key, treeAtTimeOfPrefetch, routeCacheVersion: (0, _cache.getCurrentRouteCacheVersion)(), segmentCacheVersion: (0, _cache.getCurrentSegmentCacheVersion)(), priority, phase: 2, hasBackgroundWork: false, hasPendingResponses: false, spawnedRuntimePrefetches: null, fetchStrategy, sortId: sortIdCounter++, isCanceled: false, fallbackRetryStatus: _cache.EntryStatus.Empty, onInvalidate, _heapIndex: -1 }; if (process.env.__NEXT_EXPOSE_TESTING_API) { task._navigationLockPrefetch = navigationLockPrefetch; } trackMostRecentlyHoveredLink(task); heapPush(taskHeap, task); // Schedule an async task to process the queue. // // The main reason we process the queue in an async task is for batching. // It's common for a single JS task/event to trigger multiple prefetches. // By deferring to a microtask, we only process the queue once per JS task. // If they have different priorities, it also ensures they are processed in // the optimal order. pingPrefetchScheduler(); return task; } function cancelPrefetchTask(task) { // Remove the prefetch task from the queue. If the task already completed, // then this is a no-op. // // We must also explicitly mark the task as canceled so that a blocked task // does not get added back to the queue when it's pinged by the network. task.isCanceled = true; // A running fallback-retry loop notices `isCanceled` when it next wakes and // bails (settling its status to Rejected), so there's nothing to clean up here. heapDelete(taskHeap, task); } function reschedulePrefetchTask(task, treeAtTimeOfPrefetch, fetchStrategy, priority) { // Bump the prefetch task to the top of the queue, as if it were a fresh // task. This is essentially the same as canceling the task and scheduling // a new one, except it reuses the original object. // // The primary use case is to increase the priority of a Link-initated // prefetch on hover. // Un-cancel the task, in case it was previously canceled. task.isCanceled = false; task.phase = 2; // Note: fallback-retry state is deliberately NOT reset here. A retry loop runs // at most once per task, even across reschedules, so a re-hover never starts a // second loop. A loop already running simply continues (it only stops on // cancel); `fallbackRetryStatus` never returns to `Empty` once it leaves it. // Assign a new sort ID to move it ahead of all other tasks at the same // priority level. (Higher sort IDs are processed first.) task.sortId = sortIdCounter++; task.priority = // If this task is the most recently hovered link, maintain its // Intent priority, even if the rescheduled priority is lower. task === mostRecentlyHoveredLink ? _types.PrefetchPriority.Intent : priority; task.treeAtTimeOfPrefetch = treeAtTimeOfPrefetch; task.fetchStrategy = fetchStrategy; trackMostRecentlyHoveredLink(task); if (task._heapIndex !== -1) { // The task is already in the queue. heapResift(taskHeap, task); } else { heapPush(taskHeap, task); } pingPrefetchScheduler(); } function isPrefetchTaskDirty(task, nextUrl, tree) { // This is used to quickly bail out of a prefetch task if the result is // guaranteed to not have changed since the task was initiated. This is // strictly an optimization — theoretically, if it always returned true, no // behavior should change because a full prefetch task will effectively // perform the same checks. return task.routeCacheVersion !== (0, _cache.getCurrentRouteCacheVersion)() || task.segmentCacheVersion !== (0, _cache.getCurrentSegmentCacheVersion)() || task.treeAtTimeOfPrefetch !== tree || task.key.nextUrl !== nextUrl; } function trackMostRecentlyHoveredLink(task) { // Track the mostly recently hovered link, i.e. the most recently scheduled // task at Intent priority. There must only be one such task at a time. if (task.priority === _types.PrefetchPriority.Intent && task !== mostRecentlyHoveredLink) { if (mostRecentlyHoveredLink !== null) { // Bump the previously hovered link's priority down to Default. if (mostRecentlyHoveredLink.priority !== _types.PrefetchPriority.Background) { mostRecentlyHoveredLink.priority = _types.PrefetchPriority.Default; heapResift(taskHeap, mostRecentlyHoveredLink); } } mostRecentlyHoveredLink = task; } } function pingPrefetchScheduler() { if (didScheduleMicrotask) { // Already scheduled a task to process the queue return; } didScheduleMicrotask = true; scheduleMicrotask(processQueueInMicrotask); } /** * Checks if we've exceeded the maximum number of concurrent prefetch requests, * to avoid saturating the browser's internal network queue. This is a * cooperative limit — prefetch tasks should check this before issuing * new requests. * * Also checks if we're within the revalidation cooldown window, during which * prefetch requests are delayed to allow CDN cache propagation. */ function hasNetworkBandwidth(task) { // When offline, don't issue any prefetch requests. The scheduler will be // re-pinged when connectivity is restored. if (process.env.__NEXT_USE_OFFLINE) { const { getOffline } = require('../offline'); if (getOffline()) { return false; } } // Check if we're within the revalidation cooldown window if (revalidationCooldownTimeoutHandle !== null) { // We're within the cooldown window. Return false to prevent prefetching. // When the cooldown expires, the timeout will call ensureWorkIsScheduled() // to retry the queue. return false; } // TODO: Also check if there's an in-progress navigation. We should never // add prefetch requests to the network queue if an actual navigation is // taking place, to ensure there's sufficient bandwidth for render-blocking // data and resources. // TODO: Consider reserving some amount of bandwidth for static prefetches. if (task.priority === _types.PrefetchPriority.Intent) { // The most recently hovered link is allowed to exceed the default limit. // // The goal is to always have enough bandwidth to start a new prefetch // request when hovering over a link. // // However, because we don't abort in-progress requests, it's still possible // we'll run out of bandwidth. When links are hovered in quick succession, // there could be multiple hover requests running simultaneously. return inProgressRequests < 12; } // The default limit is lower than the limit for a hovered link. return inProgressRequests < 4; } function spawnPrefetchSubtask(prefetchSubtask) { // When the scheduler spawns an async task, we don't await its result. // Instead, the async task writes its result directly into the cache, then // pings the scheduler to continue. // // We process server responses streamingly, so the prefetch subtask will // likely resolve before we're finished receiving all the data. The subtask // result includes a promise that resolves once the network connection is // closed. The scheduler uses this to control network bandwidth by tracking // and limiting the number of concurrent requests. inProgressRequests++; return prefetchSubtask.then((result)=>{ if (result === null) { // The prefetch task errored before it could start processing the // network stream. Assume the connection is closed. onPrefetchConnectionClosed(); return null; } // Wait for the connection to close before freeing up more bandwidth. result.closed.then(onPrefetchConnectionClosed); return result.value; }); } function onPrefetchConnectionClosed() { inProgressRequests--; // Notify the scheduler that we have more bandwidth, and can continue // processing tasks. pingPrefetchScheduler(); } function pingPrefetchTask(task) { // "Ping" a prefetch that's already in progress to notify it of new data. if (// Check if prefetch was canceled. task.isCanceled || // Check if prefetch is already queued. task._heapIndex !== -1) { return; } // Add the task back to the queue. heapPush(taskHeap, task); pingPrefetchScheduler(); } function processQueueInMicrotask() { didScheduleMicrotask = false; // We aim to minimize how often we read the current time. Since nearly all // functions in the prefetch scheduler are synchronous, we can read the time // once and pass it as an argument wherever it's needed. const now = Date.now(); // Process the task queue until we run out of network bandwidth. let task = heapPeek(taskHeap); while(task !== null && hasNetworkBandwidth(task)){ task.routeCacheVersion = (0, _cache.getCurrentRouteCacheVersion)(); task.segmentCacheVersion = (0, _cache.getCurrentSegmentCacheVersion)(); const exitStatus = pingRoute(now, task); // These fields are only valid for a single "pass" — one pingRoute // invocation for a task, which is what the comments here also call an // attempt or an iteration. Reset them after each iteration of the // task queue. const hasBackgroundWork = task.hasBackgroundWork; task.hasBackgroundWork = false; task.hasPendingResponses = false; task.spawnedRuntimePrefetches = null; switch(exitStatus){ case 0: // The task yielded because there are too many requests in progress. // Stop processing tasks until we have more bandwidth. return; case 1: // The task is blocked. It needs more data before it can proceed. // Keep the task out of the queue until the server responds. heapPop(taskHeap); // Continue to the next task task = heapPeek(taskHeap); continue; case 2: if (task.phase === 2) { // Finished prefetching the route tree. The two-phase (Shell then // Speculative) flow only applies to routes that have opted into // Partial Prefetching — either globally via the `partialPrefetching` // config or per segment (`prefetch: 'partial'` or // `'unstable_eager'`), all surfaced as the // `SubtreeHasPartialPrefetching` hint on the route tree. Every other // route skips the Shell phase and goes straight to Speculative. // // The route entry is fulfilled at this point (the RouteTree phase // just completed), so its prefetch hints are available. const route = (0, _cache.readRouteCacheEntry)(now, task.key); const routeHasPartialPrefetching = route !== null && route.status === _cache.EntryStatus.Fulfilled && (route.tree.prefetchHints & _approutertypes.PrefetchHint.SubtreeHasPartialPrefetching) !== 0; task.phase = routeHasPartialPrefetching ? 1 : 0; heapResift(taskHeap, task); } else if (task.phase === 1) { // Shell phase complete — a Done exit means the pass observed every // response it cares about (otherwise it would have exited Blocked; // see hasPendingResponses). Always advance to Speculative regardless // of whether Shell-phase work fired — Speculative is responsible // for the per-link concrete work and runs even on routes whose // shell phase was a no-op. task.phase = 0; heapResift(taskHeap, task); } else if (hasBackgroundWork) { // The task spawned additional background work. Reschedule the task // at background priority. task.priority = _types.PrefetchPriority.Background; heapResift(taskHeap, task); } else { // The prefetch is complete. Continue to the next task. // // Completion is terminal in the normal flow: a task only completes // after a full pass observed every response it cares about. In rare // cases, though, a task can complete while still registered on an // entry from an earlier pass whose subtree the final pass no longer // reached; when that entry later settles, it re-pings the completed // task. The re-run is a harmless idempotent no-op, but any // per-completion side effect added here must be idempotent or // once-guarded — in particular, the navigation-lock release below // must not fire twice (hence the nulling). if (process.env.__NEXT_EXPOSE_TESTING_API && task._navigationLockPrefetch != null) { // The scheduler has spawned every request for this locked-navigation // prefetch, so release its "still spawning" reference. The prefetch's // promise (awaited by `ensurePrefetchThenNavigate`) resolves once the // count reaches 0 — i.e. every spawned entry has also fulfilled, so // the navigation reads present data rather than a still-in-flight // entry. If everything already fulfilled, it resolves synchronously. // // TODO: Now that a task only completes after a full pass observes // every segment response it spawned or found in flight, the // navigation-testing lock's per-entry ref counting (pendingCount / // trackNavigationLockPrefetchEntry) is redundant — a single resolve // fired here would suffice. const { finishNavigationLockPrefetchSpawning } = require('./navigation-testing-lock'); finishNavigationLockPrefetchSpawning(task._navigationLockPrefetch); // Release at most once per task: a stale registration from an // earlier pass can re-ping a completed task (see above), so it can // pass through here again. task._navigationLockPrefetch = null; } heapPop(taskHeap); } task = heapPeek(taskHeap); continue; default: exitStatus; } } // Run LRU cleanup only when the scheduler is fully idle: no queued tasks and // no in-progress requests. At that point, all active prefetch tasks have // finished reading from the cache (moving recently used entries to the front // of the list), so only genuinely stale data gets evicted. if (task === null && inProgressRequests === 0) { (0, _lru.cleanup)(); } } /** * Check this during a prefetch task to determine if background work can be * performed. If so, it evaluates to `true`. Otherwise, it returns `false`, * while also scheduling a background task to run later. Usage: * * @example * if (background(task)) { * // Perform background-pri work * } * * TODO: Model "background" as a phase (like Shell / Speculative) rather * than as a priority. Conceptually it's the same pattern: defer work * until a later pass over the task. The current priority-based encoding * predates the phase model and could be unified. */ function background(task) { if (task.priority === _types.PrefetchPriority.Background) { return true; } task.hasBackgroundWork = true; return false; } function pingRoute(now, task) { const key = task.key; const route = (0, _cache.readOrCreateRouteCacheEntry)(now, task, key); const exitStatus = pingRootRouteTree(now, task, route); if (exitStatus !== 0 && key.search !== '') { // If the URL has a non-empty search string, also prefetch the pathname // without the search string. We use the searchless route tree as a base for // optimistic routing; see requestOptimisticRouteCacheEntry for details. // // Note that we don't need to prefetch any of the segment data. Just the // route tree. // // TODO: This is a temporary solution; the plan is to replace this by adding // a wildcard lookup method to the TupleMap implementation. This is // non-trivial to implement because it needs to account for things like // fallback route entries, hence this temporary workaround. const url = new URL(key.pathname, location.origin); const keyWithoutSearch = (0, _cachekey.createCacheKey)(url.href, key.nextUrl); const routeWithoutSearch = (0, _cache.readOrCreateRouteCacheEntry)(now, task, keyWithoutSearch); switch(routeWithoutSearch.status){ case _cache.EntryStatus.Empty: { if (background(task)) { routeWithoutSearch.status = _cache.EntryStatus.Pending; spawnPrefetchSubtask((0, _cache.fetchRouteOnCacheMiss)(routeWithoutSearch, keyWithoutSearch)); } break; } case _cache.EntryStatus.Pending: case _cache.EntryStatus.Fulfilled: case _cache.EntryStatus.Rejected: { break; } default: routeWithoutSearch; } } if (exitStatus === 2 && task.hasPendingResponses) { // The pass traversed the whole tree, but some segment responses haven't // arrived yet, so the current phase isn't actually complete. Block until // they do (see blockTaskOnPendingResponse for the full rationale). return 1; } return exitStatus; } function pingRootRouteTree(now, task, route) { switch(route.status){ case _cache.EntryStatus.Empty: { // Route is not yet cached, and there's no request already in progress. // Spawn a task to request the route, load it into the cache, and ping // the task to continue. // TODO: There are multiple strategies in the <Link> API for prefetching // a route. Currently we've only implemented the main one: per-segment, // static-data only. // // There's also `<Link prefetch={true}>` // which prefetch both static *and* dynamic data. // Similarly, we need to fallback to the old, per-page // behavior if PPR is disabled for a route (via the incremental opt-in). // // Those cases will be handled here. spawnPrefetchSubtask((0, _cache.fetchRouteOnCacheMiss)(route, task.key)); // If the request takes longer than a minute, a subsequent request should // retry instead of waiting for this one. When the response is received, // this value will be replaced by a new value based on the stale time sent // from the server. // TODO: We should probably also manually abort the fetch task, to reclaim // server bandwidth. route.staleAt = now + 60 * 1000; // Upgrade to Pending so we know there's already a request in progress route.status = _cache.EntryStatus.Pending; // Intentional fallthrough to the Pending branch } case _cache.EntryStatus.Pending: { // Still pending. We can't start prefetching the segments until the route // tree has loaded. Add the task to the set of blocked tasks so that it // is notified when the route tree is ready. const blockedTasks = route.blockedTasks; if (blockedTasks === null) { route.blockedTasks = new Set([ task ]); } else { blockedTasks.add(task); } return 1; } case _cache.EntryStatus.Rejected: { // Route tree failed to load. Treat as a 404. return 2; } case _cache.EntryStatus.Fulfilled: { if (task.phase === 2) { // Do not prefetch segment data during the route tree phase. return 2; } // Recursively fill in the segment tree. if (!hasNetworkBandwidth(task)) { // Stop prefetching segments until there's more bandwidth. return 0; } const tree = route.tree; // A task's fetch strategy gets set to `PPR` for any "auto" prefetch. // If it turned out that the route isn't PPR-enabled, we need to use `LoadingBoundary` instead. // We don't need to do this for runtime prefetches, because those are only available in // `cacheComponents`, where every route is PPR. let fetchStrategy; if (tree.prefetchHints & _approutertypes.PrefetchHint.SubtreeHasPartialPrefetching) { // If Partial Prefetching is enabled anywhere on the target route, // ignore the fetch strategy and switch to unified strategy used by // Cache Components (called `PPR` for now, will likely be renamed). // // In practice, this just means that a "full" prefetch (<Link // prefetch={true}>) has no effect. You're meant to use Runtime // Prefetching instead — that's the new pattern that replaces // prefetch={true}. // // The reason we check for the Partial Prefetching opt-in rather than // the `cacheComponents` flag is to support incremental adoption. // `prefetch={true}` will continue to work until you opt into // Partial Prefetching. fetchStrategy = _types.FetchStrategy.PPR; } else if (task.fetchStrategy === _types.FetchStrategy.PPR) { fetchStrategy = route.supportsPerSegmentPrefetching ? _types.FetchStrategy.PPR : _types.FetchStrategy.LoadingBoundary; } else { fetchStrategy = task.fetchStrategy; } switch(fetchStrategy){ case _types.FetchStrategy.PPR: { // For Cache Components pages, each segment may be prefetched // statically or using a runtime request, based on various // configurations and heuristics. We'll do this in two passes: first // traverse the tree and perform all the static prefetches. // // Then, if there are any segments that need a runtime request, // do another pass to perform a runtime prefetch. // Derive the static walk's parameters once per pass; the walk // functions below receive them as arguments and are phase-agnostic. // During the Shell phase the walk targets the App Shell variant of // each segment (keyed at the shell vary paths); otherwise it's the // ordinary per-segment static strategy. This is the only place the // phase is consulted — everything below keys off the strategy. const staticWalkStrategy = task.phase === 1 ? _types.FetchStrategy.StaticShell : _types.FetchStrategy.PPR; if (staticWalkStrategy === _types.FetchStrategy.PPR && !subtreeHasSpeculativePrefetch(task.fetchStrategy, tree.prefetchHints)) { // Nothing in the target route needs to be speculatively prefetched. // Bail out. (A PPR walk is the Speculative pass; same check as // the per-subtree bail in pingNewPartOfCacheComponentsTree.) return 2; } pingStaticHead(now, task, route, staticWalkStrategy); const exitStatus = pingSharedPartOfCacheComponentsTree(now, task, route, task.treeAtTimeOfPrefetch, tree, null, staticWalkStrategy); if (exitStatus === 0) { // Child yielded without finishing. return 0; } // We may need to do a runtime prefetch for one or more segments. // Before checking, we can do some fast checks to bail out of this // branch early. // // Runtime prefetches are only issued for walks that require runtime // completeness — the same per-pass predicate that produced the // deopt registrations during the traversal above; see the decision // point in pingNewPartOfCacheComponentsTree. Which segments // actually need a runtime request — registered directly, or only // as the fallback after an insufficient static attempt — was // decided there. if (walkRequiresRuntimeCompleteness(staticWalkStrategy, route)) { const runtimeStrategy = staticWalkStrategy === _types.FetchStrategy.StaticShell ? _types.FetchStrategy.RuntimeShell : _types.FetchStrategy.PPRRuntime; // spawnedRuntimePrefetches was populated during the traversal // above: every subtree in the new part of the tree that needs a // runtime prefetch — plus, during the Shell phase, the head, if // its static attempt was insufficient (see above). // // If it's null, nothing in the new part of the tree is a candidate // for runtime prefetching, and we don't fetch the head, either — // the head is runtime prefetched only if one of the segments is. const spawnedRuntimePrefetches = task.spawnedRuntimePrefetches; if (spawnedRuntimePrefetches !== null) { const spawnedEntries = new Map(); pingRuntimeHead(now, task, route, spawnedEntries, runtimeStrategy); const requestTree = pingRuntimePrefetches(now, task, route, tree, spawnedRuntimePrefetches, spawnedEntries, runtimeStrategy); if (spawnedEntries.size > 0) { spawnPrefetchSubtask((0, _cache.fetchSegmentPrefetchesUsingDynamicRequest)(task, route, runtimeStrategy, requestTree, spawnedEntries)); } } } return 2; } case _types.FetchStrategy.Full: case _types.FetchStrategy.PPRRuntime: case _types.FetchStrategy.LoadingBoundary: { if (task.phase === 1) { // Shell phase only does work on routes that use the PPR strategy // (Cache Components routes). Other strategies are Shell no-ops // and fall through to Speculative. return 2; } // Prefetch multiple segments using a single dynamic request. // TODO: We can consolidate this branch with previous one by modeling // it as if the first segment in the new tree has runtime prefetching // enabled. Will do this as a follow-up refactor. Might want to remove // the special metatdata case below first. In the meantime, it's not // really that much duplication, just would be nice to remove one of // these codepaths. const spawnedEntries = new Map(); pingRuntimeHead(now, task, route, spawnedEntries, fetchStrategy); const dynamicRequestTree = diffRouteTreeAgainstCurrent(now, task, route, task.treeAtTimeOfPrefetch, tree, spawnedEntries, fetchStrategy); let needsDynamicRequest = spawnedEntries.size > 0; if (needsDynamicRequest) { spawnPrefetchSubtask((0, _cache.fetchSegmentPrefetchesUsingDynamicRequest)(task, route, fetchStrategy, dynamicRequestTree, spawnedEntries)); } return 2; } default: fetchStrategy; } break; } default: { route; } } return 2; } /** * Prefetches the Head data for a page (metadata, viewport). The Head is not * really a route segment, in the sense that it doesn't appear in the route * tree, but we store it in the cache as if it were, using a special key. * * Symmetric with the per-segment decision point in * pingNewPartOfCacheComponentsTree: the head deopts to the runtime prefetch * path either when it requires runtime completeness and no static attempt is * happening, or when a fulfilled static head entry reported that a runtime * request would return more content than the entry contains. Deopting * registers the head under its metadata request key, which makes the runtime * gate in pingRootRouteTree fire even when every tree segment was * sufficient; pingRuntimeHead performs the actual head work. */ function pingStaticHead(now, task, route, // The per-pass static walk strategy; see pingRootRouteTree where // it's derived. fetchStrategy) { // The head is subject to the same per-pass runtime-completeness contract // as the route's segments: during an App Shell walk, and during any walk // of a Partial Prefetching route, the head needs a response at least as // complete as a runtime one. const headRequiresRuntimeCompleteness = walkRequiresRuntimeCompleteness(fetchStrategy, route); if (headRequiresRuntimeCompleteness && // The head is not a tree node — it hangs off the route root — so the // static-attempt hint is read from the root's node. (Segments read the // bit from their own node; see the decision point in // pingNewPartOfCacheComponentsTree.) (route.tree.prefetchHints & _approutertypes.PrefetchHint.ShouldAttemptStaticPrefetch) === 0) { // No static attempt: the head arrives via the runtime request instead. addSpawnedRuntimePrefetch(task, route.metadata.requestKey); return; } if (// If the head was inlined into a page's bundle (HeadOutlined is NOT set // on the root), skip the standalone fetch — the head data will arrive // as part of that page's response, and its runtime-completeness signal // is carried by that page's own entries. process.env.__NEXT_PREFETCH_INLINING && !(route.tree.prefetchHints & _approutertypes.PrefetchHint.HeadOutlined)) { return; } const segments = { tree: route.metadata, entry: (0, _cache.readOrCreateSegmentCacheEntry)(now, fetchStrategy, route.metadata, task._navigationLockPrefetch ?? null), parent: null }; const needsRuntimeRequest = pingSegmentBundle(now, task, route, task.key, route.metadata, segments, fetchStrategy, true); if (headRequiresRuntimeCompleteness && needsRuntimeRequest) { // The static attempt was insufficient for the head. Deopt to a // runtime prefetch. (Outside of runtime-completeness contexts the // head's signal is unused — a partial static head is filled in by the // navigation-time request, as with any other static segment.) addSpawnedRuntimePrefetch(task, route.metadata.requestKey); } } /** * Whether the task needs a cache entry at least as complete as a runtime * response for every segment it walks before the prefetch counts as done. * Runtime completeness is the universal contract for Partial Prefetching, * so the predicate is per pass, not per segment: * * - Every walk of a route that opts into Partial Prefetching (any segment * with a partial-prefetching config, or the global `partialPrefetching` * flag — both surfaced as SubtreeHasPartialPrefetching on the route * root), in both the Shell and Speculative phases. * - Every App Shell (StaticShell) walk, because the App Shell must be * reusable across all params by definition. (In practice this is implied * by the first case — the Shell phase only runs for Partial Prefetching * routes.) * * Routes without Partial Prefetching keep the static-only contract: their * walks prefetch static data and partial entries are acceptable — the * dynamic holes are filled by the navigation-time request. * * Note that on a Partial Prefetching route, non-eager subtrees are still * skipped by the Speculative pass of a default (auto) link — eagerness is * unaffected by this predicate. But every segment the pass DOES walk (eager * segments, and everything on a `prefetch={true}` walk) is held to the * runtime-completeness contract. The contract is affordable because most * routes carry the ShouldAttemptStaticPrefetch hint: their segments are * prefetched statically and the responses' own sufficiency signal makes a * runtime request rare. On a hint-unset route, a walked segment deopts * directly to the batched runtime request — which then serves the segment's * whole subtree, so navigations into it are complete without a * navigation-time request. * * This is also the gate for the batched runtime request at the end of * pingRootRouteTree; requiring runtime completeness does not itself mean a * runtime request is issued for a given segment — see the decision point in * pingNewPartOfCacheComponentsTree. */ function walkRequiresRuntimeCompleteness(staticWalkStrategy, route) { return staticWalkStrategy === _types.FetchStrategy.StaticShell || (route.tree.prefetchHints & _approutertypes.PrefetchHint.SubtreeHasPartialPrefetching) !== 0; } /** * The runtime counterpart of a pass's static walk strategy: the strategy the * batched runtime request uses if this walk deopts. Each phase has exactly one * — the Shell phase escalates to a runtime App Shell, the Speculative phase to * a per-link concrete runtime prefetch. */ function getRuntimeStrategyForWalk(staticWalkStrategy) { return staticWalkStrategy === _types.FetchStrategy.StaticShell ? _types.FetchStrategy.RuntimeShell : _types.FetchStrategy.PPRRuntime; } /** * Whether this phase's runtime request would return more content for a * fulfilled entry than the entry already holds. * * An entry records the tier its CONTENT achieved, not the one it was requested * at, and that tier spans both axes — so a static response that needed no * runtime data records the runtime counterpart of its own variant (see * `recordedFetchStrategy` in cache.ts). That makes this a pure tier * comparison: an entry at or above the phase's runtime tier has nothing to * gain from it. */ function wouldRuntimeRequestProvideMore(entry, staticWalkStrategy) { return (0, _cache.canNewFetchStrategyProvideMoreContent)(entry.fetchStrategy, getRuntimeStrategyForWalk(staticWalkStrategy)); } /** * Register a subtree root (or the head's metadata key) for the batched * runtime request issued by the gate at the end of pingRootRouteTree. */ function addSpawnedRuntimePrefetch(task, requestKey) { if (task.spawnedRuntimePrefetches === null) { task.spawnedRuntimePrefetches = new Set([ requestKey ]); } else { task.spawnedRuntimePrefetches.add(requestKey); } } function pingRuntimeHead(now, task, route, spawnedEntries, fetchStrategy) { pingRouteTreeAndIncludeDynamicData(now, task, route, route.metadata, false, spawnedEntries, // When prefetching the head, there's no difference between Full // and LoadingBoundary fetchStrategy === _types.FetchStrategy.LoadingBoundary ? _types.FetchStrategy.Full : fetchStrategy); } // TODO: Rename dynamic -> runtime throughout this module function pingSharedPartOfCacheComponentsTree(now, task, route, oldTree, newTree, parentBundle, // The per-pass static walk strategy; see pingRootRouteTree where // it's derived. fetchStrategy) { // When Cache Components is enabled (or PPR, or a fully static route when PPR // is disabled; those cases are treated equivalently to Cache Components), we // start by prefetching each segment individually. Once we reach the "new" // part of the tree — the part that doesn't exist on the current page — we // may choose to switch to a runtime prefetch instead, based on the // information sent by the server in the route tree. // // The traversal starts in the "shared" part of the tree. Once we reach the // "new" part of the tree, we switch to a different traversal, // pingNewPartOfCacheComponentsTree. // The shared part of the tree always performs the ordinary static (PPR) // prefetch, regardless of phase. Phase-specific strategies — the runtime // shell request and the Shell phase's StaticShell walk — apply only to the // new part of the tree, so the per-pass walk strategy is irrelevant here. // (The needs-runtime signal is ignored: shared segments are already // rendered on the current page, so a runtime prefetch has nothing to add.) const bundleInProgress = accumulateSegmentBundle(now, task, route, newTree, parentBundle, _types.FetchStrategy.PPR, true).bundle; // Recursively ping the children. const oldTreeChildren = oldTree[1]; const newTreeChildren = newTree.slots; if (newTreeChildren !== null) { for (const [parallelRouteKey, newTreeChild] of newTreeChildren){ if (!hasNetworkBandwidth(task)) { // Stop prefetching segments until there's more bandwidth. return 0; } const newTreeChildSegment = newTreeChild.segment; const oldTreeChild = oldTreeChildren[parallelRouteKey]; const oldTreeChildSegment = oldTreeChild?.[0]; // Only pass the bundle to the child that accepts it. A parent is // only ever bundled into one child. const bundleForChild = process.env.__NEXT_PREFETCH_INLINING && bundleInProgress !== null && newTreeChild.prefetchHints & _approutertypes.PrefetchHint.ParentInlinedIntoSelf ? bundleInProgress : null; let childExitStatus; if (oldTreeChildSegment !== undefined && doesCurrentSegmentMatchCachedSegment(route, newTreeChildSegment, oldTreeChildSegment)) { // We're still in the "shared" part of the tree. childExitStatus = pingSharedPartOfCacheComponentsTree(now, task, route, oldTreeChild, newTreeChild, bundleForChild, fetchStrategy); } else { // We've entered the "new" part of the tree. Switch // traversal functions. // // Bundle chains must not cross the strategy boundary: the shared // part walks at PPR while a Shell-phase new part walks at // StaticShell, and a chain spanning both would fulfill the shared // parent's concrete-path entry with shell-variant data. Nor may we // finish the chain by fetching the new-part child at PPR here — // that would prefetch new-part segments at the concrete tier // during the Shell phase, which only the Speculative phase is // allowed to do. So drop the bundle instead, exactly like the // Speculative walk's subtree bail does when a chain crosses into a // subtree it skips: nothing in a dropped chain was upgraded to // Pending, so no entry is stranded, and the inlined shared data is // fetched by the Speculative pass whenever its walk of the new // part permits the child fetch. const bundleForNewPart = fetchStrategy === _types.FetchStrategy.StaticShell ? null : bundleForChild; childExitStatus = pingNewPartOfCacheComponentsTree(now, task, route, newTreeChild, bundleForNewPart, fetchStrategy); } if (childExitStatus === 0) { // Child yielded without finishing. return 0; } } } return 2; } function pingNewPartOfCacheComponentsTree(now, task, route, tree, parentBundle, // The per-pass static walk strategy; see pingRootRouteTree where // it's derived. fetchStrategy) { // We're now prefetching in the "new" part of the tree, the part that // doesn't exist on the current page. (In other words, we're deeper than // the shared layouts.) Segments in here default to being prefetched // statically, at the per-pass strategy derived in pingRootRouteTree. // // When the walk requires runtime completeness — an entry at least as // complete as a runtime response for every segment before the prefetch // can complete (see walkRequiresRuntimeCompleteness) — this function is // also the per-segment decision point. If the segment's node carries the // ShouldAttemptStaticPrefetch hint (the build-time prerender accessed no // runtime data), its subtree is prefetched statically first, // and the responses themselves decide whether that was enough: every // fulfilled entry carries a needsRuntimeRequest signal. Pending responses // block the task, so the attempt is serial, never raced: static attempt → // observe → runtime only if needed. Without the hint, the segment deopts // directly. Deopting registers the segment's request key in // spawnedRuntimePrefetches; the runtime gate at the end of // pingRootRouteTree issues a single batched runtime request for // everything that accumulated, and that request re-fetches the whole // subtree, so the walk stops descending at a deopt. // // Outside a runtime-completeness walk the same needsRuntimeRequest signal // is routine and ignored — any partial entry of a page that accesses // runtime data carries it, and the dynamic holes are filled by the // navigation-time request. if (// Only the Speculative pass skips subtrees with nothing to speculatively // prefetch. (It's also the only pass that walks at FetchStrategy.PPR; // the Shell phase walks at StaticShell and covers the whole new tree.) fetchStrategy === _types.FetchStrategy.PPR && !subtreeHasSpeculativePrefetch(task.fetchStrategy, tree.prefetchHints)) { // Nothing in the new part of the tree needs to be speculatively prefetched. // Bail out. return 2; } // Constant for the whole pass; recomputed here only because the walk is // recursive and the check is cheap. const segmentRequiresRuntimeCompleteness = walkRequiresRuntimeCompleteness(fetchStrategy, route); // TODO: The static-attempt hint reflects the build-time prerender's whole // runtime-data tracking, so a page that always accesses // runtime data after the shell stage never attempts a static prefetch — // even though its shell variant is rewindable at the shell boundary and // perfectly reusable. The server could emit a second bit derived from the // shell-stage value ("a static SHELL attempt is worthwhile even though // the page accesses runtime data post-shell") to let such pages attempt // static, too. // A force-disabled segment deliberately does NOT deopt here: disabling // prefetch is passive. It never initiates a request — its accumulation // below contributes nothing — and must never be the reason a runtime // prefetch spawns, though it may ride along in a runtime response issued // on another segment's behalf. const attemptStaticPrefetchOfSegment = (tree.prefetchHints & _approutertypes.PrefetchHint.ShouldAttemptStaticPrefetch) !== 0; if (segmentRequiresRuntimeCompleteness && !attemptStaticPrefetchOfSegment) { // Deopt directly to a runtime prefetch, without a static attempt. addSpawnedRuntimePrefetch(task, tree.requestKey); // If there's a pending static bundle from a parent, we need to finish // prefetching it before bailing out to runtime prefetching. if (parentBundle !== null) { finishStaticBundleOnRuntimeBailout(now, task, route, tree, parentBundle, fetchStrategy); } return 2; } // Prefetch this segment and its subtree statically, using the normal // static bundling walk. const accumulation = accumulateSegmentBundle(now, task, route, tree, parentBundle, fetchStrategy, true); const bundleInProgress = accumulation.bundle; if (segmentRequiresRuntimeCompleteness && accumulation.needsRuntimeRequest) { // The static attempt for this segment was insufficient. Stop the walk // and deopt —