"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
0 && (module.exports = {
    pingPrefetchTask: null,
    schedulePrefetchTask: 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;
    },
    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 = [];
const taskMap = new Map();
// 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) {
    const existingTask = taskMap.get(key.id);
    if (existingTask !== undefined) {
        // A task for this URL already exists, but we should bump it to the top of
        // the queue by assigning a new sortId. However, if it already has the
        // highest sortId that's been assigned, then we can bail out.
        //
        // Note that this check is not equivalent to checking if it's at the front
        // of the queue, because there could be tasks that have a higher sortId but
        // are not currently queued (because they are blocked).
        if (existingTask.sortId === sortIdCounter) {
            // No-op
            return;
        }
        existingTask.sortId = sortIdCounter++;
        heapSift(taskHeap, existingTask);
        return;
    }
    // Spawn a new prefetch task
    const task = {
        key,
        sortId: sortIdCounter++,
        isBlocked: false,
        _heapIndex: -1
    };
    heapPush(taskHeap, task);
    taskMap.set(key.id, 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);
}
function onPrefetchRequestCompletion() {
    inProgressRequests--;
    // Notify the scheduler that we have more bandwidth, and can continue
    // processing tasks.
    ensureWorkIsScheduled();
}
function pingPrefetchTask(key) {
    // "Ping" a prefetch that's already in progress to notify it of new data.
    const task = taskMap.get(key.id);
    if (task === undefined) {
        // There's no matching prefetch task, which means it must have either
        // already completed or been canceled. This can also happen if an entry is
        // read from the cache outside the context of a prefetch.
        return;
    }
    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
                task = heapPop(taskHeap);
                continue;
            case 2:
                // The prefetch is complete. Remove the task from the map so it can be
                // garbage collected.
                taskMap.delete(task.key.id);
                // Continue to the next task
                task = heapPop(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.
                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;
}
function heapSift(heap, node) {
    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