UNPKG

next

Version:

The React Framework

325 lines (323 loc) 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { pingPrefetchTask: null, schedulePrefetchTask: null, spawnPrefetchSubtask: null, trackPrefetchRequestBandwidth: null }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { pingPrefetchTask: function() { return pingPrefetchTask; }, schedulePrefetchTask: function() { return schedulePrefetchTask; }, spawnPrefetchSubtask: function() { return spawnPrefetchSubtask; }, trackPrefetchRequestBandwidth: function() { return trackPrefetchRequestBandwidth; } }); const _cache = require("./cache"); const scheduleMicrotask = typeof queueMicrotask === 'function' ? queueMicrotask : (fn)=>Promise.resolve().then(fn).catch((error)=>setTimeout(()=>{ throw error; })); const taskHeap = []; // This is intentionally low so that when a navigation happens, the browser's // internal network queue is not already saturated with prefetch requests. const MAX_CONCURRENT_PREFETCH_REQUESTS = 3; let inProgressRequests = 0; let sortIdCounter = 0; let didScheduleMicrotask = false; function schedulePrefetchTask(key) { // Spawn a new prefetch task const task = { key, sortId: sortIdCounter++, isBlocked: false, _heapIndex: -1 }; 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(); } function ensureWorkIsScheduled() { if (didScheduleMicrotask || !hasNetworkBandwidth()) { // Either we already scheduled a task to process the queue, or there are // too many concurrent requests in progress. In the latter case, the // queue will resume processing once more bandwidth is available. 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() { // 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. return inProgressRequests < MAX_CONCURRENT_PREFETCH_REQUESTS; } function trackPrefetchRequestBandwidth(promiseForServerData) { inProgressRequests++; promiseForServerData.then(onPrefetchRequestCompletion, onPrefetchRequestCompletion); } const noop = ()=>{}; function spawnPrefetchSubtask(promise) { // When the scheduler spawns an async task, we don't await its result // directly. Instead, the async task writes its result directly into the // cache, then pings the scheduler to continue. // // This function only exists to prevent warnings about unhandled promises. promise.then(noop, noop); } function onPrefetchRequestCompletion() { 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 (!task.isBlocked) { // Prefetch is already queued. return; } // Unblock the task and requeue it. task.isBlocked = false; 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()){ const route = (0, _cache.requestRouteCacheEntryFromCache)(now, task); const exitStatus = pingRouteTree(now, task, route); 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. task.isBlocked = true; // Continue to the next task heapPop(taskHeap); task = heapPeek(taskHeap); continue; case 2: // The prefetch is complete. Continue to the next task. heapPop(taskHeap); task = heapPeek(taskHeap); continue; default: { const _exhaustiveCheck = exitStatus; return; } } } } function pingRouteTree(now, task, route) { switch(route.status){ case _cache.EntryStatus.Pending: { // Still pending. We can't start prefetching the segments until the route // tree has loaded. 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: { // Recursively fill in the segment tree. if (!hasNetworkBandwidth()) { // Stop prefetching segments until there's more bandwidth. return 0; } const tree = route.tree; (0, _cache.requestSegmentEntryFromCache)(now, task, route, tree.path, ''); return pingSegmentTree(now, task, route, tree); } default: { const _exhaustiveCheck = route; return 2; } } } function pingSegmentTree(now, task, route, tree) { if (tree.slots !== null) { // Recursively ping the children. for(const parallelRouteKey in tree.slots){ const childTree = tree.slots[parallelRouteKey]; if (!hasNetworkBandwidth()) { // Stop prefetching segments until there's more bandwidth. return 0; } else { const childPath = childTree.path; const childToken = childTree.token; (0, _cache.requestSegmentEntryFromCache)(now, task, route, childPath, childToken); } const childExitStatus = pingSegmentTree(now, task, route, childTree); if (childExitStatus === 0) { // Child yielded without finishing. return 0; } } } // This segment and all its children have finished prefetching. return 2; } // ----------------------------------------------------------------------------- // The remainider 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. // // 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; } // Not currently used, but will be once we add the ability to update a // task's priority. // function heapSift(heap: Array<PrefetchTask>, node: PrefetchTask) { // const index = node._heapIndex // if (index !== -1) { // 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