"use strict";
/**
 * An API for efficiently caching Response objects in the regional Fly cache.
 *
 * Usage:
 *
 * ```javascript
 * import { responseCache } from "@fly/cache"
 *
 * const resp = await fetch("http://example.com")
 *
 * // cache for an hour
 * await responseCache.set("example-com", resp, 3600)
 *
 * const cachedResponse = await responseCache.get("example-com")
 * ```
 *
 * See {@link fly/cache} for caching lower level types.
 * @preferred
 * @module fly/cache/response
 */
Object.defineProperty(exports, "__esModule", { value: true });
/** */
const _1 = require(".");
/**
 * Get a Response object from cache.
 * @param key cache key to get
 * @return The response associated with the key, or null if empty
 */
async function get(key) {
    let [meta, body] = await Promise.all([
        getMeta(key),
        _1.default.get(key + ":body")
    ]);
    if (!meta || !body)
        return null; // miss
    let age = 0;
    if (meta.at) {
        age = Math.round(Date.now() / 1000) - meta.at;
        meta.headers.Age = age.toString();
        meta.headers['Fly-Age'] = meta.headers.Age;
        delete meta.at;
    }
    const resp = new Response(body, meta);
    return Object.assign(resp, { key: key });
}
exports.get = get;
/**
 * Gets Request metatadata from the cache
 * @param key cache key to get metadata for
 */
async function getMeta(key) {
    let meta = await _1.default.getString(key + ':meta');
    if (!meta)
        return; // cache miss
    try {
        meta = JSON.parse(meta);
    }
    catch (err) {
        return null; // also a miss
    }
    return meta;
}
exports.getMeta = getMeta;
/**
 *
 * @param key Cache key to set
 * @param meta Metadata for the Response
 * @param opts Time to live
 */
function setMeta(key, meta, options) {
    return _1.default.set(key + ":meta", JSON.stringify(meta), options);
}
exports.setMeta = setMeta;
const defaultSkipHeaders = [
    'authorization',
    'set-cookie'
];
/**
 * Stores a Response object in the Fly cache.
 * @param key Cache key to set
 * @param resp The response to cache
 * @param options Time to live
 */
async function set(key, resp, options) {
    const ttl = typeof options === "number" ? options : (options && options.ttl);
    let tags = undefined;
    let skipHeaderOption = defaultSkipHeaders;
    if (typeof options === "object") {
        tags = options.tags;
        skipHeaderOption = [...skipHeaderOption, ...(options.skipCacheHeaders || []).map((headerKey) => headerKey.toLowerCase())];
    }
    const meta = {
        status: resp.status,
        headers: {},
        at: Math.round(Date.now() / 1000),
        ttl: ttl,
        tags: tags
    };
    const body = await resp.clone().arrayBuffer();
    let etag = resp.headers.get("etag");
    if (!etag || etag == '') {
        etag = hex(await crypto.subtle.digest("SHA-1", body));
        resp.headers.set("etag", etag);
    }
    const skipHeaderSet = new Set(skipHeaderOption);
    for (const headerSet of resp.headers) {
        const [name, value] = headerSet;
        if (skipHeaderSet.has(name.toLowerCase())) {
            continue;
        }
        const existingVal = meta.headers[name];
        if (existingVal) {
            meta.headers[name] = `${existingVal}, ${value}`;
        }
        else {
            meta.headers[name] = value;
        }
    }
    return cacheResult([
        setMeta(key, meta, options),
        _1.default.set(key + ':body', body, options)
    ]);
}
exports.set = set;
/**
 * Resets the "age" of the cached Response object
 * @param key Response to "touch"
 */
async function touch(key) {
    let meta = await getMeta(key);
    if (!meta)
        return false;
    meta.at = Math.round(Date.now() / 1000);
    return await setMeta(key, meta, { ttl: meta.ttl, tags: meta.tags });
}
exports.touch = touch;
/**
 * Sets a new expiration time for a Response object
 * @param key Key to set an expiration for
 * @param ttl Time to live
 */
async function expire(key, ttl) {
    return cacheResult([
        _1.default.expire(key + ":meta", ttl),
        _1.default.expire(key + ":body", ttl)
    ]);
}
exports.expire = expire;
/**
 * Replace tags for a cached Response
 * @param key The key to modify
 * @param tags Tags to apply to key
 * @returns true if tags were successfully updated
 */
function setTags(key, tags) {
    return cacheResult([
        _1.default.setTags(key + ":meta", tags),
        _1.default.setTags(key + ":body", tags)
    ]);
}
exports.setTags = setTags;
/**
 * Deletes a Response object from the cache
 * @param key Key to delete
 */
async function del(key) {
    return cacheResult([
        _1.default.del(key + ":meta"),
        _1.default.del(key + ":body")
    ]);
}
exports.del = del;
exports.default = {
    get,
    getMeta,
    set,
    setTags,
    touch,
    expire,
    del
};
async function cacheResult(ops) {
    const results = await Promise.all(ops);
    for (const r of results) {
        if (r === false) {
            return false;
        }
    }
    return true;
}
// converts a buffer to hex, mainly for hashes
function hex(buffer) {
    let hexCodes = [];
    let view = new DataView(buffer);
    for (let i = 0; i < view.byteLength; i += 4) {
        // Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
        let value = view.getUint32(i);
        // toString(16) will give the hex representation of the number without padding
        let stringValue = value.toString(16);
        // We use concatenation and slice for padding
        let padding = '00000000';
        let paddedValue = (padding + stringValue).slice(-padding.length);
        hexCodes.push(paddedValue);
    }
    // Join all the hex strings into one
    return hexCodes.join("");
}
//# sourceMappingURL=response.js.map