next
Version:
The React Framework
1,770 lines • 97 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) {
// Bind the task to the segment cache map that is active right now: the
// shared map, unless the Instant Navigation Testing lock is held, in which
// case the task gets the lock scope's private map. This is the single
// place work is bound to a map based on lock state — everything downstream
// receives the map explicitly. See `segmentCacheMap` in cache.ts.
let taskSegmentCacheMap = _cache.segmentCacheMap;
if (process.env.__NEXT_EXPOSE_TESTING_API) {
const { getNavigationLockSegmentCacheMap } = require('./navigation-testing-lock');
const lockMap = getNavigationLockSegmentCacheMap();
if (lockMap !== null) {
taskSegmentCacheMap = lockMap;
}
}
// Spawn a new prefetch task
const task = {
key,
treeAtTimeOfPrefetch,
routeCacheVersion: (0, _cache.getCurrentRouteCacheVersion)(),
segmentCacheVersion: (0, _cache.getCurrentSegmentCacheVersion)(),
segmentCacheMap: taskSegmentCacheMap,
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) {
// This locked-navigation prefetch is complete: the final pass
// observed every segment response it cares about, so the data the
// navigation will read has settled. Resolve the prefetch's
// promise (awaited by `ensurePrefetchThenNavigate`) so the
// navigation proceeds against present data rather than a
// still-in-flight entry.
const { resolveNavigationLockPrefetch } = require('./navigation-testing-lock');
resolveNavigationLockPrefetch(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, task.segmentCacheMap));
}
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, task.segmentCacheMap));
// 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, task.segmentCacheMap, fetchStrategy, route.metadata),
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 — the runtime prefetch covers the whole subtree. (Unlike the
// direct deopt above, any open bundle is dropped rather than finished: a
// fulfilled InlinedIntoChild node can report a true signal while its
// chain is still open. That's safe — nothing in an un-pinged chain was
// upgraded to Pending, so no entry is stranded blocking the task, and
// Empty entries in the dropped chain are re-fetched by a later pass.)
addSpawnedRuntimePrefetch(task, tree.requestKey);
return 2;
}
if (tree.slots !== null) {
if (!hasNetworkBandwidth(task)) {
// Stop prefetching segments until there's more bandwidth.
return 0;
}
// Recursively ping the children.
for (const childTree of tree.slots.values()){
// 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 && childTree.prefetchHints & _approutertypes.PrefetchHint.ParentInlinedIntoSelf ? bundleInProgress : null;
const childResult = pingNewPartOfCacheComponentsTree(now, task, route, childTree, bundleForChild, fetchStrategy);
if (childResult === 0) {
// Child yielded without finishing.
return 0;
}
}
}
// The static attempt was sufficient for this segment (each child is its
// own decision point) — or parts of it are still in flight, in which case
// the task is blocked and the decision re-runs against the received
// responses.
return 2;
}
function diffRouteTreeAgainstCurrent(now, task, route, oldTree, newTree, spawnedEntries, fetchStrategy) {
// This is a single recursive traversal that does multiple things:
// - Finds the parts of the target route (newTree) that are not part of
// of the current page (oldTree) by diffing them, using the same algorithm
// as a real navigation.
// - Constructs a request tree (FlightRouterState) that describes which
// segments need to be prefetched and which ones are already cached.
// - Creates a set of pending cache entries for the segments that need to
// be prefetched, so that a subsequent prefetch task does not request the
// same segments again.
const oldTreeChildren = oldTree[1];
const newTreeChildren = newTree.slots;
let requestTreeChildren = {};
if (newTreeChildren !== null) {
for (const [parallelRouteKey, newTreeChild] of newTreeChildren){
const newTreeChildSegment = newTreeChild.segment;
const oldTreeChild = oldTreeChildren[parallelRouteKey];
const oldTreeChildSegment = oldTreeChild?.[0];
if (oldTreeChildSegment !== undefined && doesCurrentSegmentMatchCachedSegment(route, newTreeChildSegment, oldTreeChildSegment)) {
// This segment is already part of the current route. Keep traversing.
const requestTreeChild = diffRouteTreeAgainstCurrent(now, task, route, oldTreeChild, newTreeChild, spawnedEntries, fetchStrategy);
requestTreeChildren[parallelRouteKey] = requestTreeChild;
} else {
// This segment is not part of the current route. We're entering a
// part of the tree that we need to prefetch (unless everything is
// already cached).
switch(fetchStrategy){
case _types.FetchStrategy.LoadingBoundary:
{
// When PPR is disabled, we can't prefetch per segment. We must
// fallback to the old prefetch behavior and send a dynamic request.
// Only routes that include a loading boundary can be prefetched in
// this way.
//
// This is simlar to a "full" prefetch, but we're much more
// conservative about which segments to include in the request.
//
// The server will only render up to the first loading boundary
// inside new part of the tree. If there's no loading boundary
// anywhere in the tree, the server will never return any data, so
// we can skip the request.
const subtreeHasLoadingBoundary = (newTreeChild.prefetchHints & (_approutertypes.PrefetchHint.SegmentHasLoadingBoundary | _approutertypes.PrefetchHint.SubtreeHasLoadingBoundary)) !== 0;
const requestTreeChild = subtreeHasLoadingBoundary ? pingPPRDisabledRouteTreeUpToLoadingBoundary(now, task, route, newTreeChild, null, spawnedEntries) : (0, _cache.convertRouteTreeToFlightRouterState)(newTreeChild);
requestTreeChildren[parallelRouteKey] = requestTreeChild;
break;
}
case _types.FetchStrategy.PPRRuntime:
{
// This is a runtime prefetch. Fetch all cacheable data in the tree,
// not just the static PPR shell.
const requestTreeChild = pingRouteTreeAndIncludeDynamicData(now, task, route, newTreeChild, false, spawnedEntries, fetchStrategy);
requestTreeChildren[parallelRouteKey] = requestTreeChild;
break;
}
case _types.FetchStrategy.Full:
{
// This is a "full" prefetch. Fetch all the data in the tree, both
// static and dynamic. We issue roughly the same request that we
// would during a real navigation. The goal is that once the
// navigation occurs, the router should not have to fetch any
// additional data.
//
// Although the response will include dynamic data, opting into a
// Full prefetch — via <Link prefetch={true}> — implicitly
// instructs the cache to treat the response as "static", or non-
// dynamic, since the whole point is to cache it for
// future navigations.
//
// Construct a tree (currently a FlightRouterState) that represents
// which segments need to be prefetched and which ones are already
// cached. If the tree is empty, then we can exit. Otherwise, we'll
// send the request tree to the server and use the response to
// populate the segment cache.
const requestTreeChild = pingRouteTreeAndIncludeDynamicData(now, task, route, newTreeChild, false, spawnedEntries, fetchStrategy);
requestTreeChildren[parallelRouteKey] = requestTreeChild;
break;
}
default:
fetchStrategy;
}
}
}
}
const requestTree = [
newTree.segment,
requestTreeChildren,
null,
null
];
if (newTree.prefetchHints !== 0) {
requestTree[4] = newTree.prefetchHints;
}
return requestTree;
}
function pingPPRDisabledRouteTreeUpToLoadingBoundary(now, task, route, tree, refetchMarkerContext, spawnedEntries) {
// This function is similar to pingRouteTreeAndIncludeDynamicData, except the
// server is only going to return a minimal loading state — it will stop
// rendering at the first loading boundary. Whereas a Full prefetch is
// intentionally aggressive and tries to pretfetch all the data that will be
// needed for a navigation, a LoadingBoundary prefetch is much more
// conservative. For example, it will omit from the request tree any segment
// that is already cached, regardles of whether it's partial or full. By
// contrast, a Full prefetch will refetch partial segments.
// "inside-shared-layout" tells the server where to start looking for a
// loading boundary.
let refetchMarker = refetchMarkerContext === null ? 'inside-shared-layout' : null;
const segment = (0, _cache.readOrCreateSegmentCacheEntry)(now, task.segmentCacheMap, task.fetchStrategy, tree);
switch(segment.status){
case _cache.EntryStatus.Empty:
{
// This segment is not cached. Add a refetch marker so the server knows
// to start rendering here.
// TODO: Instead of a "refetch" marker, we could just omit this subtree's
// FlightRouterState from the request tree. I think this would probably
// already work even without any updates to the server. For consistency,
// though, I'll send the full tree and we'll look into this later as part
// of a larger redesign of the request protocol.
// Add the pending cache entry to the result map.
const pendingSegment = (0, _cache.upgradeToPendingSegment)(segment, // Set the fetch strategy to LoadingBoundary to indicate that the server
// might not include it in the pending response. If another route is able
// to issue a per-segment request, we'll do that in the background.
_types.FetchStrategy.LoadingBoundary);
spawnedEntries.set(tree.requestKey, pendingSegment);
// The pass blocks on every request it spawns, not just requests it
// finds already in flight.
blockTaskOnPendingResponse(task, pendingSegment);
if (refetchMarkerContext !== 'refetch') {
refetchMarker = refetchMarkerContext = 'refetch';
} else {
// There's already a parent with a refetch marker, so we don't need
// to add another one.
}
break;
}
case _cache.EntryStatus.Fulfilled:
{
// The segment is already cached.
const segmentHasLoadingBoundary = (tree.prefetchHints & _approutertypes.PrefetchHint.SegmentHasLoadingBoundary) !== 0;
if (segmentHasLoadingBoundary) {
// This segment has a loading boundary, which means the server won't
// render its children. So there's nothing left to prefetch along this
// path. We can bail out.
return (0, _cache.convertRouteTreeToFlightRouterState)(tree);
}
break;
}
case _cache.EntryStatus.Pending:
{
// There's another prefetch currently in progress. Don't add the refetch
// marker yet, so the server knows it can skip rendering this segment.
// The pass still depends on the in-flight response, so wait for it
// before the phase can complete.
blockTaskOnPendingResponse(task, segment);
break;
}
case _cache.EntryStatus.Rejected:
{
break;
}
default:
segment;
}
const requestTreeChildren = {};
if (tree.slots !== null) {
for (const [parallelRouteKey, childTree] of tree.slots){
requestTreeChildren[parallelRouteKey] = pingPPRDisabledRouteTreeUpToLoadingBoundary(now, task, route, childTree, refetchMarkerContext, spawnedEntries);
}
}
const requestTree = [
tree.segment,
requestTreeChildren,
null,
refetchMarker
];
if (tree.prefetchHints !== 0) {
requestTree[4] = tree.prefetchHints;
}
return requestTree;
}
/**
* Called during a pass when a segment's response hasn't been received yet —
* whether the request was just spawned by this pass or was already in flight.
* Marks the task as blocked: a phase only completes once a full pass observes
* every segment response it cares about, because later decisions (like
* whether a segment needs a follow-up runtime request) are made against the
* contents of those responses, and a phase may need to restart its work based
* on what they contain. The task is re-pinged (via pingBlockedTasks in
* cache.ts) when the entry resolves, re-running the pass against the
* received data. Only a pass that observes every response may advance the
* phase or complete the task.
*
* Never call this for an entry that's already Rejected — nothing ever pings
* a Rejected entry, so registering on one would strand the task. A rejected
* segment is simply skipped: the pass keeps prefetching the rest of the tree
* without it.
*/ function blockTaskOnPendingResponse(task, segment) {
// This state is reset after each iteration of the task queue. We use it to
// inform the scheduler that the task is blocked.
task.hasPendingResponses = true;
// Add the task to this segment's blocked tasks, so it can be rescheduled
// once the segment finishes loading.
if (segment.blockedTasks === null) {
segment.blockedTasks = new Set([
task
]);
} else {
segment.blockedTasks.add(task);
}
}
function pingRouteTreeAndIncludeDynamicData(now, task, route, tree, isInsideRefetchingParent, spawnedEntries, fetchStrategy) {
// The tree we're constructing is the same shape as the tree we're navigating
// to. But even though this is a "new" tree, some of the individual segments
// may be cached as a result of other route prefetches.
//
// So we need to find the first uncached segment along each path add an
// explicit "refetch" marker so the server knows where to start rendering.
// Once the server starts rendering along a path, it keeps rendering the
// entire subtree.
const segment = (0, _cache.readOrCreateSegmentCacheEntry)(now, task.segmentCacheMap, // Note that `fetchStrategy` might be different from `task.fetchStrategy`,
// and we have to use the former here.
// We can have a task with `FetchStrategy.PPR` where some of its segments are configured to
// always use runtime prefetching (via `export const prefetch`), and those should check for
// entries that include search params.
fetchStrategy, tree);
let spawnedSegment = null;
switch(segment.status){
case _cache.EntryStatus.Empty:
{
// This segment is not cached.
if (fetchStrategy === _types.FetchStrategy.Full) {
// Check if there's a matching entry in the bfcache. If so, fulfill the
// segment using the bfcache entry instead of issuing a new request.
const fulfilled = (0, _cache.attemptToFulfillDynamicSegmentFromBFCache)(now, segment, tree);
if (fulfilled !== null) {
break;
}
}
// Include it in the request.
spawnedSegment = (0, _cache.upgradeToPendingSegment)(segment, fetchStrategy);
break;
}
case _cache.EntryStatus.Fulfilled:
{
// The segment is already cached.
if (segment.isPartial && (0, _cache.canNewFetchStrategyProvideMoreContent)(segment.fetchStrategy, fetchStrategy)) {
// The cached segment contains dynamic holes, and was prefetched using a
// less specific strategy than the current one. This means we're in one
// of these cases:
// - we have a static prefetch, and we're doing a runtime prefetch
// - we have a static or runtime prefetch, and we're doing a Full
// prefetch (or a navigation).
// In either case, we need to include it in the request to get a more
// specific (or full) version. However, if there's a non-stale bfcache
// entry from a previous navigation, prefer that over making a new
// request.
if (fetchStrategy === _types.FetchStrategy.Full) {
const fulfilled = (0, _cache.attemptToUpgradeSegmentFromBFCache)(now, task.segmentCacheMap, tree);
if (fulfilled !== null) {
break;
}
}
spawnedSegment = pingFullSegmentRevalidation(now, task, tree, fetchStrategy);
}
break;
}
case _cache.EntryStatus.Pending:
case _cache.EntryStatus.Rejected:
{
// There's either another prefetch currently in progress, or the previous
// attempt failed. If the new strategy can provide more content, fetch it again.
if ((0, _cache.canNewFetchStrategyProvideMoreContent)(segment.fetchStrategy, fetchStrategy)) {
spawnedSegment = pingFullSegmentRevalidation(now, task, tree, fetchStrategy);
}
if (segment.status === _cache.EntryStatus.Pending) {
// A response for this segment is still in flight. The pass must
// observe it before the phase can complete.
blockTaskOnPendingResponse(task, segment);
} else {
// The segment failed to load, or the server intentionally omitted it
// from a response (both are encoded as Rejected). Skip it and keep
// prefetching the rest of the tree; the entry's staleAt governs when
// it may be retried. Don't register the task on the rejected entry —
// nothing ever pings a Rejected entry.
//
// TODO: The cache encodes real failures and intentional server
// omissions identically (both Rejected); with per-segment skipping
// this has no task-lifecycle consequence, but distinguishing them
// could still be useful someday.
}
break;
}
default:
segment;
}
if (spawnedSegment !== null) {
// A pass must observe the response for every request it spawns before
// its phase can complete — not just requests it finds already in flight.
// Block on the entry we just spawned; the task is re-pinged when it's
// fulfilled or rejected.
blockTaskOnPendingResponse(task, spawnedSegment);
}
const requestTreeChildren = {};
if (tree.slots !== null) {
for (const [parallelRouteKey, childTree] of tree.slots){
requestTreeChildren[parallelRouteKey] = pingRouteTreeAndIncludeDynamicData(now, task, route, childTree, isInsideRefetchingParent || spawnedSegment !== null, spawnedEntries, fetchStrategy);
}
}
if (spawnedSegment !== null) {
// Add the pending entry to the result map.
spawnedEntries.set(tree.requestKey, spawnedSegment);
}
// Don't bother to add a refetch marker if one is already present in a parent.
const refetchMarker = !isInsideRefetchingParent && spawnedSegment !== null ? 'refetch' : null;
const requestTree = [
tree.segment,
requestTreeChildren,
null,
refetchMarker
];
if (tree.prefetchHints !== 0) {
requestTree[4] = tree.prefetchHints;
}
return requestTree;
}
function pingRuntimePrefetches(now, task, route, tree, spawnedRuntimePrefetches, spawnedEntries, fetchStrategy) {
// Construct a request tree (FlightRouterState) for a runtime prefetch. If
// a segment is part of the runtime prefetch, the tree is constructed by
// diffing against what's already in the prefetch cache. Otherwise, we send
// a regular FlightRouterState with no special markers.
//
// See pingRouteTreeAndIncludeDynamicData for details.
if (spawnedRuntimePrefetches.has(tree.requestKey)) {
// This segment needs a runtime prefetch.
return pingRouteTreeAndIncludeDynamicData(now, task, route, tree, false, spawnedEntries, fetchStrategy);
}
let requestTreeChildren = {};
const slots = tree.slots;
if (slots !== null) {
for (const [parallelRouteKey, childTree] of slots){
requestTreeChildren[parallelRouteKey] = pingRuntimePrefetches(now, task, route, childTree, spawnedRuntimePrefetches, spawnedEntries, fetchStrategy);
}
}
// This segment is not part of the runtime prefetch. Clone the base tree.
const requestTree = [
tree.segment,
requestTreeChildren,
null,
null
];
if (tree.prefetchHints !== 0) {
requestTree[4] = tree.prefetchHints;
}
return requestTree;
}
/**
* Walk a SegmentBundle, apply status-based logic to each entry, and if any
* entries need data, spawn a single fetch request for the whole bundle.
*
* Returns true if a fulfilled entry in the bundle reported that a runtime
* request would return more content than the entry contains
* (needsRuntimeRequest, derived at write time from the response that
* produced the entry). The callers surface this signal to the per-segment
* decision point in pingNewPartOfCacheComponentsTree (and its analog for
* the head in pingStaticHead), which uses it during a static attempt to
* decide whether to fall back to a runtime prefetch.
*/ function pingSegmentBundle(now, task, route, routeKey, tree, segments, // Per-pass static walk strategy; see pingRootRouteTree where it's derived.
fetchStrategy, // False when finishing an open bundle chain on a runtime-prefetch bailout
// (finishStaticBundleOnRuntimeBailout). The finish exists only to fetch
// data the batched runtime request won't cover — Empty entries in the
// chain — so it must not spawn revalidations over entries that are
// already settled or in flight: the chain's terminal segments are inside
// the deopted subtree, and re-fetching their static bundle would at best
// duplicate the runtime request and at worst replace a runtime-complete
// entry (e.g. a runtime App Shell) with a less complete static fallback
// response.
spawnRevalidations) {
let segmentCount = 0;
let needsFetch = false;
let needsRuntimeRequest = false;
let node = segments;
while(node !== null){
segmentCount++;
const nodeEntry = node.entry;
const nodeTree = node.tree;
if (nodeEntry === null || nodeTree === null) {
node = node.parent;
continue;
}
switch(nodeEntry.status){
case _cache.EntryStatus.Empty:
(0, _cache.upgradeToPendingSegment)(nodeEntry, fetchStrategy);
needsFetch = true;
// The pass blocks on every request it spawns, not just requests it
// finds already in flight.
blockTaskOnPendingResponse(task, nodeEntry);
break;
case _cache.EntryStatus.Pending:
if (spawnRevalidations && // During a static shell attempt, never spawn revalidations — just
// wait for the in-flight response (blocked below); its sufficiency
// is checked on the re-run pass.
fetchStrategy === _types.FetchStrategy.PPR && (0, _cache.canNewFetchStrategyProvideMoreContent)(nodeEntry.fetchStrategy, fetchStrategy)) {
const revalidatingEntry = (0, _cache.readOrCreateRevalidatingSegmentEntry)(now, task.segmentCacheMap, fetchStrategy, nodeTree);
if (revalidatingEntry.status === _cache.EntryStatus.Empty) {
(0, _cache.upgradeToPendingSegment)(revalidatingEntry, fetchStrategy);
node.entry = revalidatingEntry;
needsFetch = true;
// Block on the revalidation request we just spawned, in
// addition to the original in-flight entry (blocked below).
blockTaskOnPendingResponse(task, revalidatingEntry);
} else {
node.entry = null;
}
} else {
node.entry = null;
}
blockTaskOnPendingResponse(task, nodeEntry);
break;
case _cache.EntryStatus.Rejected:
if (spawnRevalidations && // During a static shell attempt, a rejected entry is skipped
// outright: no retry revalidation, and — deliberately — no runtime
// fallback either (per-segment rejection semantics; the entry's
// staleAt governs when it may be retried). Note that the cache
// path encodes "no shell exists" (a static response whose shell
// byte offset is 0, i.e. the page wasn't produced by staged
// rendering) as a rejection too, so such segments get no shell
// prefetch at all — an edge that shouldn't occur for hint-set
// Cache Components routes.
fetchStrategy === _types.FetchStrategy.PPR && (0, _cache.canNewFetchStrategyProvideMoreContent)(nodeEntry.fetchStrategy, fetchStrategy)) {
const revalidatingEntry = (0, _cache.readOrCreateRevalidatingSegmentEntry)(now, task.segmentCacheMap, fetchStrategy, nodeTree);
if (revalidatingEntry.status === _cache.EntryStatus.Empty) {
(0, _cache.upgradeToPendingSegment)(revalidatingEntry, fetchStrategy);
node.entry = revalidatingEntry;
needsFetch = true;
// Block on the retry revalidation we just spawned, like any
// other pending response. If the retry succeeds, its upsert
// evicts the rejected entry (see evictShadowingSegmentEntries
// in cache.ts) and the re-run pass reads the healed data. If it
// rejects too, the re-run observes a settled revalidation and
// moves on.
blockTaskOnPendingResponse(task, revalidatingEntry);
} else {
node.entry = null;
}
} else {
node.entry = null;
}
break;
case _cache.EntryStatus.Fulfilled:
{
const runtimeWouldProvideMore = wouldRuntimeRequestProvideMore(nodeEntry, fetchStrategy);
if (runtimeWouldProvideMore) {
// A runtime request would return more content for this segment
// than the entry contains. Surface it via the return value, so the
// caller can deopt this subtree to a runtime prefetch.
needsRuntimeRequest = true;
}
// For entries below this phase's tier, upgrade during the phase
// itself — no background deferral, since the whole point of the
// Speculative phase is to bring the cache up to the
// per-link-concrete tier. `isPartial` ensures a complete entry isn't
// re-fetched.
//
// Exception: when a runtime request would return more AND this walk
// permits one, skip the static path entirely. The runtime request
// covers this segment and supersedes anything a static fetch could
// add, so a static upgrade would at best duplicate it — delivering
// the same content twice — and at worst replace runtime content with
// static content.
//
// When no runtime request is permitted, the signal is irrelevant:
// nothing can act on it, so it must not suppress the static upgrade.
// That's what keeps a link prefetching static content on top of a
// cached shell.
const willBeSupersededByRuntimeRequest = runtimeWouldProvideMore && walkRequiresRuntimeCompleteness(fetchStrategy, route);
// Check if we should attempt to upgrade a fallback ISR response to
// a concrete version.
const isUpgradeableISRFallbackRetry = nodeEntry.isUpgradeableISRFallback && // If the status is empty, then we haven't yet attempted to upgrade
// the fallback.
//
// If the status is fulfilled, then the fallback was
// successfully upgraded to a concrete version.
//
// Do not attempt to upgrade if the status is Pending or Rejected.
(task.fallbackRetryStatus === _cache.EntryStatus.Empty || task.fallbackRetryStatus === _cache.EntryStatus.Fulfilled);
if (spawnRevalidations && !willBeSupersededByRuntimeRequest && (nodeEntry.isPartial && (0, _cache.canNewFetchStrategyProvideMoreContent)(nodeEntry.fetchStrategy, fetchStrategy) || isUpgradeableISRFallbackRetry)) {
const revalidatingEntry = (0, _cache.readOrCreateRevalidatingSegmentEntry)(now, task.segmentCacheMap, fetchStrategy, nodeTree);
if (revalidatingEntry.status === _cache.EntryStatus.Empty) {
(0, _cache.upgradeToPendingSegment)(revalidatingEntry, fetchStrategy);
node.entry = revalidatingEntry;
needsFetch = true;
// The pass blocks on every request it spawns, including
// revalidations of an already-fulfilled entry.
blockTaskOnPendingResponse(task, revalidatingEntry);
} else {
// A non-empty revalidating entry means a request is already in
// flight (or recently settled), so we dedupe and don't issue a
// competing one — including for ISR-fallback upgrades, which then
// share the same revalidation across tasks.
node.entry = null;
if (revalidatingEntry.status === _cache.EntryStatus.Pending) {
// The deduped-against revalidation is still in flight, and this
// pass depends on its response. Wait for it before the phase
// can complete. (A settled revalidation we chose not to use
// needs no waiting and is not a prefetch failure — the base
// entry here is already Fulfilled.)
blockTaskOnPendingResponse(task, revalidatingEntry);
}
}
} else {
node.entry = null;
}
break;
}
default:
nodeEntry;
}
node = node.parent;
}
if (needsFetch) {
spawnPrefetchSubtask((0, _cache.fetchSegmentsOnCacheMiss)(task, route, routeKey, tree, segments, segmentCount, fetchStrategy));
}
return needsRuntimeRequest;
}
/**
* During the tree walk, decide whether this segment should be added to the
* in-progress bundle (if it has InlinedIntoChild) or finalize the bundle
* and ping it, triggering a fetch if any of its entries need data (if it
* doesn't). Returns the updated bundle to pass to children (null if the
* bundle was finalized here), along with the needs-runtime signal from the
* bundle ping, if one happened (always false otherwise).
*/ function accumulateSegmentBundle(now, task, route, tree, parentBundle, // Per-pass static walk strategy; see pingRootRouteTree where it's derived.
// PPR for the normal static bundling walk; StaticShell during the Shell
// phase's static App Shell attempt, whose entries are keyed at the shell
// vary paths.
fetchStrategy, // False when finishing a chain on a runtime-prefetch bailout; see
// pingSegmentBundle.
spawnRevalidations) {
// Prefetching is disabled for this segment (prefetch: 'force-disabled'):
// the server emits null for its slot, and it participates in the bundle
// chain with null tree/entry so the null-slot positions line up.
// (Partial Prefetching segments are NOT in this mask — the server emits
// static data for them unconditionally.) Intentionally not gated by the
// prefetch inlining flag: we never statically prefetch unprefetchable
// segments.
if (tree.prefetchHints & _approutertypes.StaticPrefetchDisabled) {
return {
bundle: {
tree: null,
entry: null,
parent: parentBundle
},
needsRuntimeRequest: false
};
}
const segment = (0, _cache.readOrCreateSegmentCacheEntry)(now, task.segmentCacheMap, fetchStrategy, tree);
if (process.env.__NEXT_PREFETCH_INLINING && tree.prefetchHints & _approutertypes.PrefetchHint.InlinedIntoChild) {
if (segment.status === _cache.EntryStatus.Pending) {
// The chain this entry joins may be dropped before it's ever pinged
// (see the drop sites in pingNewPartOfCacheComponentsTree), and only
// the ping blocks on Pending entries. Register on the in-flight
// response at read time instead, so the pass observes it before the
// phase can complete even if the chain is dropped. When the chain does
// get pinged, the ping's own registration dedupes against this one.
blockTaskOnPendingResponse(task, segment);
}
return {
bundle: {
tree,
entry: segment,
parent: parentBundle
},
// No bundle ping happens here, but the node's own entry may already be
// fulfilled and insufficient. Report that signal directly: the chain
// ping only reaches the terminal descendant, and if that descendant is
// itself a decision point it consumes the signal for its own subtree,
// leaving this ancestor's insufficiency invisible to the decision
// point above it.
needsRuntimeRequest: segment.status === _cache.EntryStatus.Fulfilled && wouldRuntimeRequestProvideMore(segment, fetchStrategy)
};
}
// Not bundled. Build a single-node bundle and ping it. If this page
// accepts the head (HeadInlinedIntoSelf), prepend the head's cache entry
// to the bundle.
let effectiveParent = parentBundle;
if (process.env.__NEXT_PREFETCH_INLINING && tree.prefetchHints & _approutertypes.PrefetchHint.HeadInlinedIntoSelf) {
effectiveParent = {
tree: route.metadata,
entry: (0, _cache.readOrCreateSegmentCacheEntry)(now, task.segmentCacheMap, fetchStrategy, route.metadata),
parent: parentBundle
};
}
const segments = {
tree,
entry: segment,
parent: effectiveParent
};
const needsRuntimeRequest = pingSegmentBundle(now, task, route, task.key, tree, segments, fetchStrategy, spawnRevalidations);
return {
bundle: null,
needsRuntimeRequest
};
}
function finishStaticBundleOnRuntimeBailout(now, task, route, tree, parentBundle, // The same static walk strategy the parent bundle was accumulated with.
// Any needs-runtime signal from finishing the bundle is dropped: the
// caller is already deopting this subtree to a runtime prefetch.
fetchStrategy) {
const bundle = accumulateSegmentBundle(now, task, route, tree, parentBundle, fetchStrategy, // The batched runtime request covers the deopted subtree; only fetch
// Empty entries the chain would otherwise strand — never spawn
// revalidations over settled or in-flight ones. See pingSegmentBundle.
false).bundle;
if (bundle === null) {
return;
}
if (tree.slots !== null) {
for (const childTree of tree.slots.values()){
if (childTree.prefetchHints & _approutertypes.PrefetchHint.ParentInlinedIntoSelf) {
finishStaticBundleOnRuntimeBailout(now, task, route, childTree, bundle, fetchStrategy);
return;
}
}
}
}
function pingFullSegmentRevalidation(now, task, tree, fetchStrategy) {
const revalidatingSegment = (0, _cache.readOrCreateRevalidatingSegmentEntry)(now, task.segmentCacheMap, fetchStrategy, tree);
if (revalidatingSegment.status === _cache.EntryStatus.Empty) {
// During a Full/PPRRuntime prefetch, a single dynamic request is made for all the
// segments that we need. So we don't initiate a request here directly. By
// returning a pending entry from this function, it signals to the caller
// that this segment should be included in the request that's sent to
// the server.
const pendingSegment = (0, _cache.upgradeToPendingSegment)(revalidatingSegment, fetchStrategy);
// The upsert is handled by fulfillEntrySpawnedByRuntimePrefetch
// when the dynamic prefetch response is written into the cache.
return pendingSegment;
} else {
// There's already a revalidation in progress.
const nonEmptyRevalidatingSegment = revalidatingSegment;
if ((0, _cache.canNewFetchStrategyProvideMoreContent)(nonEmptyRevalidatingSegment.fetchStrategy, fetchStrategy)) {
// The existing revalidation was fetched using a less specific strategy.
// Reset it and start a new revalidation.
const emptySegment = (0, _cache.overwriteRevalidatingSegmentCacheEntry)(now, task.segmentCacheMap, fetchStrategy, tree);
const pendingSegment = (0, _cache.upgradeToPendingSegment)(emptySegment, fetchStrategy);
// The upsert is handled by fulfillEntrySpawnedByRuntimePrefetch
// when the dynamic prefetch response is written into the cache.
return pendingSegment;
}
switch(nonEmptyRevalidatingSegment.status){
case _cache.EntryStatus.Pending:
// There's already an in-progress prefetch that includes this segment.
// The pass needs the contents of that response, too. Wait for it
// before the phase can complete.
blockTaskOnPendingResponse(task, nonEmptyRevalidatingSegment);
return null;
case _cache.EntryStatus.Fulfilled:
case _cache.EntryStatus.Rejected:
// A previous revalidation attempt finished, but we chose not to replace
// the existing entry in the cache. Don't try again until or unless the
// revalidation entry expires.
return null;
default:
nonEmptyRevalidatingSegment;
return null;
}
}
}
function doesCurrentSegmentMatchCachedSegment(route, currentSegment, cachedSegment) {
if (cachedSegment === _segment.PAGE_SEGMENT_KEY) {
// In the FlightRouterState stored by the router, the page segment has the
// rendered search params appended to the name of the segment. In the
// prefetch cache, however, this is stored separately. So, when comparing
// the router's current FlightRouterState to the cached FlightRouterState,
// we need to make sure we compare both parts of the segment.
// TODO: This is not modeled clearly. We use the same type,
// FlightRouterState, for both the CacheNode tree _and_ the prefetch cache
// _and_ the server response format, when conceptually those are three
// different things and treated in different ways. We should encode more of
// this information into the type design so mistakes are less likely.
return currentSegment === (0, _segment.addSearchParamsIfPageSegment)(_segment.PAGE_SEGMENT_KEY, (0, _routeparams.urlSearchParamsToParsedUrlQuery)(new URLSearchParams(route.renderedSearch)));
}
// Non-page segments are compared using the same function as the server
return (0, _matchsegments.matchSegment)(cachedSegment, currentSegment);
}
function subtreeHasSpeculativePrefetch(fetchStrategy, prefetchHints) {
return(// Check if this is a "full" prefetch (<Link prefetch={true}>).
fetchStrategy === _types.FetchStrategy.Full || // Check if something in this subtree is configured to be eagerly
// prefetched at the route level. Segments that don't opt into Partial
// Prefetching are marked eager, so a route without any Partial Prefetching
// still speculatively prefetches everything.
(prefetchHints & _approutertypes.PrefetchHint.SubtreeHasEagerPrefetch) !== 0);
}
// -----------------------------------------------------------------------------
// The remainder of the module is a MinHeap implementation. Try not to put any
// logic below here unless it's related to the heap algorithm. We can extract
// this to a separate module if/when we need multiple kinds of heaps.
// -----------------------------------------------------------------------------
function compareQueuePriority(a, b) {
// Since the queue is a MinHeap, this should return a positive number if b is
// higher priority than a, and a negative number if a is higher priority
// than b.
// `priority` is an integer, where higher numbers are higher priority.
const priorityDiff = b.priority - a.priority;
if (priorityDiff !== 0) {
return priorityDiff;
}
// If the priority is the same, check which phase the prefetch is in — is it
// prefetching the route tree, or the segments? Route trees are prioritized.
const phaseDiff = b.phase - a.phase;
if (phaseDiff !== 0) {
return phaseDiff;
}
// Finally, check the insertion order. `sortId` is an incrementing counter
// assigned to prefetches. We want to process the newest prefetches first.
return b.sortId - a.sortId;
}
function heapPush(heap, node) {
const index = heap.length;
heap.push(node);
node._heapIndex = index;
heapSiftUp(heap, node, index);
}
function heapPeek(heap) {
return heap.length === 0 ? null : heap[0];
}
function heapPop(heap) {
if (heap.length === 0) {
return null;
}
const first = heap[0];
first._heapIndex = -1;
const last = heap.pop();
if (last !== first) {
heap[0] = last;
last._heapIndex = 0;
heapSiftDown(heap, last, 0);
}
return first;
}
function heapDelete(heap, node) {
const index = node._heapIndex;
if (index !== -1) {
node._heapIndex = -1;
if (heap.length !== 0) {
const last = heap.pop();
if (last !== node) {
heap[index] = last;
last._heapIndex = index;
heapSiftDown(heap, last, index);
}
}
}
}
function heapResift(heap, node) {
const index = node._heapIndex;
if (index !== -1) {
if (index === 0) {
heapSiftDown(heap, node, 0);
} else {
const parentIndex = index - 1 >>> 1;
const parent = heap[parentIndex];
if (compareQueuePriority(parent, node) > 0) {
// The parent is larger. Sift up.
heapSiftUp(heap, node, index);
} else {
// The parent is smaller (or equal). Sift down.
heapSiftDown(heap, node, index);
}
}
}
}
function heapSiftUp(heap, node, i) {
let index = i;
while(index > 0){
const parentIndex = index - 1 >>> 1;
const parent = heap[parentIndex];
if (compareQueuePriority(parent, node) > 0) {
// The parent is larger. Swap positions.
heap[parentIndex] = node;
node._heapIndex = parentIndex;
heap[index] = parent;
parent._heapIndex = index;
index = parentIndex;
} else {
// The parent is smaller. Exit.
return;
}
}
}
function heapSiftDown(heap, node, i) {
let index = i;
const length = heap.length;
const halfLength = length >>> 1;
while(index < halfLength){
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// If the left or right node is smaller, swap with the smaller of those.
if (compareQueuePriority(left, node) < 0) {
if (rightIndex < length && compareQueuePriority(right, left) < 0) {
heap[index] = right;
right._heapIndex = index;
heap[rightIndex] = node;
node._heapIndex = rightIndex;
index = rightIndex;
} else {
heap[index] = left;
left._heapIndex = index;
heap[leftIndex] = node;
node._heapIndex = leftIndex;
index = leftIndex;
}
} else if (rightIndex < length && compareQueuePriority(right, node) < 0) {
heap[index] = right;
right._heapIndex = index;
heap[rightIndex] = node;
node._heapIndex = rightIndex;
index = rightIndex;
} else {
// Neither child is smaller. Exit.
return;
}
}
}
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=scheduler.js.map