import { getDurableRequestStore as getBaseRequestStore, getExpiringStore, deleteDurableRequestBody } from "../data/index.js";
import { ok } from "../is.js";
import { HeaderList } from "http-header-list";
import { getOrigin } from "../listen/config.js";
import { getConfig } from "../config/index.js";
import { fromDurableRequest, fromDurableResponse, fromRequestResponse } from "../data/index.js";
import { join } from "node:path";
import { createHash } from "crypto";
import { getInternalStorageBucket } from "../storage-buckets/internal.js";
function getDurableCacheStorageOrigin() {
    const config = getConfig();
    return (config.getDurableCacheStorageOrigin?.() ??
        getOrigin());
}
async function getRequest(request, getOrigin) {
    if (typeof request === "string" || request instanceof URL) {
        return new Request(new URL(request, getOrigin?.()));
    }
    if (request instanceof Request) {
        return request;
    }
    return fromDurableRequest(request, getOrigin);
}
function getRequestQueryURL(requestQuery, getOrigin) {
    if (typeof requestQuery === "string") {
        return new URL(requestQuery, getOrigin?.()).toString();
    }
    if (requestQuery instanceof URL) {
        return requestQuery.toString();
    }
    return requestQuery.url;
}
// https://w3c.github.io/ServiceWorker/#query-cache
function isQueryCacheMatch(requestQuery, request, options) {
    return (isMethodMatch() &&
        isURLMatch() &&
        isVaryMatch());
    function isMethodMatch() {
        if (options?.ignoreMethod) {
            return true;
        }
        return (request.method === "GET" ||
            request.method === "HEAD");
    }
    function isURLMatch() {
        if (!requestQuery) {
            return true;
        }
        const queryUrl = getRequestQueryURL(requestQuery, () => request.url);
        return getCacheURLString(queryUrl, options) === getCacheURLString(request.url, options);
    }
    function isVaryMatch() {
        if (!request.response) {
            return true;
        }
        if (options?.ignoreVary) {
            return true;
        }
        const responseHeaders = new Headers(request.response.headers);
        const vary = responseHeaders.get("Vary");
        if (!vary) {
            return true;
        }
        const requestHeaders = getRequestQueryHeaders(requestQuery);
        if (!requestHeaders) {
            // No headers no match, always matches
            return true;
        }
        const { list } = new HeaderList(vary);
        const values = list.map(({ value }) => value);
        ok(values.length, "Unexpected Vary list");
        for (const name of values) {
            if (name === "*") {
                return false;
            }
            const requestHeader = requestHeaders.get(name);
            const responseHeader = responseHeaders.get(name);
            if (requestHeader !== responseHeader) {
                return false;
            }
        }
        return true;
    }
}
function getCacheURLString(string, options) {
    const instance = new URL(string);
    if (options?.ignoreSearch) {
        instance.search = "";
    }
    if (instance.hash) {
        instance.hash = "";
    }
    return instance.toString();
}
function getRequestQueryHeaders(requestQuery) {
    if (typeof requestQuery === "string" || requestQuery instanceof URL) {
        return undefined;
    }
    return new Headers(requestQuery.headers);
}
function assertVaryValid(vary) {
    if (!vary) {
        return true;
    }
    /*
If innerResponse’s header list contains a header named `Vary`, then:

Let fieldValues be the list containing the items corresponding to the Vary header’s field-values.

For each fieldValue in fieldValues:

If fieldValue matches "*", return a promise rejected with a TypeError.
     */
    const { list } = new HeaderList(vary);
    const wildcard = list.find(({ value }) => value === "*");
    if (wildcard) {
        throw new TypeError(`Unsupported header "Vary: *"`);
    }
}
async function fetchPutOperation(info, init, getOrigin) {
    const request = await getRequest(info, getOrigin);
    const response = await fetch(request, init);
    return {
        type: "put",
        request,
        response
    };
}
export class DurableCache {
    name;
    url;
    constructor({ name, url }) {
        this.name = name;
        this.url = url;
    }
    async add(info, init) {
        const { request, response } = await fetchPutOperation(info, init, this.url);
        return this.put(request, response);
    }
    async addAll(requests) {
        const controller = new AbortController();
        const { signal } = controller;
        try {
            const operations = await Promise.all(requests.map(request => fetchPutOperation(request, {
                signal
            }, this.url)));
            await Promise.all(operations.map(({ request, response }) => this.put(request, response)));
        }
        catch (error) {
            if (!signal.aborted) {
                controller.abort(error);
            }
            throw error;
        }
        finally {
            if (!signal.aborted) {
                controller.abort();
            }
        }
    }
    async delete(requestQuery, options) {
        return deleteDurableRequests(this.name, requestQuery, options, this.url);
    }
    async match(requestQuery, options) {
        return matchResponse(this.name, requestQuery, options, this.url);
    }
    async matchAll(requestQuery, options) {
        const responses = [];
        for await (const response of matchResponses(this.name, requestQuery, options, this.url)) {
            responses.push(response);
        }
        return responses;
    }
    // https://w3c.github.io/ServiceWorker/#cache-put
    async put(requestQuery, response) {
        const url = getRequestQueryURL(requestQuery, this.url);
        ok(!response.bodyUsed);
        assertVaryValid(response.headers.get("Vary"));
        const method = getRequestMethod();
        ok(method === "GET" || method === "HEAD", "Requests that aren't GET or HEAD will not be matchable");
        const clonedRequest = new Request(url, {
            method,
            headers: getRequestQueryHeaders(requestQuery)
        });
        const cacheUrl = getBaseCacheURLString(url);
        const { search } = new URL(url);
        const durableRequestIdHash = createHash("sha512");
        durableRequestIdHash.update(search || "?");
        const durableRequestId = durableRequestIdHash.digest().toString("hex");
        const durableRequestData = await fromRequestResponse(clonedRequest, response, {
            fileName: join(CACHE_STORE_NAME, this.name, cacheUrl, durableRequestId),
            persist: true
        });
        const createdAt = new Date().toISOString();
        const durableRequest = {
            ...durableRequestData,
            durableRequestId,
            createdAt,
            updatedAt: createdAt
        };
        const urlStore = getDurableURLStore(this.name);
        const requestStore = getDurableRequestStore(this.name, cacheUrl);
        await Promise.all([
            urlStore.set(cacheUrl, {
                url: cacheUrl
            }),
            requestStore.set(durableRequestId, durableRequest)
        ]);
        function getRequestMethod() {
            if (typeof requestQuery === "string" || requestQuery instanceof URL) {
                return "GET";
            }
            return requestQuery.method;
        }
    }
    async keys(requestQuery, options) {
        const requests = [];
        for await (const durableRequest of matchDurableRequests(this.name, requestQuery, options, this.url)) {
            requests.push(await fromDurableRequest(durableRequest, this.url));
        }
        return requests;
    }
}
async function deleteDurableRequests(cacheName, requestQuery, options, getOrigin) {
    let deleted = false;
    const stores = new Map();
    for await (const durableRequest of matchDurableRequests(cacheName, requestQuery, options, getOrigin)) {
        const url = getRequestQueryURL(durableRequest, getOrigin);
        const cacheUrl = getBaseCacheURLString(url);
        const requestStore = stores.get(cacheUrl) ?? getDurableRequestStore(cacheName, url);
        stores.set(cacheUrl, requestStore);
        await Promise.all([
            requestStore.delete(durableRequest.durableRequestId),
            await deleteDurableRequestBody(durableRequest)
        ]);
        deleted = true;
    }
    const urlStore = getDurableURLStore(cacheName);
    for (const [url, store] of stores.entries()) {
        const keys = await store.keys();
        if (!keys.length) {
            await urlStore.delete(url);
        }
    }
    return deleted;
}
const CACHE_STORE_NAME = "fetch:cache";
const CACHE_REFERENCE_STORE_NAME = "fetch:cacheReference";
const CACHE_URL_STORE_NAME = "fetch:cacheURL";
function getBaseCacheURLString(url) {
    return getCacheURLString(url, {
        ignoreSearch: true
    });
}
function getDurableRequestStore(cacheName, url) {
    return getDurableRequestStoreWithPrefix(`${cacheName}:request:${getBaseCacheURLString(url)}`);
}
function getDurableURLStore(cacheName) {
    return getExpiringStore(`${CACHE_URL_STORE_NAME}:${cacheName}`);
}
function getDurableRequestStoreWithPrefix(prefix) {
    return getBaseRequestStore({
        name: CACHE_STORE_NAME,
        prefix,
    });
}
function getDurableCacheReferenceStore(internalBucket) {
    return internalBucket.getKeyValueStore(CACHE_REFERENCE_STORE_NAME, {
        counter: false
    });
}
async function firstNext(iterable) {
    const iterator = iterable[Symbol.asyncIterator]();
    const next = await iterator.next();
    await iterator.return?.();
    return next;
}
async function matchResponse(cacheName, requestQuery, options, getOrigin) {
    const next = await firstNext(matchResponses(cacheName, requestQuery, options, getOrigin));
    return next.value;
}
async function* matchResponses(cacheName, requestQuery, options, getOrigin) {
    for await (const { response } of matchDurableRequests(cacheName, requestQuery, options, getOrigin)) {
        if (response) {
            yield await fromDurableResponse(response);
        }
    }
}
async function* matchDurableRequests(cacheName, requestQuery, options, getOrigin) {
    if (requestQuery) {
        for await (const request of generateDurableRequestsForUrl(getRequestQueryURL(requestQuery, getOrigin))) {
            if (isQueryCacheMatch(requestQuery, request, options)) {
                yield request;
            }
        }
    }
    else {
        const store = getDurableURLStore(cacheName);
        for (const url of await store.keys()) {
            yield* generateDurableRequestsForUrl(url);
        }
    }
    async function* generateDurableRequestsForUrl(url) {
        for await (const request of getDurableRequestStore(cacheName, url)) {
            yield request;
        }
    }
}
export class DurableCacheStorage {
    caches;
    url;
    internalBucket;
    constructor(options) {
        this.url = options.url;
        this.caches = new Map();
        this.internalBucket = options.internalBucket ?? getInternalStorageBucket();
    }
    async open(cacheName) {
        const existing = this.caches.get(cacheName);
        if (existing) {
            return existing;
        }
        const store = getDurableCacheReferenceStore(this.internalBucket);
        const existingReference = await store.get(cacheName);
        const lastOpenedAt = new Date().toISOString();
        const reference = {
            createdAt: existingReference?.createdAt || lastOpenedAt,
            cacheName,
            lastOpenedAt
        };
        await store.set(cacheName, reference);
        const cache = new DurableCache({
            name: cacheName,
            url: this.url
        });
        this.caches.set(cacheName, cache);
        return cache;
    }
    async has(cacheName) {
        if (this.caches.has(cacheName)) {
            return true;
        }
        const store = getDurableCacheReferenceStore(this.internalBucket);
        return store.has(cacheName);
    }
    async keys() {
        const store = getDurableCacheReferenceStore(this.internalBucket);
        return await store.keys();
    }
    async delete(cacheName) {
        if (!(await this.has(cacheName))) {
            return false;
        }
        await deleteDurableRequests(cacheName, undefined, undefined, this.url);
        const store = getDurableCacheReferenceStore(this.internalBucket);
        await store.delete(cacheName);
        this.caches.delete(cacheName);
        return true;
    }
    async match(requestQuery, options) {
        for (const cacheName of await this.keys()) {
            const cache = await this.open(cacheName);
            const match = await cache.match(requestQuery, options);
            if (match) {
                return match;
            }
        }
        return undefined;
    }
}
export const caches = new DurableCacheStorage({
    url: getDurableCacheStorageOrigin
});
//# sourceMappingURL=cache.js.map