import { matchSegment } from '../match-segments';
import { readOrCreateRouteCacheEntry, readOrCreateSegmentCacheEntry, fetchRouteOnCacheMiss, fetchSegmentOnCacheMiss, EntryStatus, fetchSegmentPrefetchesForPPRDisabledRoute, convertRouteTreeToFlightRouterState } from './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;
/**
 * 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
 */ export function schedulePrefetchTask(key, treeAtTimeOfPrefetch) {
    // Spawn a new prefetch task
    const task = {
        key,
        treeAtTimeOfPrefetch,
        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 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++;
    prefetchSubtask.then((result)=>{
        if (result === null) {
            // The prefetch task errored before it could start processing the
            // network stream. Assume the connection is closed.
            return;
        }
        // Wait for the connection to close before freeing up more bandwidth.
        return result.closed;
    }).then(onPrefetchConnectionClosed, onPrefetchConnectionClosed);
}
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 (!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 = readOrCreateRouteCacheEntry(now, task);
        const exitStatus = pingRootRouteTree(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 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 prefetches 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));
                // 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:
            {
                // Recursively fill in the segment tree.
                if (!hasNetworkBandwidth()) {
                    // Stop prefetching segments until there's more bandwidth.
                    return 0;
                }
                const tree = route.tree;
                if (route.isPPREnabled) {
                    return pingRouteTree(now, task, route, tree);
                } else {
                    // When PPR is disabled, we can't prefetch per segment. We must fallback
                    // to the old prefetch behavior and send a dynamic request.
                    //
                    // 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.
                    //
                    // Only routes that include a loading boundary can be prefetched in this
                    // way. The server will only render up to the first loading boundary
                    // inside new part of the tree. If there's no loading boundary, the
                    // server will never return any data.
                    // TODO: When we prefetch the route tree, the server should
                    // indicate whether there's a loading boundary so the client doesn't
                    // send a second request for no reason.
                    const spawnedEntries = new Map();
                    const dynamicRequestTree = pingRouteTreeForPPRDisabledRoute(now, route, task.treeAtTimeOfPrefetch, tree, spawnedEntries);
                    const needsDynamicRequest = spawnedEntries.size > 0;
                    if (needsDynamicRequest) {
                        // Perform a dynamic prefetch request and populate the cache with
                        // the result
                        spawnPrefetchSubtask(fetchSegmentPrefetchesForPPRDisabledRoute(task, route, dynamicRequestTree, spawnedEntries));
                    }
                    return 2;
                }
            }
        default:
            {
                const _exhaustiveCheck = route;
                return 2;
            }
    }
}
function pingRouteTree(now, task, route, tree) {
    const segment = readOrCreateSegmentCacheEntry(now, route, tree.key);
    pingSegment(route, segment, task.key, tree.key, tree.token);
    if (tree.slots !== null) {
        if (!hasNetworkBandwidth()) {
            // 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 = pingRouteTree(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 pingRouteTreeForPPRDisabledRoute(now, route, oldTree, newTree, spawnedEntries) {
    // 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];
            let requestTreeChild;
            if (oldTreeChildSegment !== undefined && matchSegment(newTreeChildSegment, oldTreeChildSegment)) {
                // This segment is already part of the current route. Keep traversing.
                requestTreeChild = pingRouteTreeForPPRDisabledRoute(now, route, oldTreeChild, newTreeChild, spawnedEntries);
            } 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).
                requestTreeChild = createDynamicRequestTreeForPartiallyCachedSegments(now, route, newTreeChild, null, spawnedEntries);
            }
            requestTreeChildren[parallelRouteKey] = requestTreeChild;
        }
    }
    const requestTree = [
        newTree.segment,
        requestTreeChildren,
        null,
        null,
        newTree.isRootLayout
    ];
    return requestTree;
}
function createDynamicRequestTreeForPartiallyCachedSegments(now, route, tree, refetchMarkerContext, spawnedEntries) {
    // The tree we're constructing is the same shape as the tree we're navigating
    // to — specifically, it's the subtree that isn't present in the previous
    // route. 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 until it hits a loading boundary. We use `refetchMarkerContext`
    // to represent the nearest parent marker.
    let refetchMarker = refetchMarkerContext === null ? 'inside-shared-layout' : null;
    const segment = readOrCreateSegmentCacheEntry(now, route, tree.key);
    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.
                segment.status = EntryStatus.Pending;
                spawnedEntries.set(tree.key, segment);
                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.
                // TODO: The server should include a `hasLoading` field as part of the
                // route tree prefetch.
                if (segment.loading !== null) {
                    // 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] = createDynamicRequestTreeForPartiallyCachedSegments(now, route, childTree, refetchMarkerContext, spawnedEntries);
        }
    }
    const requestTree = [
        tree.segment,
        requestTreeChildren,
        null,
        refetchMarker,
        tree.isRootLayout
    ];
    return requestTree;
}
function pingSegment(route, segment, routeKey, segmentKey, accessToken) {
    if (segment.status === EntryStatus.Empty) {
        if (accessToken === null) {
        // We don't have an access token for this segment, which means we can't
        // do a per-segment prefetch. This happens when the route tree was
        // returned by a dynamic server response. Or if the server has decided
        // not to grant access to this segment.
        } else {
            // Segment is not yet cached, and there's no request already in progress.
            // Spawn a task to request the segment and load it into the cache.
            // Upgrade to Pending so we know there's already a request in progress
            segment.status = EntryStatus.Pending;
            spawnPrefetchSubtask(fetchSegmentOnCacheMiss(route, segment, routeKey, segmentKey, accessToken));
        }
    }
// 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`).
}
// -----------------------------------------------------------------------------
// 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;
        }
    }
}

//# sourceMappingURL=scheduler.js.map