"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
0 && (module.exports = {
    createDebugChannel: null,
    getOrCreateDebugChannelReadableWriterPair: null
});
function _export(target, all) {
    for(var name in all)Object.defineProperty(target, name, {
        enumerable: true,
        get: all[name]
    });
}
_export(exports, {
    createDebugChannel: function() {
        return createDebugChannel;
    },
    getOrCreateDebugChannelReadableWriterPair: function() {
        return getOrCreateDebugChannelReadableWriterPair;
    }
});
const _approuterheaders = require("../components/app-router-headers");
const _invarianterror = require("../../shared/lib/invariant-error");
const pairs = new Map();
/**
 * Upper bound on the number of in-memory debug-channel pairs we retain, evicted
 * least-recently-used, bounding the live per-request map.
 *
 * A pair must outlive its stream's close so a late decode of the same response
 * (the primary decode plus stage extractions via `decodeStageUntilBoundary`,
 * which can run after the channel closed over the WebSocket) still finds the
 * buffered data. The cap only needs to exceed the pairs live or recently closed
 * at once (bounded by how many prefetch/navigation requests are in flight
 * together), so a few dozen leaves ample headroom even for the segment-heavy
 * bursts the Instant Navs DevTools capture can produce.
 */ const MAX_DEBUG_CHANNEL_PAIRS = 64;
const DB_NAME = '__next_debug_channel';
const STORE_NAME = 'channels';
const CREATED_AT_INDEX = 'createdAt';
/**
 * Upper bound on persisted document debug channels in IndexedDB (one per
 * document, kept for HTTP-cache restore), evicted oldest-first.
 */ const MAX_PERSISTED_DOCUMENT_CHANNELS = 10;
