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, createUnclosingPrefetchStream, urlToUrlWithoutFlightMarker } from '../router-reducer/fetch-server-response';
import { trackPrefetchRequestBandwidth, pingPrefetchTask } from './scheduler';
import { getAppBuildId } from '../../app-build-id';
import { createHrefFromUrl } from '../router-reducer/create-href-from-url';
export var EntryStatus = /*#__PURE__*/ function(EntryStatus) {
    EntryStatus[EntryStatus["Pending"] = 0] = "Pending";
    EntryStatus[EntryStatus["Rejected"] = 1] = "Rejected";
    EntryStatus[EntryStatus["Fulfilled"] = 2] = "Fulfilled";
    return EntryStatus;
}({});
const routeCache = new Map();
const segmentCache = new Map();
export function readRouteCacheEntry(now, key) {
    const existingEntry = routeCache.get(key.id);
    if (existingEntry !== undefined) {
        // Check if the entry is stale
        if (existingEntry.staleAt > now) {
            // Reuse the existing entry.
            return existingEntry;
        } else {
            // Evict the stale entry from the cache.
            evictRouteCacheEntryFromCache(key);
        }
    }
    return null;
}
export function readSegmentCacheEntry(now, path) {
    const existingEntry = segmentCache.get(path);
    if (existingEntry !== undefined) {
        // Check if the entry is stale
        if (existingEntry.staleAt > now) {
            // Reuse the existing entry.
            return existingEntry;
        } else {
            // Evict the stale entry from the cache.
            evictSegmentEntryFromCache(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 existingEntry = readRouteCacheEntry(now, task.key);
    if (existingEntry !== null) {
        return existingEntry;
    }
    // Create a pending entry and spawn a request for its data.
    const pendingEntry = {
        canonicalUrl: null,
        status: 0,
        tree: null,
        head: null,
        // 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,
        couldBeIntercepted: false
    };
    const key = task.key;
    const requestPromise = fetchRouteOnCacheMiss(pendingEntry, key);
    trackPrefetchRequestBandwidth(requestPromise);
    routeCache.set(key.id, 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,
        promise: null
    };
    trackPrefetchRequestBandwidth(fetchSegmentEntryOnCacheMiss(route, pendingEntry, task.key, path, accessToken));
    segmentCache.set(path, pendingEntry);
    return pendingEntry;
}
function evictRouteCacheEntryFromCache(key) {
    routeCache.delete(key.id);
}
function evictSegmentEntryFromCache(entry, key) {
    segmentCache.delete(key);
    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 fulfillRouteCacheEntry(entry, tree, head, staleAt, couldBeIntercepted, canonicalUrl) {
    const fulfilledEntry = entry;
    fulfilledEntry.status = 2;
    fulfilledEntry.tree = tree;
    fulfilledEntry.head = head;
    fulfilledEntry.staleAt = staleAt;
    fulfilledEntry.couldBeIntercepted = couldBeIntercepted;
    fulfilledEntry.canonicalUrl = canonicalUrl;
}
function fulfillSegmentCacheEntry(segmentCacheEntry, rsc, loading, staleAt) {
    const fulfilledEntry = segmentCacheEntry;
    fulfilledEntry.status = 2;
    fulfilledEntry.rsc = rsc;
    fulfilledEntry.loading = loading;
    fulfilledEntry.staleAt = staleAt;
    // 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;
}
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, key) {
    // 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 href = key.href;
    try {
        const response = await fetchSegmentPrefetchResponse(href, '/_tree');
        if (!response || !response.ok || !response.body) {
            // Received an unexpected response.
            rejectRouteCacheEntry(entry, Date.now() + 10 * 1000);
            return;
        }
        const serverData = await createFromNextReadableStream(response.body);
        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 ? varyHeader.includes(NEXT_URL) : false;
        fulfillRouteCacheEntry(entry, serverData.tree, serverData.head, Date.now() + serverData.staleTime, couldBeIntercepted, canonicalUrl);
    } catch (error) {
        // Either the connection itself failed, or something bad happened while
        // decoding the response.
        rejectRouteCacheEntry(entry, Date.now() + 10 * 1000);
        pingPrefetchTask(key);
    } finally{
        // The request for the route tree is was blocking this task from prefetching
        // the segments. Now that the route tree is ready, notify the scheduler to
        // unblock the prefetch task. We do this even if the request failed, so the
        // scheduler can dispose of the task.
        pingPrefetchTask(key);
    }
}
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);
        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. See comment in
        // createUnclosingPrefetchStream for more details.
        const prefetchStream = createUnclosingPrefetchStream(response.body);
        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);
    } 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) {
    const headers = {
        [RSC_HEADER]: '1',
        [NEXT_ROUTER_PREFETCH_HEADER]: '1',
        [NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: segmentPath
    };
    const fetchPriority = 'low';
    const response = await createFetch(new URL(href), headers, fetchPriority);
    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 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