import { NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_URL, RSC_CONTENT_TYPE_HEADER, RSC_HEADER } from '../app-router-headers';
import { createFetch, createFromNextReadableStream, urlToUrlWithoutFlightMarker } from '../router-reducer/fetch-server-response';
import { trackPrefetchRequestBandwidth, pingPrefetchTask, spawnPrefetchSubtask } from './scheduler';
import { getAppBuildId } from '../../app-build-id';
import { createHrefFromUrl } from '../router-reducer/create-href-from-url';
import { createTupleMap } from './tuple-map';
import { createLRU } from './lru';
export var EntryStatus = /*#__PURE__*/ function(EntryStatus) {
    EntryStatus[EntryStatus["Pending"] = 0] = "Pending";
    EntryStatus[EntryStatus["Rejected"] = 1] = "Rejected";
    EntryStatus[EntryStatus["Fulfilled"] = 2] = "Fulfilled";
    return EntryStatus;
}({});
const routeCacheMap = createTupleMap();
// We use an LRU for memory management. We must update this whenever we add or
// remove a new cache entry, or when an entry changes size.
// TODO: I chose the max size somewhat arbitrarily. Consider setting this based
// on navigator.deviceMemory, or some other heuristic. We should make this
// customizable via the Next.js config, too.
const maxRouteLruSize = 10 * 1024 * 1024 // 10 MB
;
const routeCacheLru = createLRU(maxRouteLruSize, onRouteLRUEviction);
// TODO: We may eventually store segment entries in a tuple map, too, to
// account for search params.
const segmentCacheMap = new Map();
// NOTE: Segments and Route entries are managed by separate LRUs. We could
// combine them into a single LRU, but because they are separate types, we'd
// need to wrap each one in an extra LRU node (to maintain monomorphism, at the
// cost of additional memory).
const maxSegmentLruSize = 50 * 1024 * 1024 // 50 MB
;
const segmentCacheLru = createLRU(maxSegmentLruSize, onSegmentLRUEviction);
export function readExactRouteCacheEntry(now, href, nextUrl) {
    const keypath = nextUrl === null ? [
        href
    ] : [
        href,
        nextUrl
    ];
    const existingEntry = routeCacheMap.get(keypath);
    if (existingEntry !== null) {
        // Check if the entry is stale
        if (existingEntry.staleAt > now) {
            // Reuse the existing entry.
            // Since this is an access, move the entry to the front of the LRU.
            routeCacheLru.put(existingEntry);
            return existingEntry;
        } else {
            // Evict the stale entry from the cache.
            deleteRouteFromCache(existingEntry, keypath);
        }
    }
    return null;
}
export function readRouteCacheEntry(now, key) {
    // First check if there's a non-intercepted entry. Most routes cannot be
    // intercepted, so this is the common case.
    const nonInterceptedEntry = readExactRouteCacheEntry(now, key.href, null);
    if (nonInterceptedEntry !== null && !nonInterceptedEntry.couldBeIntercepted) {
        // Found a match, and the route cannot be intercepted. We can reuse it.
        return nonInterceptedEntry;
    }
    // There was no match. Check again but include the Next-Url this time.
    return readExactRouteCacheEntry(now, key.href, key.nextUrl);
}
export function readSegmentCacheEntry(now, path) {
    const existingEntry = segmentCacheMap.get(path);
    if (existingEntry !== undefined) {
        // Check if the entry is stale
        if (existingEntry.staleAt > now) {
            // Reuse the existing entry.
            // Since this is an access, move the entry to the front of the LRU.
            segmentCacheLru.put(existingEntry);
            return existingEntry;
        } else {
            // Evict the stale entry from the cache.
            deleteSegmentFromCache(existingEntry, path);
        }
    }
    return null;
}
export function waitForSegmentCacheEntry(pendingEntry) {
    // Because the entry is pending, there's already a in-progress request.
    // Attach a promise to the entry that will resolve when the server responds.
    let promiseWithResolvers = pendingEntry.promise;
    if (promiseWithResolvers === null) {
        promiseWithResolvers = pendingEntry.promise = createPromiseWithResolvers();
    } else {
    // There's already a promise we can use
    }
    return promiseWithResolvers.promise;
}
/**
 * Reads the route cache for a matching entry *and* spawns a request if there's
 * no match. Because this may issue a network request, it should only be called
 * from within the context of a prefetch task.
 */ export function requestRouteCacheEntryFromCache(now, task) {
    const key = task.key;
    // First check if there's a non-intercepted entry. Most routes cannot be
    // intercepted, so this is the common case.
    const nonInterceptedEntry = readExactRouteCacheEntry(now, key.href, null);
    if (nonInterceptedEntry !== null && !nonInterceptedEntry.couldBeIntercepted) {
        // Found a match, and the route cannot be intercepted. We can reuse it.
        return nonInterceptedEntry;
    }
    // There was no match. Check again but include the Next-Url this time.
    const exactEntry = readExactRouteCacheEntry(now, key.href, key.nextUrl);
    if (exactEntry !== null) {
        return exactEntry;
    }
    // Create a pending entry and spawn a request for its data.
    const pendingEntry = {
        canonicalUrl: null,
        status: 0,
        blockedTasks: null,
        tree: null,
        head: null,
        isHeadPartial: true,
        // 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.
        staleAt: now + 60 * 1000,
        // This is initialized to true because we don't know yet whether the route
        // could be intercepted. It's only set to false once we receive a response
        // from the server.
        couldBeIntercepted: true,
        // LRU-related fields
        keypath: null,
        next: null,
        prev: null,
        size: 0
    };
    spawnPrefetchSubtask(fetchRouteOnCacheMiss(pendingEntry, task));
    const keypath = key.nextUrl === null ? [
        key.href
    ] : [
        key.href,
        key.nextUrl
    ];
    routeCacheMap.set(keypath, pendingEntry);
    // Stash the keypath on the entry so we know how to remove it from the map
    // if it gets evicted from the LRU.
    pendingEntry.keypath = keypath;
    routeCacheLru.put(pendingEntry);
    return pendingEntry;
}
/**
 * Reads the route cache for a matching entry *and* spawns a request if there's
 * no match. Because this may issue a network request, it should only be called
 * from within the context of a prefetch task.
 */ export function requestSegmentEntryFromCache(now, task, route, path, accessToken) {
    const existingEntry = readSegmentCacheEntry(now, path);
    if (existingEntry !== null) {
        return existingEntry;
    }
    // Create a pending entry and spawn a request for its data.
    const pendingEntry = {
        status: 0,
        rsc: null,
        loading: null,
        staleAt: route.staleAt,
        isPartial: true,
        promise: null,
        // LRU-related fields
        key: null,
        next: null,
        prev: null,
        size: 0
    };
    spawnPrefetchSubtask(fetchSegmentEntryOnCacheMiss(route, pendingEntry, task.key, path, accessToken));
    segmentCacheMap.set(path, pendingEntry);
    // Stash the keypath on the entry so we know how to remove it from the map
    // if it gets evicted from the LRU.
    pendingEntry.key = path;
    segmentCacheLru.put(pendingEntry);
    return pendingEntry;
}
function deleteRouteFromCache(entry, keypath) {
    pingBlockedTasks(entry);
    routeCacheMap.delete(keypath);
    routeCacheLru.delete(entry);
}
function deleteSegmentFromCache(entry, key) {
    cancelEntryListeners(entry);
    segmentCacheMap.delete(key);
    segmentCacheLru.delete(entry);
}
function onRouteLRUEviction(entry) {
    // The LRU evicted this entry. Remove it from the map.
    const keypath = entry.keypath;
    if (keypath !== null) {
        entry.keypath = null;
        pingBlockedTasks(entry);
        routeCacheMap.delete(keypath);
    }
}
function onSegmentLRUEviction(entry) {
    // The LRU evicted this entry. Remove it from the map.
    const key = entry.key;
    if (key !== null) {
        entry.key = null;
        cancelEntryListeners(entry);
        segmentCacheMap.delete(key);
    }
}
function cancelEntryListeners(entry) {
    if (entry.status === 0 && entry.promise !== null) {
        // There were listeners for this entry. Resolve them with `null` to indicate
        // that the prefetch failed. It's up to the listener to decide how to handle
        // this case.
        // NOTE: We don't currently propagate the reason the prefetch was canceled
        // but we could by accepting a `reason` argument.
        entry.promise.resolve(null);
        entry.promise = null;
    }
}
function pingBlockedTasks(entry) {
    const blockedTasks = entry.blockedTasks;
    if (blockedTasks !== null) {
        for (const task of blockedTasks){
            pingPrefetchTask(task);
        }
        entry.blockedTasks = null;
    }
}
function fulfillRouteCacheEntry(entry, tree, head, isHeadPartial, staleAt, couldBeIntercepted, canonicalUrl) {
    const fulfilledEntry = entry;
    fulfilledEntry.status = 2;
    fulfilledEntry.tree = tree;
    fulfilledEntry.head = head;
    fulfilledEntry.isHeadPartial = isHeadPartial;
    fulfilledEntry.staleAt = staleAt;
    fulfilledEntry.couldBeIntercepted = couldBeIntercepted;
    fulfilledEntry.canonicalUrl = canonicalUrl;
    pingBlockedTasks(entry);
    return fulfilledEntry;
}
function fulfillSegmentCacheEntry(segmentCacheEntry, rsc, loading, staleAt, isPartial) {
    const fulfilledEntry = segmentCacheEntry;
    fulfilledEntry.status = 2;
    fulfilledEntry.rsc = rsc;
    fulfilledEntry.loading = loading;
    fulfilledEntry.staleAt = staleAt;
    fulfilledEntry.isPartial = isPartial;
    // Resolve any listeners that were waiting for this data.
    if (segmentCacheEntry.promise !== null) {
        segmentCacheEntry.promise.resolve(fulfilledEntry);
        // Free the promise for garbage collection.
        fulfilledEntry.promise = null;
    }
}
function rejectRouteCacheEntry(entry, staleAt) {
    const rejectedEntry = entry;
    rejectedEntry.status = 1;
    rejectedEntry.staleAt = staleAt;
    pingBlockedTasks(entry);
}
function rejectSegmentCacheEntry(entry, staleAt) {
    const rejectedEntry = entry;
    rejectedEntry.status = 1;
    rejectedEntry.staleAt = staleAt;
    if (entry.promise !== null) {
        // NOTE: We don't currently propagate the reason the prefetch was canceled
        // but we could by accepting a `reason` argument.
        entry.promise.resolve(null);
        entry.promise = null;
    }
}
async function fetchRouteOnCacheMiss(entry, task) {
    // This function is allowed to use async/await because it contains the actual
    // fetch that gets issued on a cache miss. Notice though that it does not
    // return anything; it writes the result to the cache entry directly, then
    // pings the scheduler to unblock the corresponding prefetch task.
    const key = task.key;
    const href = key.href;
    const nextUrl = key.nextUrl;
    try {
        const response = await fetchSegmentPrefetchResponse(href, '/_tree', nextUrl);
        if (!response || !response.ok || !response.body) {
            // Received an unexpected response.
            rejectRouteCacheEntry(entry, Date.now() + 10 * 1000);
            return;
        }
        const prefetchStream = createPrefetchResponseStream(response.body, routeCacheLru, entry);
        const serverData = await createFromNextReadableStream(prefetchStream);
        if (serverData.buildId !== getAppBuildId()) {
            // The server build does not match the client. Treat as a 404. During
            // an actual navigation, the router will trigger an MPA navigation.
            // TODO: Consider moving the build ID to a response header so we can check
            // it before decoding the response, and so there's one way of checking
            // across all response types.
            rejectRouteCacheEntry(entry, Date.now() + 10 * 1000);
            return;
        }
        // This is a bit convoluted but it's taken from router-reducer and
        // fetch-server-response
        const canonicalUrl = response.redirected ? createHrefFromUrl(urlToUrlWithoutFlightMarker(response.url)) : href;
        // Check whether the response varies based on the Next-Url header.
        const varyHeader = response.headers.get('vary');
        const couldBeIntercepted = varyHeader !== null && varyHeader.includes(NEXT_URL);
        fulfillRouteCacheEntry(entry, serverData.tree, serverData.head, serverData.isHeadPartial, Date.now() + serverData.staleTime, couldBeIntercepted, canonicalUrl);
        if (!couldBeIntercepted && nextUrl !== null) {
            // This route will never be intercepted. So we can use this entry for all
            // requests to this route, regardless of the Next-Url header. This works
            // because when reading the cache we always check for a valid
            // non-intercepted entry first.
            //
            // Re-key the entry. Since we're in an async task, we must first confirm
            // that the entry hasn't been concurrently modified by a different task.
            const currentKeypath = [
                href,
                nextUrl
            ];
            const expectedEntry = routeCacheMap.get(currentKeypath);
            if (expectedEntry === entry) {
                routeCacheMap.delete(currentKeypath);
                const newKeypath = [
                    href
                ];
                routeCacheMap.set(newKeypath, entry);
                // We don't need to update the LRU because the entry is already in it.
                // But since we changed the keypath, we do need to update that, so we
                // know how to remove it from the map if it gets evicted from the LRU.
                entry.keypath = newKeypath;
            } else {
            // Something else modified this entry already. Since the re-keying is
            // just a performance optimization, we can safely skip it.
            }
        }
    } catch (error) {
        // Either the connection itself failed, or something bad happened while
        // decoding the response.
        rejectRouteCacheEntry(entry, Date.now() + 10 * 1000);
    }
}
async function fetchSegmentEntryOnCacheMiss(route, segmentCacheEntry, routeKey, segmentPath, accessToken) {
    // This function is allowed to use async/await because it contains the actual
    // fetch that gets issued on a cache miss. Notice though that it does not
    // return anything; it writes the result to the cache entry directly.
    //
    // Segment fetches are non-blocking so we don't need to ping the scheduler
    // on completion.
    const href = routeKey.href;
    try {
        const response = await fetchSegmentPrefetchResponse(href, accessToken === '' ? segmentPath : segmentPath + "." + accessToken, routeKey.nextUrl);
        if (!response || !response.ok || !response.body) {
            // Server responded with an error. We should still cache the response, but
            // we can try again after 10 seconds.
            rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000);
            return;
        }
        // Wrap the original stream in a new stream that never closes. That way the
        // Flight client doesn't error if there's a hanging promise.
        const prefetchStream = createPrefetchResponseStream(response.body, segmentCacheLru, segmentCacheEntry);
        const serverData = await createFromNextReadableStream(prefetchStream);
        if (serverData.buildId !== getAppBuildId()) {
            // The server build does not match the client. Treat as a 404. During
            // an actual navigation, the router will trigger an MPA navigation.
            // TODO: Consider moving the build ID to a response header so we can check
            // it before decoding the response, and so there's one way of checking
            // across all response types.
            rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000);
            return;
        }
        fulfillSegmentCacheEntry(segmentCacheEntry, serverData.rsc, serverData.loading, // TODO: The server does not currently provide per-segment stale time.
        // So we use the stale time of the route.
        route.staleAt, serverData.isPartial);
    } catch (error) {
        // Either the connection itself failed, or something bad happened while
        // decoding the response.
        rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000);
    }
}
async function fetchSegmentPrefetchResponse(href, segmentPath, nextUrl) {
    const headers = {
        [RSC_HEADER]: '1',
        [NEXT_ROUTER_PREFETCH_HEADER]: '1',
        [NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: segmentPath
    };
    if (nextUrl !== null) {
        headers[NEXT_URL] = nextUrl;
    }
    const fetchPriority = 'low';
    const responsePromise = createFetch(new URL(href), headers, fetchPriority);
    trackPrefetchRequestBandwidth(responsePromise);
    const response = await responsePromise;
    const contentType = response.headers.get('content-type');
    const isFlightResponse = contentType && contentType.startsWith(RSC_CONTENT_TYPE_HEADER);
    if (!response.ok || !isFlightResponse) {
        return null;
    }
    return response;
}
function createPrefetchResponseStream(originalFlightStream, lru, lruEntry) {
    // When PPR is enabled, prefetch streams may contain references that never
    // resolve, because that's how we encode dynamic data access. In the decoded
    // object returned by the Flight client, these are reified into hanging
    // promises that suspend during render, which is effectively what we want.
    // The UI resolves when it switches to the dynamic data stream
    // (via useDeferredValue(dynamic, static)).
    //
    // However, the Flight implementation currently errors if the server closes
    // the response before all the references are resolved. As a cheat to work
    // around this, we wrap the original stream in a new stream that never closes,
    // and therefore doesn't error.
    //
    // While processing the original stream, we also incrementally update the size
    // of the cache entry in the LRU.
    let totalByteLength = 0;
    const reader = originalFlightStream.getReader();
    return new ReadableStream({
        async pull (controller) {
            while(true){
                const { done, value } = await reader.read();
                if (!done) {
                    // Pass to the target stream and keep consuming the Flight response
                    // from the server.
                    controller.enqueue(value);
                    // Incrementally update the size of the cache entry in the LRU.
                    // NOTE: Since prefetch responses are delivered in a single chunk,
                    // it's not really necessary to do this streamingly, but I'm doing it
                    // anyway in case this changes in the future.
                    totalByteLength += value.byteLength;
                    lru.updateSize(lruEntry, totalByteLength);
                    continue;
                }
                // The server stream has closed. Exit, but intentionally do not close
                // the target stream.
                return;
            }
        }
    });
}
function createPromiseWithResolvers() {
    // Shim of Stage 4 Promise.withResolvers proposal
    let resolve;
    let reject;
    const promise = new Promise((res, rej)=>{
        resolve = res;
        reject = rej;
    });
    return {
        resolve: resolve,
        reject: reject,
        promise
    };
}

//# sourceMappingURL=cache.js.map