1125 lines
55 KiB
JavaScript
1125 lines
55 KiB
JavaScript
import { HasLoadingBoundary } from '../../../shared/lib/app-router-types';
|
|
import { matchSegment } from '../match-segments';
|
|
import { readOrCreateRouteCacheEntry, readOrCreateSegmentCacheEntry, fetchRouteOnCacheMiss, fetchSegmentOnCacheMiss, EntryStatus, fetchSegmentPrefetchesUsingDynamicRequest, convertRouteTreeToFlightRouterState, readOrCreateRevalidatingSegmentEntry, upsertSegmentEntry, upgradeToPendingSegment, waitForSegmentCacheEntry, overwriteRevalidatingSegmentCacheEntry, canNewFetchStrategyProvideMoreContent } from './cache';
|
|
import { getSegmentVaryPathForRequest } from './vary-path';
|
|
import { createCacheKey } from './cache-key';
|
|
import { FetchStrategy, PrefetchPriority } from './types';
|
|
import { getCurrentCacheVersion } from './cache';
|
|
import { addSearchParamsIfPageSegment, PAGE_SEGMENT_KEY } from '../../../shared/lib/segment';
|
|
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;
|
|
/**
|
|
* Called by the cache when revalidation occurs. Starts a cooldown period
|
|
* during which prefetch requests are blocked to allow CDN cache propagation.
|
|
*/ export 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.
|
|
ensureWorkIsScheduled();
|
|
}, REVALIDATION_COOLDOWN_MS);
|
|
}
|
|
/**
|
|
* Initiates a prefetch task for the given URL. If a prefetch for the same URL
|
|
* is already in progress, this will bump it to the top of the queue.
|
|
*
|
|
* This is not a user-facing function. By the time this is called, the href is
|
|
* expected to be validated and normalized.
|
|
*
|
|
* @param key The RouteCacheKey to prefetch.
|
|
* @param treeAtTimeOfPrefetch The app's current FlightRouterState
|
|
* @param fetchStrategy Whether to prefetch dynamic data, in addition to
|
|
* static data. This is used by `<Link prefetch={true}>`.
|
|
*/ export function schedulePrefetchTask(key, treeAtTimeOfPrefetch, fetchStrategy, priority, onInvalidate) {
|
|
// Spawn a new prefetch task
|
|
const task = {
|
|
key,
|
|
treeAtTimeOfPrefetch,
|
|
cacheVersion: getCurrentCacheVersion(),
|
|
priority,
|
|
phase: 1,
|
|
hasBackgroundWork: false,
|
|
spawnedRuntimePrefetches: null,
|
|
fetchStrategy,
|
|
sortId: sortIdCounter++,
|
|
isCanceled: false,
|
|
onInvalidate,
|
|
_heapIndex: -1
|
|
};
|
|
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.
|
|
ensureWorkIsScheduled();
|
|
return task;
|
|
}
|
|
export 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;
|
|
heapDelete(taskHeap, task);
|
|
}
|
|
export 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 = 1;
|
|
// 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 ? 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);
|
|
}
|
|
ensureWorkIsScheduled();
|
|
}
|
|
export 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.
|
|
const currentCacheVersion = getCurrentCacheVersion();
|
|
return task.cacheVersion !== currentCacheVersion || 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 === PrefetchPriority.Intent && task !== mostRecentlyHoveredLink) {
|
|
if (mostRecentlyHoveredLink !== null) {
|
|
// Bump the previously hovered link's priority down to Default.
|
|
if (mostRecentlyHoveredLink.priority !== PrefetchPriority.Background) {
|
|
mostRecentlyHoveredLink.priority = PrefetchPriority.Default;
|
|
heapResift(taskHeap, mostRecentlyHoveredLink);
|
|
}
|
|
}
|
|
mostRecentlyHoveredLink = task;
|
|
}
|
|
}
|
|
function ensureWorkIsScheduled() {
|
|
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) {
|
|
// 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 === 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.
|
|
ensureWorkIsScheduled();
|
|
}
|
|
/**
|
|
* Notify the scheduler that we've received new data for an in-progress
|
|
* prefetch. The corresponding task will be added back to the queue (unless the
|
|
* task has been canceled in the meantime).
|
|
*/ export 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);
|
|
ensureWorkIsScheduled();
|
|
}
|
|
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.cacheVersion = getCurrentCacheVersion();
|
|
const exitStatus = pingRoute(now, task);
|
|
// These fields are only valid for a single attempt. Reset them after each
|
|
// iteration of the task queue.
|
|
const hasBackgroundWork = task.hasBackgroundWork;
|
|
task.hasBackgroundWork = 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 === 1) {
|
|
// Finished prefetching the route tree. Proceed to prefetching
|
|
// the segments.
|
|
task.phase = 0;
|
|
heapResift(taskHeap, task);
|
|
} else if (hasBackgroundWork) {
|
|
// The task spawned additional background work. Reschedule the task
|
|
// at background priority.
|
|
task.priority = PrefetchPriority.Background;
|
|
heapResift(taskHeap, task);
|
|
} else {
|
|
// The prefetch is complete. Continue to the next task.
|
|
heapPop(taskHeap);
|
|
}
|
|
task = heapPeek(taskHeap);
|
|
continue;
|
|
default:
|
|
exitStatus;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* 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
|
|
* }
|
|
*/ function background(task) {
|
|
if (task.priority === PrefetchPriority.Background) {
|
|
return true;
|
|
}
|
|
task.hasBackgroundWork = true;
|
|
return false;
|
|
}
|
|
function pingRoute(now, task) {
|
|
const key = task.key;
|
|
const route = 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 = createCacheKey(url.href, key.nextUrl);
|
|
const routeWithoutSearch = readOrCreateRouteCacheEntry(now, task, keyWithoutSearch);
|
|
switch(routeWithoutSearch.status){
|
|
case EntryStatus.Empty:
|
|
{
|
|
if (background(task)) {
|
|
routeWithoutSearch.status = EntryStatus.Pending;
|
|
spawnPrefetchSubtask(fetchRouteOnCacheMiss(routeWithoutSearch, task, keyWithoutSearch));
|
|
}
|
|
break;
|
|
}
|
|
case EntryStatus.Pending:
|
|
case EntryStatus.Fulfilled:
|
|
case EntryStatus.Rejected:
|
|
{
|
|
break;
|
|
}
|
|
default:
|
|
routeWithoutSearch;
|
|
}
|
|
}
|
|
return exitStatus;
|
|
}
|
|
function pingRootRouteTree(now, task, route) {
|
|
switch(route.status){
|
|
case 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(fetchRouteOnCacheMiss(route, task, 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 = EntryStatus.Pending;
|
|
// Intentional fallthrough to the Pending branch
|
|
}
|
|
case 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 EntryStatus.Rejected:
|
|
{
|
|
// Route tree failed to load. Treat as a 404.
|
|
return 2;
|
|
}
|
|
case EntryStatus.Fulfilled:
|
|
{
|
|
if (task.phase !== 0) {
|
|
// Do not prefetch segment data until we've entered the segment 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.
|
|
const fetchStrategy = task.fetchStrategy === FetchStrategy.PPR ? route.isPPREnabled ? FetchStrategy.PPR : FetchStrategy.LoadingBoundary : task.fetchStrategy;
|
|
switch(fetchStrategy){
|
|
case 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.
|
|
pingStaticHead(now, task, route);
|
|
const exitStatus = pingSharedPartOfCacheComponentsTree(now, task, route, task.treeAtTimeOfPrefetch, tree);
|
|
if (exitStatus === 0) {
|
|
// Child yielded without finishing.
|
|
return 0;
|
|
}
|
|
const spawnedRuntimePrefetches = task.spawnedRuntimePrefetches;
|
|
if (spawnedRuntimePrefetches !== null) {
|
|
// During the first pass, we discovered segments that require a
|
|
// runtime prefetch. Do a second pass to construct a request tree.
|
|
const spawnedEntries = new Map();
|
|
pingRuntimeHead(now, task, route, spawnedEntries, FetchStrategy.PPRRuntime);
|
|
const requestTree = pingRuntimePrefetches(now, task, route, tree, spawnedRuntimePrefetches, spawnedEntries);
|
|
let needsDynamicRequest = spawnedEntries.size > 0;
|
|
if (needsDynamicRequest) {
|
|
// Perform a dynamic prefetch request and populate the cache with
|
|
// the result.
|
|
spawnPrefetchSubtask(fetchSegmentPrefetchesUsingDynamicRequest(task, route, FetchStrategy.PPRRuntime, requestTree, spawnedEntries));
|
|
}
|
|
}
|
|
return 2;
|
|
}
|
|
case FetchStrategy.Full:
|
|
case FetchStrategy.PPRRuntime:
|
|
case FetchStrategy.LoadingBoundary:
|
|
{
|
|
// 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(fetchSegmentPrefetchesUsingDynamicRequest(task, route, fetchStrategy, dynamicRequestTree, spawnedEntries));
|
|
}
|
|
return 2;
|
|
}
|
|
default:
|
|
fetchStrategy;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
route;
|
|
}
|
|
}
|
|
return 2;
|
|
}
|
|
function pingStaticHead(now, task, route) {
|
|
// The Head data for a page (metadata, viewport) 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.
|
|
pingStaticSegmentData(now, task, route, readOrCreateSegmentCacheEntry(now, FetchStrategy.PPR, route, route.metadata), task.key, route.metadata);
|
|
}
|
|
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 === FetchStrategy.LoadingBoundary ? FetchStrategy.Full : fetchStrategy);
|
|
}
|
|
// TODO: Rename dynamic -> runtime throughout this module
|
|
function pingSharedPartOfCacheComponentsTree(now, task, route, oldTree, newTree) {
|
|
// 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.
|
|
// Prefetch this segment's static data.
|
|
const segment = readOrCreateSegmentCacheEntry(now, task.fetchStrategy, route, newTree);
|
|
pingStaticSegmentData(now, task, route, segment, task.key, newTree);
|
|
// Recursively ping the children.
|
|
const oldTreeChildren = oldTree[1];
|
|
const newTreeChildren = newTree.slots;
|
|
if (newTreeChildren !== null) {
|
|
for(const parallelRouteKey in newTreeChildren){
|
|
if (!hasNetworkBandwidth(task)) {
|
|
// Stop prefetching segments until there's more bandwidth.
|
|
return 0;
|
|
}
|
|
const newTreeChild = newTreeChildren[parallelRouteKey];
|
|
const newTreeChildSegment = newTreeChild.segment;
|
|
const oldTreeChild = oldTreeChildren[parallelRouteKey];
|
|
const oldTreeChildSegment = oldTreeChild?.[0];
|
|
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);
|
|
} else {
|
|
// We've entered the "new" part of the tree. Switch
|
|
// traversal functions.
|
|
childExitStatus = pingNewPartOfCacheComponentsTree(now, task, route, newTreeChild);
|
|
}
|
|
if (childExitStatus === 0) {
|
|
// Child yielded without finishing.
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
return 2;
|
|
}
|
|
function pingNewPartOfCacheComponentsTree(now, task, route, tree) {
|
|
// 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.
|
|
// However, if the server instructs us to, we may switch to a runtime
|
|
// prefetch instead. Traverse the tree and check at each segment.
|
|
if (tree.hasRuntimePrefetch) {
|
|
// This route has a runtime prefetch response. Since we're below the shared
|
|
// layout, everything from this point should be prefetched using a single,
|
|
// combined runtime request, rather than using per-segment static requests.
|
|
// This is true even if some of the child segments are known to be fully
|
|
// static — once we've decided to perform a runtime prefetch, we might as
|
|
// well respond with the static segments in the same roundtrip. (That's how
|
|
// regular navigations work, too.) We'll still skip over segments that are
|
|
// already cached, though.
|
|
//
|
|
// It's the server's responsibility to set a reasonable value of
|
|
// `hasRuntimePrefetch`. Currently it's user-defined, but eventually, the
|
|
// server may send a value of `false` even if the user opts in, if it
|
|
// determines during build that the route is always fully static. There are
|
|
// more optimizations we can do once we implement fallback param
|
|
// tracking, too.
|
|
//
|
|
// Use the task object to collect the segments that need a runtime prefetch.
|
|
// This will signal to the outer task queue that a second traversal is
|
|
// required to construct a request tree.
|
|
if (task.spawnedRuntimePrefetches === null) {
|
|
task.spawnedRuntimePrefetches = new Set([
|
|
tree.requestKey
|
|
]);
|
|
} else {
|
|
task.spawnedRuntimePrefetches.add(tree.requestKey);
|
|
}
|
|
// Then exit the traversal without prefetching anything further.
|
|
return 2;
|
|
}
|
|
// This segment should not be runtime prefetched. Prefetch its static data.
|
|
const segment = readOrCreateSegmentCacheEntry(now, task.fetchStrategy, route, tree);
|
|
pingStaticSegmentData(now, task, route, segment, task.key, tree);
|
|
if (tree.slots !== null) {
|
|
if (!hasNetworkBandwidth(task)) {
|
|
// Stop prefetching segments until there's more bandwidth.
|
|
return 0;
|
|
}
|
|
// Recursively ping the children.
|
|
for(const parallelRouteKey in tree.slots){
|
|
const childTree = tree.slots[parallelRouteKey];
|
|
const childExitStatus = pingNewPartOfCacheComponentsTree(now, task, route, childTree);
|
|
if (childExitStatus === 0) {
|
|
// Child yielded without finishing.
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
// This segment and all its children have finished prefetching.
|
|
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 in newTreeChildren){
|
|
const newTreeChild = newTreeChildren[parallelRouteKey];
|
|
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 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.hasLoadingBoundary !== HasLoadingBoundary.SubtreeHasNoLoadingBoundary;
|
|
const requestTreeChild = subtreeHasLoadingBoundary ? pingPPRDisabledRouteTreeUpToLoadingBoundary(now, task, route, newTreeChild, null, spawnedEntries) : convertRouteTreeToFlightRouterState(newTreeChild);
|
|
requestTreeChildren[parallelRouteKey] = requestTreeChild;
|
|
break;
|
|
}
|
|
case 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 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,
|
|
newTree.isRootLayout
|
|
];
|
|
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 = readOrCreateSegmentCacheEntry(now, task.fetchStrategy, route, tree);
|
|
switch(segment.status){
|
|
case 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.
|
|
spawnedEntries.set(tree.requestKey, 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.
|
|
FetchStrategy.LoadingBoundary));
|
|
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 EntryStatus.Fulfilled:
|
|
{
|
|
// The segment is already cached.
|
|
const segmentHasLoadingBoundary = tree.hasLoadingBoundary === HasLoadingBoundary.SegmentHasLoadingBoundary;
|
|
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 convertRouteTreeToFlightRouterState(tree);
|
|
}
|
|
break;
|
|
}
|
|
case EntryStatus.Pending:
|
|
{
|
|
break;
|
|
}
|
|
case EntryStatus.Rejected:
|
|
{
|
|
break;
|
|
}
|
|
default:
|
|
segment;
|
|
}
|
|
const requestTreeChildren = {};
|
|
if (tree.slots !== null) {
|
|
for(const parallelRouteKey in tree.slots){
|
|
const childTree = tree.slots[parallelRouteKey];
|
|
requestTreeChildren[parallelRouteKey] = pingPPRDisabledRouteTreeUpToLoadingBoundary(now, task, route, childTree, refetchMarkerContext, spawnedEntries);
|
|
}
|
|
}
|
|
const requestTree = [
|
|
tree.segment,
|
|
requestTreeChildren,
|
|
null,
|
|
refetchMarker,
|
|
tree.isRootLayout
|
|
];
|
|
return requestTree;
|
|
}
|
|
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 = readOrCreateSegmentCacheEntry(now, // 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, route, tree);
|
|
let spawnedSegment = null;
|
|
switch(segment.status){
|
|
case EntryStatus.Empty:
|
|
{
|
|
// This segment is not cached. Include it in the request.
|
|
spawnedSegment = upgradeToPendingSegment(segment, fetchStrategy);
|
|
break;
|
|
}
|
|
case EntryStatus.Fulfilled:
|
|
{
|
|
// The segment is already cached.
|
|
if (segment.isPartial && 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.
|
|
spawnedSegment = pingFullSegmentRevalidation(now, route, tree, fetchStrategy);
|
|
}
|
|
break;
|
|
}
|
|
case EntryStatus.Pending:
|
|
case 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 (canNewFetchStrategyProvideMoreContent(segment.fetchStrategy, fetchStrategy)) {
|
|
spawnedSegment = pingFullSegmentRevalidation(now, route, tree, fetchStrategy);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
segment;
|
|
}
|
|
const requestTreeChildren = {};
|
|
if (tree.slots !== null) {
|
|
for(const parallelRouteKey in tree.slots){
|
|
const childTree = tree.slots[parallelRouteKey];
|
|
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,
|
|
tree.isRootLayout
|
|
];
|
|
return requestTree;
|
|
}
|
|
function pingRuntimePrefetches(now, task, route, tree, spawnedRuntimePrefetches, spawnedEntries) {
|
|
// 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.PPRRuntime);
|
|
}
|
|
let requestTreeChildren = {};
|
|
const slots = tree.slots;
|
|
if (slots !== null) {
|
|
for(const parallelRouteKey in slots){
|
|
const childTree = slots[parallelRouteKey];
|
|
requestTreeChildren[parallelRouteKey] = pingRuntimePrefetches(now, task, route, childTree, spawnedRuntimePrefetches, spawnedEntries);
|
|
}
|
|
}
|
|
// This segment is not part of the runtime prefetch. Clone the base tree.
|
|
const requestTree = [
|
|
tree.segment,
|
|
requestTreeChildren,
|
|
null,
|
|
null
|
|
];
|
|
return requestTree;
|
|
}
|
|
function pingStaticSegmentData(now, task, route, segment, routeKey, tree) {
|
|
switch(segment.status){
|
|
case EntryStatus.Empty:
|
|
// Upgrade to Pending so we know there's already a request in progress
|
|
spawnPrefetchSubtask(fetchSegmentOnCacheMiss(route, upgradeToPendingSegment(segment, FetchStrategy.PPR), routeKey, tree));
|
|
break;
|
|
case EntryStatus.Pending:
|
|
{
|
|
// There's already a request in progress. Depending on what kind of
|
|
// request it is, we may want to revalidate it.
|
|
switch(segment.fetchStrategy){
|
|
case FetchStrategy.PPR:
|
|
case FetchStrategy.PPRRuntime:
|
|
case FetchStrategy.Full:
|
|
break;
|
|
case FetchStrategy.LoadingBoundary:
|
|
// There's a pending request, but because it's using the old
|
|
// prefetching strategy, we can't be sure if it will be fulfilled by
|
|
// the response — it might be inside the loading boundary. Perform
|
|
// a revalidation, but because it's speculative, wait to do it at
|
|
// background priority.
|
|
if (background(task)) {
|
|
// TODO: Instead of speculatively revalidating, consider including
|
|
// `hasLoading` in the route tree prefetch response.
|
|
pingPPRSegmentRevalidation(now, route, routeKey, tree);
|
|
}
|
|
break;
|
|
default:
|
|
segment.fetchStrategy;
|
|
}
|
|
break;
|
|
}
|
|
case EntryStatus.Rejected:
|
|
{
|
|
// The existing entry in the cache was rejected. Depending on how it
|
|
// was originally fetched, we may or may not want to revalidate it.
|
|
switch(segment.fetchStrategy){
|
|
case FetchStrategy.PPR:
|
|
case FetchStrategy.PPRRuntime:
|
|
case FetchStrategy.Full:
|
|
break;
|
|
case FetchStrategy.LoadingBoundary:
|
|
// There's a rejected entry, but it was fetched using the loading
|
|
// boundary strategy. So the reason it wasn't returned by the server
|
|
// might just be because it was inside a loading boundary. Or because
|
|
// there was a dynamic rewrite. Revalidate it using the per-
|
|
// segment strategy.
|
|
//
|
|
// Because a rejected segment will definitely prevent the segment (and
|
|
// all of its children) from rendering, we perform this revalidation
|
|
// immediately instead of deferring it to a background task.
|
|
pingPPRSegmentRevalidation(now, route, routeKey, tree);
|
|
break;
|
|
default:
|
|
segment.fetchStrategy;
|
|
}
|
|
break;
|
|
}
|
|
case EntryStatus.Fulfilled:
|
|
break;
|
|
default:
|
|
segment;
|
|
}
|
|
// Segments do not have dependent tasks, so once the prefetch is initiated,
|
|
// there's nothing else for us to do (except write the server data into the
|
|
// entry, which is handled by `fetchSegmentOnCacheMiss`).
|
|
}
|
|
function pingPPRSegmentRevalidation(now, route, routeKey, tree) {
|
|
const revalidatingSegment = readOrCreateRevalidatingSegmentEntry(now, FetchStrategy.PPR, route, tree);
|
|
switch(revalidatingSegment.status){
|
|
case EntryStatus.Empty:
|
|
// Spawn a prefetch request and upsert the segment into the cache
|
|
// upon completion.
|
|
upsertSegmentOnCompletion(spawnPrefetchSubtask(fetchSegmentOnCacheMiss(route, upgradeToPendingSegment(revalidatingSegment, FetchStrategy.PPR), routeKey, tree)), getSegmentVaryPathForRequest(FetchStrategy.PPR, tree));
|
|
break;
|
|
case EntryStatus.Pending:
|
|
break;
|
|
case EntryStatus.Fulfilled:
|
|
case EntryStatus.Rejected:
|
|
break;
|
|
default:
|
|
revalidatingSegment;
|
|
}
|
|
}
|
|
function pingFullSegmentRevalidation(now, route, tree, fetchStrategy) {
|
|
const revalidatingSegment = readOrCreateRevalidatingSegmentEntry(now, fetchStrategy, route, tree);
|
|
if (revalidatingSegment.status === 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 = upgradeToPendingSegment(revalidatingSegment, fetchStrategy);
|
|
upsertSegmentOnCompletion(waitForSegmentCacheEntry(pendingSegment), getSegmentVaryPathForRequest(fetchStrategy, tree));
|
|
return pendingSegment;
|
|
} else {
|
|
// There's already a revalidation in progress.
|
|
const nonEmptyRevalidatingSegment = revalidatingSegment;
|
|
if (canNewFetchStrategyProvideMoreContent(nonEmptyRevalidatingSegment.fetchStrategy, fetchStrategy)) {
|
|
// The existing revalidation was fetched using a less specific strategy.
|
|
// Reset it and start a new revalidation.
|
|
const emptySegment = overwriteRevalidatingSegmentCacheEntry(fetchStrategy, route, tree);
|
|
const pendingSegment = upgradeToPendingSegment(emptySegment, fetchStrategy);
|
|
upsertSegmentOnCompletion(waitForSegmentCacheEntry(pendingSegment), getSegmentVaryPathForRequest(fetchStrategy, tree));
|
|
return pendingSegment;
|
|
}
|
|
switch(nonEmptyRevalidatingSegment.status){
|
|
case EntryStatus.Pending:
|
|
// There's already an in-progress prefetch that includes this segment.
|
|
return null;
|
|
case EntryStatus.Fulfilled:
|
|
case 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;
|
|
}
|
|
}
|
|
}
|
|
const noop = ()=>{};
|
|
function upsertSegmentOnCompletion(promise, varyPath) {
|
|
// Wait for a segment to finish loading, then upsert it into the cache
|
|
promise.then((fulfilled)=>{
|
|
if (fulfilled !== null) {
|
|
// Received new data. Attempt to replace the existing entry in the cache.
|
|
upsertSegmentEntry(Date.now(), varyPath, fulfilled);
|
|
}
|
|
}, noop);
|
|
}
|
|
function doesCurrentSegmentMatchCachedSegment(route, currentSegment, cachedSegment) {
|
|
if (cachedSegment === 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 === addSearchParamsIfPageSegment(PAGE_SEGMENT_KEY, Object.fromEntries(new URLSearchParams(route.renderedSearch)));
|
|
}
|
|
// Non-page segments are compared using the same function as the server
|
|
return matchSegment(cachedSegment, currentSegment);
|
|
}
|
|
// -----------------------------------------------------------------------------
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
//# sourceMappingURL=scheduler.js.map
|