import * as z from 'zod';
const defaultCacheOptions = {
    itemLimit: 10_000,
    ttl: 3600_000,
};
export const CacheData = z.record(z.string(), z.looseObject({
    /** @todo replace with temporal instant */
    $timestamp: z.coerce.date(),
}));
function isThenable(value) {
    return !!(value && typeof value == 'object' && 'then' in value && typeof value.then == 'function');
}
const kTimestamp = Symbol('kTimestamp');
/**
 * Cache some arbitrary operation.
 * This is primarily intended for de-duplicating API requests
 */
export class Cache {
    miss;
    items = new Map();
    pending = new Map();
    options;
    onWrite;
    constructor(
    /**
     * Function to run when there is a cache miss
     * @
     */
    miss, options = {}) {
        this.miss = miss;
        this.options = { ...defaultCacheOptions, ...options };
    }
    key(keys) {
        return keys.join(':');
    }
    get size() {
        return this.items.size;
    }
    valid(timestamp) {
        return !this.options.ttl || !timestamp || timestamp.valueOf() + this.options.ttl < Date.now();
    }
    write(key, value) {
        if (isThenable(value)) {
            this.pending.set(key, value);
            void value.then(resolved => {
                this.write(key, resolved);
                this.pending.delete(key);
            });
            return;
        }
        if (this.items.size >= this.options.itemLimit) {
            const [key] = this.items.entries().next().value;
            this.items.delete(key);
        }
        this.items.set(key, Object.assign(value, { [kTimestamp]: Date.now() }));
        this.onWrite?.(this.toJSON());
    }
    get(...keys) {
        const key = this.key(keys);
        const pending = this.pending.get(key);
        if (pending)
            return pending;
        const cached = this.items.get(key);
        if (cached) {
            if (this.valid(cached[kTimestamp]))
                return cached;
            this.items.delete(key);
        }
        const result = this.miss(...keys);
        this.write(key, result);
        return result;
    }
    set(...args) {
        const value = args.pop();
        const key = this.key(args);
        this.write(key, value);
    }
    invalidate(...keys) {
        this.items.delete(this.key(keys));
    }
    persist(onWrite, 
    /** @todo replace `$timestamp` with `Temporal.InstantLike` */
    existingContent) {
        this.onWrite = onWrite;
        if (!existingContent)
            return;
        for (const [key, value] of Object.entries(existingContent)) {
            const { $timestamp = Date.now() } = value;
            if (!this.valid($timestamp))
                continue;
            delete value.$timestamp;
            this.items.set(key, Object.assign(value, { [kTimestamp]: $timestamp }));
        }
    }
    toJSON() {
        return Object.fromEntries(this.items.entries().map(([k, v]) => [k, { ...v, $timestamp: v[kTimestamp] }]));
    }
}
export default Cache;
