rag-context-optimizer / frontend /node_modules /next /dist /client /components /segment-cache-impl /scheduler.js
| ; | |
| Object.defineProperty(exports, "__esModule", { | |
| value: true | |
| }); | |
| 0 && (module.exports = { | |
| cancelPrefetchTask: null, | |
| isPrefetchTaskDirty: null, | |
| pingPrefetchTask: null, | |
| reschedulePrefetchTask: null, | |
| schedulePrefetchTask: 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; | |
| }, | |
| pingPrefetchTask: function() { | |
| return pingPrefetchTask; | |
| }, | |
| reschedulePrefetchTask: function() { | |
| return reschedulePrefetchTask; | |
| }, | |
| schedulePrefetchTask: function() { | |
| return schedulePrefetchTask; | |
| } | |
| }); | |
| const _types = require("../../../server/app-render/types"); | |
| const _matchsegments = require("../match-segments"); | |
| const _cache = require("./cache"); | |
| const _segmentcache = require("../segment-cache"); | |
| const _segment = require("../../../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; | |
| function schedulePrefetchTask(key, treeAtTimeOfPrefetch, fetchStrategy, priority, onInvalidate) { | |
| // Spawn a new prefetch task | |
| const task = { | |
| key, | |
| treeAtTimeOfPrefetch, | |
| cacheVersion: (0, _segmentcache.getCurrentCacheVersion)(), | |
| priority, | |
| phase: 1, | |
| hasBackgroundWork: false, | |
| 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; | |
| } | |
| 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); | |
| } | |
| 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 ? _segmentcache.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(); | |
| } | |
| 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 = (0, _segmentcache.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 === _segmentcache.PrefetchPriority.Intent && task !== mostRecentlyHoveredLink) { | |
| if (mostRecentlyHoveredLink !== null) { | |
| // Bump the previously hovered link's priority down to Default. | |
| if (mostRecentlyHoveredLink.priority !== _segmentcache.PrefetchPriority.Background) { | |
| mostRecentlyHoveredLink.priority = _segmentcache.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. | |
| */ function hasNetworkBandwidth(task) { | |
| // 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 === _segmentcache.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(); | |
| } | |
| 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 = (0, _segmentcache.getCurrentCacheVersion)(); | |
| const route = (0, _cache.readOrCreateRouteCacheEntry)(now, task); | |
| const exitStatus = pingRootRouteTree(now, task, route); | |
| // The `hasBackgroundWork` field is only valid for a single attempt. Reset | |
| // it immediately upon exit. | |
| const hasBackgroundWork = task.hasBackgroundWork; | |
| task.hasBackgroundWork = false; | |
| 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 = _segmentcache.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 === _segmentcache.PrefetchPriority.Background) { | |
| return true; | |
| } | |
| task.hasBackgroundWork = true; | |
| return false; | |
| } | |
| 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)); | |
| // 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 !== 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 === _segmentcache.FetchStrategy.PPR ? route.isPPREnabled ? _segmentcache.FetchStrategy.PPR : _segmentcache.FetchStrategy.LoadingBoundary : task.fetchStrategy; | |
| switch(fetchStrategy){ | |
| case _segmentcache.FetchStrategy.PPR: | |
| // Individually prefetch the static shell for each segment. This is | |
| // the default prefetching behavior for static routes, or when PPR is | |
| // enabled. It will not include any dynamic data. | |
| return pingPPRRouteTree(now, task, route, tree); | |
| case _segmentcache.FetchStrategy.Full: | |
| case _segmentcache.FetchStrategy.PPRRuntime: | |
| case _segmentcache.FetchStrategy.LoadingBoundary: | |
| { | |
| // Prefetch multiple segments using a single dynamic request. | |
| const spawnedEntries = new Map(); | |
| const dynamicRequestTree = diffRouteTreeAgainstCurrent(now, task, route, task.treeAtTimeOfPrefetch, tree, spawnedEntries, fetchStrategy); | |
| let needsDynamicRequest = spawnedEntries.size > 0; | |
| if (!needsDynamicRequest && route.isHeadPartial && route.TODO_metadataStatus === _cache.EntryStatus.Empty) { | |
| // All the segment data is already cached, however, we need to issue | |
| // a request anyway so we can prefetch the head. Update the status | |
| // field to prevent additional requests from being spawned while | |
| // this one is in progress. | |
| // TODO: This is a temporary, targeted solution to fix a regression | |
| // we found. It exists to prevent the scheduler from sending a | |
| // redundant request if there's already one in progress. | |
| // Essentially, it will attempt once at most, then give up until the | |
| // route entry expires or is evicted by other means. But because | |
| // this doesn't have its own stale time separate from the route | |
| // itself, there will be edge cases where the metadata fails to be | |
| // fully prefetched. Consider caching metadata using a separate | |
| // entry type so we can model this more cleanly. The circumstances | |
| // that lead to this branch running in the first place are | |
| // relatively rare, so it's not critical. | |
| route.TODO_metadataStatus = _cache.EntryStatus.Fulfilled; | |
| needsDynamicRequest = true; | |
| // This instructs the server to only send the metadata. | |
| dynamicRequestTree[3] = 'metadata-only'; | |
| // We can null out the children to reduce the request size, since | |
| // they won't be needed. | |
| dynamicRequestTree[1] = {}; | |
| } | |
| if (needsDynamicRequest) { | |
| // Perform a dynamic prefetch request and populate the cache with | |
| // the result | |
| spawnPrefetchSubtask((0, _cache.fetchSegmentPrefetchesUsingDynamicRequest)(task, route, fetchStrategy, dynamicRequestTree, spawnedEntries)); | |
| } | |
| return 2; | |
| } | |
| default: | |
| fetchStrategy; | |
| } | |
| break; | |
| } | |
| default: | |
| { | |
| route; | |
| } | |
| } | |
| return 2; | |
| } | |
| function pingPPRRouteTree(now, task, route, tree) { | |
| const segment = (0, _cache.readOrCreateSegmentCacheEntry)(now, task, route, tree.cacheKey); | |
| pingPerSegment(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 = pingPPRRouteTree(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 == null ? void 0 : 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 _segmentcache.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 !== _types.HasLoadingBoundary.SubtreeHasNoLoadingBoundary; | |
| const requestTreeChild = subtreeHasLoadingBoundary ? pingPPRDisabledRouteTreeUpToLoadingBoundary(now, task, route, newTreeChild, null, spawnedEntries) : (0, _cache.convertRouteTreeToFlightRouterState)(newTreeChild); | |
| requestTreeChildren[parallelRouteKey] = requestTreeChild; | |
| break; | |
| } | |
| case _segmentcache.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 _segmentcache.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 = (0, _cache.readOrCreateSegmentCacheEntry)(now, task, route, tree.cacheKey); | |
| 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. | |
| spawnedEntries.set(tree.cacheKey, (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. | |
| _segmentcache.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 _cache.EntryStatus.Fulfilled: | |
| { | |
| // The segment is already cached. | |
| const segmentHasLoadingBoundary = tree.hasLoadingBoundary === _types.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 (0, _cache.convertRouteTreeToFlightRouterState)(tree); | |
| } | |
| break; | |
| } | |
| case _cache.EntryStatus.Pending: | |
| { | |
| break; | |
| } | |
| case _cache.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 = (0, _cache.readOrCreateSegmentCacheEntry)(now, task, route, tree.cacheKey); | |
| let spawnedSegment = null; | |
| switch(segment.status){ | |
| case _cache.EntryStatus.Empty: | |
| { | |
| // This segment is not cached. 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. | |
| spawnedSegment = pingFullSegmentRevalidation(now, task, route, segment, 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, route, segment, 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.cacheKey, 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 pingPerSegment(now, task, route, segment, routeKey, tree) { | |
| switch(segment.status){ | |
| case _cache.EntryStatus.Empty: | |
| // Upgrade to Pending so we know there's already a request in progress | |
| spawnPrefetchSubtask((0, _cache.fetchSegmentOnCacheMiss)(route, (0, _cache.upgradeToPendingSegment)(segment, _segmentcache.FetchStrategy.PPR), routeKey, tree)); | |
| break; | |
| case _cache.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 _segmentcache.FetchStrategy.PPR: | |
| case _segmentcache.FetchStrategy.PPRRuntime: | |
| case _segmentcache.FetchStrategy.Full: | |
| break; | |
| case _segmentcache.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, task, segment, route, routeKey, tree); | |
| } | |
| break; | |
| default: | |
| segment.fetchStrategy; | |
| } | |
| break; | |
| } | |
| case _cache.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 _segmentcache.FetchStrategy.PPR: | |
| case _segmentcache.FetchStrategy.PPRRuntime: | |
| case _segmentcache.FetchStrategy.Full: | |
| break; | |
| case _segmentcache.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, task, segment, route, routeKey, tree); | |
| break; | |
| default: | |
| segment.fetchStrategy; | |
| } | |
| break; | |
| } | |
| case _cache.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, task, currentSegment, route, routeKey, tree) { | |
| const revalidatingSegment = (0, _cache.readOrCreateRevalidatingSegmentEntry)(now, currentSegment); | |
| switch(revalidatingSegment.status){ | |
| case _cache.EntryStatus.Empty: | |
| // Spawn a prefetch request and upsert the segment into the cache | |
| // upon completion. | |
| upsertSegmentOnCompletion(task, route, tree.cacheKey, spawnPrefetchSubtask((0, _cache.fetchSegmentOnCacheMiss)(route, (0, _cache.upgradeToPendingSegment)(revalidatingSegment, _segmentcache.FetchStrategy.PPR), routeKey, tree))); | |
| break; | |
| case _cache.EntryStatus.Pending: | |
| break; | |
| case _cache.EntryStatus.Fulfilled: | |
| case _cache.EntryStatus.Rejected: | |
| break; | |
| default: | |
| revalidatingSegment; | |
| } | |
| } | |
| function pingFullSegmentRevalidation(now, task, route, currentSegment, tree, fetchStrategy) { | |
| const revalidatingSegment = (0, _cache.readOrCreateRevalidatingSegmentEntry)(now, currentSegment); | |
| 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); | |
| upsertSegmentOnCompletion(task, route, tree.cacheKey, (0, _cache.waitForSegmentCacheEntry)(pendingSegment)); | |
| 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.resetRevalidatingSegmentEntry)(nonEmptyRevalidatingSegment); | |
| const pendingSegment = (0, _cache.upgradeToPendingSegment)(emptySegment, fetchStrategy); | |
| upsertSegmentOnCompletion(task, route, tree.cacheKey, (0, _cache.waitForSegmentCacheEntry)(pendingSegment)); | |
| return pendingSegment; | |
| } | |
| switch(nonEmptyRevalidatingSegment.status){ | |
| case _cache.EntryStatus.Pending: | |
| // There's already an in-progress prefetch that includes this segment. | |
| 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; | |
| } | |
| } | |
| } | |
| const noop = ()=>{}; | |
| function upsertSegmentOnCompletion(task, route, cacheKey, promise) { | |
| // 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. | |
| const keypath = (0, _cache.getSegmentKeypathForTask)(task, route, cacheKey); | |
| (0, _cache.upsertSegmentEntry)(Date.now(), keypath, fulfilled); | |
| } | |
| }, noop); | |
| } | |
| 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, Object.fromEntries(new URLSearchParams(route.renderedSearch))); | |
| } | |
| // Non-page segments are compared using the same function as the server | |
| return (0, _matchsegments.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; | |
| } | |
| } | |
| } | |
| 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 |