function openDebugChannelDB() {
    return new Promise((resolve, reject)=>{
        const openRequest = indexedDB.open(DB_NAME, 1);
        openRequest.onupgradeneeded = ()=>{
            const store = openRequest.result.createObjectStore(STORE_NAME, {
                keyPath: 'requestId'
            });
            store.createIndex(CREATED_AT_INDEX, 'createdAt');
        };
        openRequest.onsuccess = ()=>resolve(openRequest.result);
        openRequest.onerror = ()=>reject(openRequest.error);
        openRequest.onblocked = ()=>reject(openRequest.error);
    });
}
/**
 * Resolves on the next idle period via `requestIdleCallback`, falling back to a
 * `setTimeout` where `requestIdleCallback` is unavailable.
 */ function whenIdle() {
    return new Promise((resolve)=>{
        if (typeof requestIdleCallback === 'function') {
            requestIdleCallback(()=>resolve());
        } else {
            setTimeout(resolve, 0);
        }
    });
}
async function persistDebugChannelToIndexedDB(requestId, chunks) {
    let db;
    try {
        db = await openDebugChannelDB();
    } catch (error) {
        console.debug('Failed to open debug channel IndexedDB for write', error);
        return;
    }
    try {
        await new Promise((resolve, reject)=>{
            const transaction = db.transaction(STORE_NAME, 'readwrite');
            const store = transaction.objectStore(STORE_NAME);
            store.put({
                requestId,
                createdAt: Date.now(),
                chunks
            });
            // Prune oldest entries beyond the cap to bound storage growth across tabs
            // and/or page loads. The createdAt index gives ordered traversal without
            // scanning, and the cursor deletes commit atomically with the put above.
            const countReq = store.count();
            countReq.onsuccess = ()=>{
                let entriesToDelete = countReq.result - MAX_PERSISTED_DOCUMENT_CHANNELS;
                if (entriesToDelete <= 0) {
                    return;
                }
                const cursorReq = store.index(CREATED_AT_INDEX).openCursor();
                cursorReq.onsuccess = ()=>{
                    const cursor = cursorReq.result;
                    if (!cursor || entriesToDelete === 0) {
                        return;
                    }
                    cursor.delete();
                    entriesToDelete--;
                    cursor.continue();
                };
            };
            transaction.oncomplete = ()=>{
                if (process.env.__NEXT_TEST_MODE) {
                    // Test-only flag, set once this document's debug channel entry is
                    // durably committed. Persistence is deferred to an idle callback and
                    // the IndexedDB write is async, so this flag lets e2e tests await
                    // persistence deterministically — coupling only to "an entry was
                    // persisted" and not to how or where it is stored. It resets
                    // naturally on each navigation since every document gets a fresh
                    // window. The local cast keeps the augmentation out of the shipped
                    // declaration files.
                    ;
                    self.__NEXT_DEBUG_CHANNEL_PERSISTED = true;
                }
                resolve();
            };
            transaction.onerror = ()=>reject(transaction.error);
            transaction.onabort = ()=>reject(transaction.error);
        });
    } catch (error) {
        // Best-effort: if persistence fails (quota, transaction abort, etc.), an
        // HTTP cache restore will fall back to location.reload() since no entry
        // will be found.
        console.debug('Failed to write debug channel entry to IndexedDB', error);
    } finally{
        db.close();
    }
}
function restoreDebugChannelFromIndexedDB(requestId) {
    return new ReadableStream({
        async start (controller) {
            let entry;
            try {
                const db = await openDebugChannelDB();
                try {
                    entry = await new Promise((resolve, reject)=>{
                        const tx = db.transaction(STORE_NAME, 'readonly');
                        const store = tx.objectStore(STORE_NAME);
                        const getReq = store.get(requestId);
                        getReq.onsuccess = ()=>resolve(getReq.result);
                        getReq.onerror = ()=>reject(getReq.error);
                    });
                } finally{
                    db.close();
                }
            } catch (error) {
                // Treat any IDB failure as "no entry" and fall through to reload.
                console.debug('Failed to read debug channel entry from IndexedDB', error);
            }
            if (!entry) {
                // Debug channel can't be restored — missing debug chunks would block
                // hydration. Force a fresh page load from the server. Leave the stream
                // parked (no enqueue, no close) so the Flight client stays put until
                // the reload tears the document down, instead of synchronously erroring
                // with "Connection closed.".
                location.reload();
                return;
            }
            for (const chunk of entry.chunks){
                controller.enqueue(chunk);
            }
            controller.close();
        }
    });
}
/**
 * Decide at script-execution time whether the document was served from the
 * browser's cache or freshly fetched from the server. `type === 'back_forward'`
 * alone isn't enough: a back/forward navigation can also be a fresh server
 * re-fetch when the HTTP cache entry was evicted (long-lived tab, storage
 * pressure, manual cache clear), and treating that as a cache restore would
 * trigger an unnecessary `location.reload()` when no persisted chunks are
 * found.
 */ function wasServedFromCacheKnownAtExec(entry) {
    if (!entry) {
        return 1;
    }
    // Safari tab-duplication cache restore: type='navigate' paired with
    // responseStart=0 (no first-body-byte over the network) and a non-zero
    // responseEnd. Fresh navigations always have responseStart > 0.
    if (entry.type === 'navigate' && entry.responseStart === 0 && entry.responseEnd > 0) {
        return 0;
    }
    // Every remaining cache-restore signal requires a back/forward navigation.
    // (bfcache restores don't re-execute scripts and never reach this code.)
    if (entry.type !== 'back_forward') {
        return 1;
    }
    // Chrome ≥109 and Safari ≥17 populate `deliveryType` at exec time even when
    // the size fields aren't filled in yet. This is the only exec-time fast path
    // for real Safari ≥17 cache restores (Safari leaves encodedBodySize at 0 at
    // exec).
    if (entry.deliveryType === 'cache') {
        return 0;
    }
    // Chrome and Firefox publish an HTTP cache restore as transferSize=0 (no
    // bytes over the wire) plus a non-zero cached body size at exec time.
    if (entry.transferSize === 0 && entry.encodedBodySize > 0) {
        return 0;
    }
    // No body bytes measured yet. Either the response is still streaming, or
    // WebKit is reporting transferSize=0 and encodedBodySize=0 at exec time
    // regardless of whether the document was cached or re-fetched. Defer to
    // `pageshow` where the two cases become distinguishable.
    if (entry.encodedBodySize === 0) {
        return 2;
    }
    // Body bytes already measured at exec time with no other cache signal: a
    // re-fetched back-nav whose response happened to complete before our script
    // ran. The deferred branch above would have caught the same case if the
    // response had still been streaming.
    return 1;
}
/**
 * Re-check the cache-restore decision at `pageshow`, when every browser has
 * populated the navigation-entry size fields. Only called when
 * `wasServedFromCacheKnownAtExec` returned `ExecTimeCacheDecision.Undecided`.
 */ function wasServedFromCacheAtPageshow(entry) {
    if (!entry) {
        return false;
    }
    // Safari tab-duplication signature; see the matching branch in
    // `wasServedFromCacheKnownAtExec`.
    if (entry.type === 'navigate' && entry.responseStart === 0 && entry.responseEnd > 0) {
        return true;
    }
    // A back/forward navigation where at least one of the size fields is zero
    // means the body didn't come over the wire. Browsers signal a cache restore
    // differently — Chrome/Firefox zero `transferSize` and keep a non-zero cached
    // `encodedBodySize`; Safari does the inverse with a small `transferSize`
    // (header overhead) and `encodedBodySize=0`; WebKit under Playwright zeros
    // both. A fresh re-fetch populates both with the response size.
    return entry.type === 'back_forward' && (entry.transferSize === 0 || entry.encodedBodySize === 0);
}
function getNavigationEntry() {
    try {
        return performance.getEntriesByType('navigation')[0];
    } catch  {
        return undefined;
    }
}
/**
 * Reclaim the least-recently-used debug-channel pairs once the map exceeds
 * `MAX_DEBUG_CHANNEL_PAIRS`. The map is iterated in insertion order and we
 * re-insert entries on access (see
 * `getOrCreateDebugChannelReadableWriterPair`), so the least-recently-used
 * pairs sit at the front. Evicting only ever affects future lookups for that
 * request id; consumers that already hold a tee branch keep reading
 * independently of the map.
 */ function evictExcessDebugChannelPairs() {
    while(pairs.size > MAX_DEBUG_CHANNEL_PAIRS){
        const oldestRequestId = pairs.keys().next().value;
        if (oldestRequestId === undefined) {
            break;
        }
        pairs.delete(oldestRequestId);
    }
}
function getOrCreateDebugChannelReadableWriterPair(requestId) {
    const existingPair = pairs.get(requestId);
    if (existingPair) {
        // Refresh the LRU recency of an already-known channel by re-inserting it at
        // the most-recent position, so a channel that's still being written to or
        // read from isn't evicted while a late consumer (e.g. a stage re-decode of
        // the same response) still needs it.
        pairs.delete(requestId);
        pairs.set(requestId, existingPair);
        return existingPair;
    }
    // Buffer chunks only for the initial document's debug channel, not for
    // client-side navigation requests. Persisted to IndexedDB once complete so it
    // can be restored when the browser serves the page from HTTP cache
    // (back-forward navigation, tab duplication, etc.).
    const chunks = requestId === self.__next_r ? [] : null;
    const { readable, writable } = new TransformStream({
        transform (chunk, controller) {
            if (chunks) {
                chunks.push(chunk.slice());
            }
            controller.enqueue(chunk);
        }
    });
    const pair = {
        readable,
        writer: writable.getWriter()
    };
    pairs.set(requestId, pair);
    // Retain the pair past its stream's close (see MAX_DEBUG_CHANNEL_PAIRS) and
    // bound the map by reclaiming the least-recently-used.
    evictExcessDebugChannelPairs();
    pair.writer.closed.then(async ()=>{
        if (!chunks) {
            return;
        }
        // The initial document's debug stream closes while hydration is still
        // running, so persisting here would steal main-thread time from it. Wait
        // for genuine idle (no timeout): persistence is best-effort, so if the
        // page never idles before navigation we skip it and a later restore falls
        // back to a reload, rather than forcing a blocking write.
        await whenIdle();
        await persistDebugChannelToIndexedDB(requestId, chunks);
    }).catch((error)=>{
        // writer.closed rejected (e.g., stream aborted), nothing to persist.
        console.debug('Debug channel writer closed with error', error);
    }).finally(()=>{
        // Keep the now-closed pair in the map so late decodes of this request
        // still resolve against its buffered stream; it's reclaimed later by LRU
        // eviction. Release the IndexedDB staging buffer now that it's persisted.
        if (chunks) {
            chunks.length = 0;
        }
    });
    return pair;
}
function createDebugChannel(requestHeaders) {
    let requestId;
    if (requestHeaders) {
        requestId = requestHeaders[_approuterheaders.NEXT_REQUEST_ID_HEADER] ?? undefined;
        if (!requestId) {
            throw Object.defineProperty(new _invarianterror.InvariantError(`Expected a ${JSON.stringify(_approuterheaders.NEXT_REQUEST_ID_HEADER)} request header.`), "__NEXT_ERROR_CODE", {
                value: "E854",
                enumerable: false,
                configurable: true
            });
        }
    } else {
        requestId = self.__next_r;
        if (!requestId) {
            throw Object.defineProperty(new _invarianterror.InvariantError(`Expected a request ID to be defined for the document via self.__next_r.`), "__NEXT_ERROR_CODE", {
                value: "E806",
                enumerable: false,
                configurable: true
            });
        }
    }
    // Only attempt to restore the IndexedDB debug channel entry for the
    // initial document load (no request headers). Client-side navigations pass
    // request headers and should always use the WebSocket-backed debug channel.
    if (!requestHeaders) {
        switch(wasServedFromCacheKnownAtExec(getNavigationEntry())){
            case 0:
                return {
                    readable: restoreDebugChannelOrReload(requestId)
                };
            case 2:
                // Body bytes haven't been measured on the navigation entry yet. Suspend
                // the stream until pageshow, re-check there, then source from the
                // persisted chunks or the WebSocket-backed pair accordingly.
                return {
                    readable: createDeferredDebugChannelReadable(requestId)
                };
            case 1:
                break;
        }
    }
    const pair = getOrCreateDebugChannelReadableWriterPair(requestId);
    // Hand out a fresh tee branch per consumer and keep the remainder for the
    // next one (see the `readable` field doc above).
    const [branch, rest] = pair.readable.tee();
    pair.readable = rest;
    return {
        readable: branch
    };
}
/**
 * Try to restore the debug channel from the persisted chunks. If none are
 * found, force a fresh page load.
 */ function restoreDebugChannelOrReload(requestId) {
    const readable = restoreDebugChannelFromIndexedDB(requestId);
    if (readable) {
        return readable;
    }
    // No persisted entry. Typically this happens when the HTTP cache held the
    // HTML but the persisted entry was never written, or was overwritten by a
    // newer document in this tab.
    location.reload();
    // Never-closing stream. Keeps the Flight client suspended until the reload
    // tears the document down, instead of letting it synchronously error with
    // "Connection closed.".
    return new ReadableStream();
}
/**
 * Used when `wasServedFromCacheKnownAtExec` returns
 * `ExecTimeCacheDecision.Undecided`. Waits for `pageshow`, re-runs the check,
 * and forwards data from either the persisted chunks or the WebSocket.
 */ function createDeferredDebugChannelReadable(requestId) {
    return new ReadableStream({
        async start (controller) {
            // By `pageshow` every browser has populated the navigation-entry size
            // fields, so the re-check below is unambiguous.
            await new Promise((resolve)=>{
                window.addEventListener('pageshow', ()=>resolve(), {
                    once: true
                });
            });
            const source = wasServedFromCacheAtPageshow(getNavigationEntry()) ? restoreDebugChannelOrReload(requestId) : getOrCreateDebugChannelReadableWriterPair(requestId).readable;
            const reader = source.getReader();
            try {
                while(true){
                    const { done, value } = await reader.read();
                    if (done) {
                        controller.close();
                        return;
                    }
                    controller.enqueue(value);
                }
            } catch (error) {
                controller.error(error);
            } finally{
                reader.releaseLock();
            }
        }
    });
}

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=debug-channel.js.map