next
Version:
The React Framework
900 lines • 96.4 kB
JavaScript
"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 —