"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "createTieredCacheHandler", {
    enumerable: true,
    get: function() {
        return createTieredCacheHandler;
    }
});
const _workasyncstorageexternal = require("../app-render/work-async-storage.external");
const _clonecacheentry = require("./clone-cache-entry");
function createTieredCacheHandler(front, backing) {
    // Holds the in-flight (or chained) background sync per key, so a sync for a
    // key runs after any earlier one for that key rather than in parallel.
    const inFlightSyncs = new Map();
    function scheduleBackgroundSync(cacheKey, sync) {
        // Serialize syncs per key: chain this one after any in-flight sync rather
        // than running a second in parallel. The trailing sync still re-reads the
        // backing, so the front converges to the latest state; a later read is
        // never dropped in favor of an earlier, possibly stale, in-flight read.
        const previous = inFlightSyncs.get(cacheKey);
        let pending;
        if (previous) {
            pending = previous.then(sync);
        } else {
            pending = sync();
        }
        pending = pending.finally(()=>{
            if (inFlightSyncs.get(cacheKey) === pending) {
                inFlightSyncs.delete(cacheKey);
            }
        });
        inFlightSyncs.set(cacheKey, pending);
        // Register the sync on the current request's revalidation writes so it is
        // awaited rather than left untracked. Reading the work store here (rather
        // than capturing it at construction) is what lets the handler be a shared
        // singleton; `get` always runs within the request's async context, so the
        // store is present.
        const workStore = _workasyncstorageexternal.workAsyncStorage.getStore();
        if (workStore) {
            workStore.pendingRevalidateWrites ??= [];
            workStore.pendingRevalidateWrites.push(pending);
        }
    }
    return {
        async get (cacheKey, softTags) {
            const frontEntry = await front.get(cacheKey, softTags);
            if (frontEntry) {
                // Cache hit: serve immediately (in a microtask). A background reconcile
                // keeps the front in sync with the backing for the next read;
                // reconciles for the same key are serialized, so concurrent cache hits
                // don't hit the backing in parallel.
                scheduleBackgroundSync(cacheKey, ()=>reconcileFrontFromBacking(front, backing, cacheKey, softTags, frontEntry));
                return frontEntry;
            }
            // Cold or evicted front entry: we pay the backing latency here (this is a
            // read that may legitimately surface a cold cache indicator). A miss
            // returns undefined and the "use cache" wrapper generates the entry and
            // writes it through both tiers via `set`.
            const backingEntry = await backing.get(cacheKey, softTags);
            if (!backingEntry) {
                return undefined;
            }
            // Mirror this freshly read backing entry into the front so the next read
            // hits it. The mirror is serialized per key: if a sync is already
            // running, this chains after it, so the front converges to this read even
            // if the backing changed since that sync started.
            const [servedEntry, mirroredEntry] = (0, _clonecacheentry.cloneCacheEntry)(backingEntry);
            scheduleBackgroundSync(cacheKey, ()=>mirrorIntoFront(front, cacheKey, mirroredEntry));
            return servedEntry;
        },
        async set (cacheKey, pendingEntry) {
            // Write through to both tiers. The entry's value stream is single-use, so
            // tee it into one entry per tier.
            const entry = await pendingEntry;
            const [frontEntry, backingEntry] = (0, _clonecacheentry.cloneCacheEntry)(entry);
            await Promise.all([
                front.set(cacheKey, Promise.resolve(frontEntry)),
                backing.set(cacheKey, Promise.resolve(backingEntry))
            ]);
        }
    };
}
/**
 * After serving a cache hit from the front, consult the backing and mirror a
 * newer entry into the front for the next read. Runs in the background;
 * failures are non-fatal.
 */ async function reconcileFrontFromBacking(front, backing, cacheKey, softTags, frontEntry) {
    try {
        const backingEntry = await backing.get(cacheKey, softTags);
        if (!backingEntry) {
            // The backing no longer has this entry (it was purged out-of-band). The
            // cache-handler interface has no per-key delete, so evict the front entry
            // by overwriting it with an already-expired copy: the next read sees a
            // front miss, falls through to the (also empty) backing, and the wrapper
            // regenerates. The entry we just served was the last stale read.
            await front.set(cacheKey, Promise.resolve(toExpiredEntry(frontEntry)));
            return;
        }
        if (backingEntry.timestamp > frontEntry.timestamp) {
            await front.set(cacheKey, Promise.resolve(backingEntry));
        } else {
            // The front is already up to date, so the backing entry goes unused.
            // Release its stream without awaiting: a teed stream's `cancel()` only
            // settles once the sibling branch (retained by the backing handler) is
            // also cancelled, so awaiting it here would hang the reconcile.
            void backingEntry.value.cancel();
        }
    } catch  {
    // Background warming; failures are non-fatal.
    }
}
/**
 * Mirror a backing entry into the front.
 */ async function mirrorIntoFront(front, cacheKey, entry) {
    try {
        await front.set(cacheKey, Promise.resolve(entry));
    } catch  {
    // Background warming; failures are non-fatal.
    }
}
/**
 * Build an already-expired copy of an entry, used to evict it from the front
 * handler (which has no per-key delete) once the backing no longer has it. The
 * default handler treats a negative `expire` as an eviction sentinel and
 * reports the entry as missing on the next read. A negative `expire` is used
 * rather than `0` because the dev front handler enforces a minimum retention,
 * so a `0` `expire` would be kept alive by that minimum instead of evicted. The
 * value is never read once the entry is evicted, but it must carry at least one
 * byte because the built-in LRU cache refuses to store size-0 entries.
 */ function toExpiredEntry(entry) {
    return {
        ...entry,
        expire: -1,
        value: new ReadableStream({
            start (controller) {
                controller.enqueue(new Uint8Array(1));
                controller.close();
            }
        })
    };
}

//# sourceMappingURL=tiered-cache-handler.js.map