import { MapDef, toRootHex } from "@lodestar/utils";
import { MapTracker } from "./mapMetrics.js";
import { CacheItemType } from "./types.js";
const MAX_EPOCHS = 10;
/**
 * In memory cache of CachedBeaconState
 * belonging to checkpoint
 *
 * Similar API to Repository
 */
export class InMemoryCheckpointStateCache {
    cache;
    /** Epoch -> Set<blockRoot> */
    epochIndex = new MapDef(() => new Set());
    /**
     * Max number of epochs allowed in the cache
     */
    maxEpochs;
    metrics;
    preComputedCheckpoint = null;
    preComputedCheckpointHits = null;
    constructor({ metrics = null }, { maxEpochs = MAX_EPOCHS } = {}) {
        this.cache = new MapTracker(metrics?.cpStateCache);
        if (metrics) {
            this.metrics = metrics.cpStateCache;
            metrics.cpStateCache.size.addCollect(() => metrics.cpStateCache.size.set({ type: CacheItemType.inMemory }, this.cache.size));
            metrics.cpStateCache.epochSize.addCollect(() => metrics.cpStateCache.epochSize.set({ type: CacheItemType.inMemory }, this.epochIndex.size));
        }
        this.maxEpochs = maxEpochs;
    }
    async getOrReload(cp, opts) {
        return this.get(cp, opts);
    }
    async getStateOrBytes(cp) {
        // no need to transfer cache for this api
        return this.get(cp, { dontTransferCache: true });
    }
    async getOrReloadLatest(rootHex, maxEpoch, opts) {
        return this.getLatest(rootHex, maxEpoch, opts);
    }
    async processState() {
        // do nothing, this class does not support prunning
        return 0;
    }
    get(cp, opts) {
        this.metrics?.lookups.inc();
        const cpKey = toCheckpointKey(cp);
        const item = this.cache.get(cpKey);
        if (!item) {
            return null;
        }
        this.metrics?.hits.inc();
        if (cpKey === this.preComputedCheckpoint) {
            this.preComputedCheckpointHits = (this.preComputedCheckpointHits ?? 0) + 1;
        }
        this.metrics?.stateClonedCount.observe(item.clonedCount);
        return item.clone(opts?.dontTransferCache);
    }
    add(cp, item) {
        const cpHex = toCheckpointHex(cp);
        const key = toCheckpointKey(cpHex);
        if (this.cache.has(key)) {
            return;
        }
        this.metrics?.adds.inc();
        this.cache.set(key, item);
        this.epochIndex.getOrDefault(cp.epoch).add(cpHex.rootHex);
    }
    /**
     * Searches for the latest cached state with a `root`, starting with `epoch` and descending
     */
    getLatest(rootHex, maxEpoch, opts) {
        // sort epochs in descending order, only consider epochs lte `epoch`
        const epochs = Array.from(this.epochIndex.keys())
            .sort((a, b) => b - a)
            .filter((e) => e <= maxEpoch);
        for (const epoch of epochs) {
            if (this.epochIndex.get(epoch)?.has(rootHex)) {
                return this.get({ rootHex, epoch }, opts);
            }
        }
        return null;
    }
    /**
     * Update the precomputed checkpoint and return the number of his for the
     * previous one (if any).
     */
    updatePreComputedCheckpoint(rootHex, epoch) {
        const previousHits = this.preComputedCheckpointHits;
        this.preComputedCheckpoint = toCheckpointKey({ rootHex, epoch });
        this.preComputedCheckpointHits = 0;
        return previousHits;
    }
    pruneFinalized(finalizedEpoch) {
        for (const epoch of this.epochIndex.keys()) {
            if (epoch < finalizedEpoch) {
                this.deleteAllEpochItems(epoch);
            }
        }
    }
    prune(finalizedEpoch, justifiedEpoch) {
        const epochs = Array.from(this.epochIndex.keys()).filter((epoch) => epoch !== finalizedEpoch && epoch !== justifiedEpoch);
        if (epochs.length > this.maxEpochs) {
            for (const epoch of epochs.slice(0, epochs.length - this.maxEpochs)) {
                this.deleteAllEpochItems(epoch);
            }
        }
    }
    delete(cp) {
        this.cache.delete(toCheckpointKey(toCheckpointHex(cp)));
        const epochKey = toRootHex(cp.root);
        const value = this.epochIndex.get(cp.epoch);
        if (value) {
            value.delete(epochKey);
            if (value.size === 0) {
                this.epochIndex.delete(cp.epoch);
            }
        }
    }
    deleteAllEpochItems(epoch) {
        for (const rootHex of this.epochIndex.get(epoch) || []) {
            this.cache.delete(toCheckpointKey({ rootHex, epoch }));
        }
        this.epochIndex.delete(epoch);
    }
    clear() {
        this.cache.clear();
        this.epochIndex.clear();
    }
    /** ONLY FOR DEBUGGING PURPOSES. For lodestar debug API */
    dumpSummary() {
        return Array.from(this.cache.entries()).map(([key, state]) => ({
            slot: state.slot,
            root: toRootHex(state.hashTreeRoot()),
            reads: this.cache.readCount.get(key) ?? 0,
            lastRead: this.cache.lastRead.get(key) ?? 0,
            checkpointState: true,
        }));
    }
    getStates() {
        return this.cache.values();
    }
    /** ONLY FOR DEBUGGING PURPOSES. For spec tests on error */
    dumpCheckpointKeys() {
        return Array.from(this.cache.keys());
    }
}
export function toCheckpointHex(checkpoint) {
    return {
        epoch: checkpoint.epoch,
        rootHex: toRootHex(checkpoint.root),
    };
}
export function toCheckpointKey(cp) {
    return `${cp.rootHex}:${cp.epoch}`;
}
//# sourceMappingURL=inMemoryCheckpointsCache.js.map