"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const CachedResponseStateEnum_js_1 = __importDefault(require("./CachedResponseStateEnum.cjs"));
const Headers_js_1 = __importDefault(require("../../Headers.cjs"));
const UPDATE_RESPONSE_HEADERS = ['Cache-Control', 'Last-Modified', 'Vary', 'ETag'];
/**
 * Fetch response cache.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
 * @see https://www.mnot.net/cache_docs/
 */
class ResponseCache {
    #entries = {};
    /**
     * Returns cached response.
     *
     * @param request Request.
     * @returns Cached response.
     */
    get(request) {
        if (request.headers.get('Cache-Control')?.includes('no-cache')) {
            return null;
        }
        const url = request.url;
        if (this.#entries[url]) {
            for (let i = 0, max = this.#entries[url].length; i < max; i++) {
                const entry = this.#entries[url][i];
                let isMatch = entry.request.method === request.method;
                if (isMatch) {
                    for (const header of Object.keys(entry.vary)) {
                        const requestHeader = request.headers.get(header);
                        if (requestHeader !== null && entry.vary[header] !== requestHeader) {
                            isMatch = false;
                            break;
                        }
                    }
                }
                if (isMatch) {
                    if (entry.expires && entry.expires < Date.now()) {
                        if (entry.lastModified) {
                            entry.state = CachedResponseStateEnum_js_1.default.stale;
                        }
                        else if (!entry.etag) {
                            this.#entries[url].splice(i, 1);
                            return null;
                        }
                    }
                    return entry;
                }
            }
        }
        return null;
    }
    /**
     * Adds a cache entity.
     *
     * @param request Request.
     * @param response Response.
     * @returns Cached response.
     */
    add(request, response) {
        // We should only cache GET and HEAD requests.
        if ((request.method !== 'GET' && request.method !== 'HEAD') ||
            request.headers.get('Cache-Control')?.includes('no-cache')) {
            return null;
        }
        const url = request.url;
        let cachedResponse = this.get(request);
        if (response.status === 304) {
            if (!cachedResponse) {
                throw new Error('ResponseCache: Cached response not found.');
            }
            for (const name of UPDATE_RESPONSE_HEADERS) {
                if (response.headers.has(name)) {
                    cachedResponse.response.headers.set(name, response.headers.get(name));
                }
            }
            cachedResponse.cacheUpdateTime = Date.now();
            cachedResponse.state = CachedResponseStateEnum_js_1.default.fresh;
        }
        else {
            if (cachedResponse) {
                const index = this.#entries[url].indexOf(cachedResponse);
                if (index !== -1) {
                    this.#entries[url].splice(index, 1);
                }
            }
            cachedResponse = {
                response: {
                    status: response.status,
                    statusText: response.statusText,
                    url: response.url,
                    headers: new Headers_js_1.default(response.headers),
                    // We need to wait for the body to be consumed and then populated if set to true (e.g. by using Response.text()).
                    waitingForBody: response.waitingForBody,
                    body: response.body ?? null
                },
                request: {
                    headers: request.headers,
                    method: request.method
                },
                vary: {},
                expires: null,
                etag: null,
                cacheUpdateTime: Date.now(),
                lastModified: null,
                mustRevalidate: false,
                staleWhileRevalidate: false,
                state: CachedResponseStateEnum_js_1.default.fresh
            };
            this.#entries[url] = this.#entries[url] || [];
            this.#entries[url].push(cachedResponse);
        }
        if (response.headers.has('Cache-Control')) {
            const age = response.headers.get('Age');
            for (const part of response.headers.get('Cache-Control').split(',')) {
                const [key, value] = part.trim().split('=');
                switch (key) {
                    case 'max-age':
                        cachedResponse.expires =
                            Date.now() + parseInt(value) * 1000 - (age ? parseInt(age) * 1000 : 0);
                        break;
                    case 'no-cache':
                    case 'no-store':
                        const index = this.#entries[url].indexOf(cachedResponse);
                        if (index !== -1) {
                            this.#entries[url].splice(index, 1);
                        }
                        return null;
                    case 'must-revalidate':
                        cachedResponse.mustRevalidate = true;
                        break;
                    case 'stale-while-revalidate':
                        cachedResponse.staleWhileRevalidate = true;
                        break;
                }
            }
        }
        if (response.headers.has('Last-Modified')) {
            cachedResponse.lastModified = Date.parse(response.headers.get('Last-Modified'));
        }
        if (response.headers.has('Vary')) {
            for (const header of response.headers.get('Vary').split(',')) {
                const name = header.trim();
                const value = request.headers.get(name);
                if (value) {
                    cachedResponse.vary[name] = value;
                }
            }
        }
        if (response.headers.has('ETag')) {
            cachedResponse.etag = response.headers.get('ETag');
        }
        if (!cachedResponse.expires) {
            const expires = response.headers.get('Expires');
            if (expires) {
                cachedResponse.expires = Date.parse(expires);
            }
        }
        // Cache is invalid if it has expired and doesn't have an ETag.
        if (!cachedResponse.etag && (!cachedResponse.expires || cachedResponse.expires < Date.now())) {
            const index = this.#entries[url].indexOf(cachedResponse);
            if (index !== -1) {
                this.#entries[url].splice(index, 1);
            }
            return null;
        }
        return cachedResponse;
    }
    /**
     * Clears the cache.
     *
     * @param [options] Options.
     * @param [options.url] URL.
     * @param [options.toTime] Removes all entries that are older than this time. Time in MS.
     */
    clear(options) {
        if (options) {
            if (options.toTime) {
                for (const key of options.url ? [options.url] : Object.keys(this.#entries)) {
                    if (this.#entries[key]) {
                        for (let i = 0, max = this.#entries[key].length; i < max; i++) {
                            if (this.#entries[key][i].cacheUpdateTime < options.toTime) {
                                this.#entries[key].splice(i, 1);
                                i--;
                                max--;
                            }
                        }
                    }
                }
            }
            else if (options.url) {
                delete this.#entries[options.url];
            }
        }
        else {
            this.#entries = {};
        }
    }
}
exports.default = ResponseCache;
//# sourceMappingURL=ResponseCache.cjs.map