export class ProbedResourceCache {
    #entries = new Map();
    #staleTimeMs;
    #errorCooldownMs;
    #nowMs;
    #listenersByKey = new Map();
    constructor(params) {
        this.#staleTimeMs = Math.max(0, params.staleTimeMs);
        this.#errorCooldownMs = Math.max(0, params.errorCooldownMs);
        this.#nowMs = typeof params.nowMs === 'function' ? params.nowMs : () => Date.now();
    }
    keys() {
        return this.#entries.keys();
    }
    clear() {
        this.#entries.clear();
        this.#listenersByKey.clear();
    }
    delete(key) {
        this.#entries.delete(key);
        this.#emit(key);
    }
    read(key) {
        const entry = this.#entries.get(key);
        if (!entry)
            return null;
        return {
            data: entry.data,
            dataUpdatedAt: entry.dataUpdatedAt,
            error: entry.error,
            errorUpdatedAt: entry.errorUpdatedAt,
            inflight: entry.inflight,
        };
    }
    getSnapshot(key, nowMs = this.#nowMs()) {
        const entry = this.#entries.get(key) ?? null;
        const data = entry?.data ?? null;
        const dataUpdatedAt = entry?.dataUpdatedAt ?? null;
        const error = entry?.error ?? null;
        const errorUpdatedAt = entry?.errorUpdatedAt ?? null;
        const inflight = entry?.inflight ?? null;
        const isStale = dataUpdatedAt !== null &&
            nowMs >= 0 &&
            nowMs - dataUpdatedAt > this.#staleTimeMs;
        const phase = inflight ? (data ? 'refreshing' : 'loading') : 'idle';
        return { phase, data, dataUpdatedAt, error, errorUpdatedAt, isStale };
    }
    setSuccess(key, data, nowMs = this.#nowMs()) {
        const entry = this.#entries.get(key) ?? {
            data: null,
            dataUpdatedAt: null,
            error: null,
            errorUpdatedAt: null,
            inflight: null,
        };
        entry.data = data;
        entry.dataUpdatedAt = nowMs;
        entry.error = null;
        entry.errorUpdatedAt = null;
        this.#entries.set(key, entry);
        this.#emit(key);
    }
    setError(key, error, nowMs = this.#nowMs()) {
        const entry = this.#entries.get(key) ?? {
            data: null,
            dataUpdatedAt: null,
            error: null,
            errorUpdatedAt: null,
            inflight: null,
        };
        entry.error = error;
        entry.errorUpdatedAt = nowMs;
        this.#entries.set(key, entry);
        this.#emit(key);
    }
    subscribe(key, listener) {
        const listeners = this.#listenersByKey.get(key) ?? new Set();
        listeners.add(listener);
        this.#listenersByKey.set(key, listeners);
        return () => {
            const current = this.#listenersByKey.get(key);
            if (!current)
                return;
            current.delete(listener);
            if (current.size === 0)
                this.#listenersByKey.delete(key);
        };
    }
    async ensure(key, fetcher, opts) {
        const nowMs = typeof opts?.nowMs === 'number' ? opts.nowMs : this.#nowMs();
        const existing = this.#entries.get(key) ?? {
            data: null,
            dataUpdatedAt: null,
            error: null,
            errorUpdatedAt: null,
            inflight: null,
        };
        if (existing.inflight)
            return await existing.inflight;
        const hasData = existing.dataUpdatedAt !== null;
        const isStale = hasData &&
            nowMs >= 0 &&
            nowMs - existing.dataUpdatedAt > this.#staleTimeMs;
        const inErrorCooldown = existing.errorUpdatedAt !== null &&
            nowMs >= 0 &&
            nowMs - existing.errorUpdatedAt < this.#errorCooldownMs;
        const shouldFetch = Boolean(opts?.force === true ||
            (!inErrorCooldown && (!hasData || isStale)));
        if (!shouldFetch) {
            this.#entries.set(key, existing);
            return existing.data;
        }
        const pending = (async () => {
            try {
                const value = await fetcher();
                const commitNowMs = this.#nowMs();
                existing.data = value;
                existing.dataUpdatedAt = commitNowMs;
                existing.error = null;
                existing.errorUpdatedAt = null;
                return value;
            }
            catch (e) {
                const commitNowMs = this.#nowMs();
                existing.error = e;
                existing.errorUpdatedAt = commitNowMs;
                return existing.data;
            }
            finally {
                existing.inflight = null;
                this.#entries.set(key, existing);
                this.#emit(key);
            }
        })();
        existing.inflight = pending;
        this.#entries.set(key, existing);
        this.#emit(key);
        return await pending;
    }
    #emit(key) {
        const listeners = this.#listenersByKey.get(key);
        if (!listeners)
            return;
        for (const listener of listeners) {
            try {
                listener();
            }
            catch {
                // ignore
            }
        }
    }
}
//# sourceMappingURL=probedResourceCache.js.map