import { toRootHex } from "@lodestar/utils";
import { LinkedList } from "../../util/array.js";
import { MapTracker } from "./mapMetrics.js";
/**
 * Given `maxSkipSlots` = 32 and `DEFAULT_EARLIEST_PERMISSIBLE_SLOT_DISTANCE` = 32, lodestar doesn't need to
 * reload states in order to process a gossip block.
 *
 * |-----------------------------------------------|-----------------------------------------------|
 *                 maxSkipSlots                      DEFAULT_EARLIEST_PERMISSIBLE_SLOT_DISTANCE    ^
 *                                                                                             clock slot
 */
export const DEFAULT_MAX_BLOCK_STATES = 64;
/**
 * New implementation of BlockStateCache that keeps the most recent n states consistently
 *  - Maintain a linked list (FIFO) with special handling for head state, which is always the first item in the list
 *  - Prune per add() instead of per checkpoint so it only keeps n historical states consistently, prune from tail
 *  - No need to prune per finalized checkpoint
 *
 * Given this block tree with Block 11 as head:
 * ```
         Block 10
           |
     +-----+-----+
     |           |
  Block 11   Block 12
     ^           |
     |           |
    head       Block 13
 * ```
 * The maintained key order would be: 11 -> 13 -> 12 -> 10, and state 10 will be pruned first.
 */
export class FIFOBlockStateCache {
    /**
     * Max number of states allowed in the cache
     */
    maxStates;
    cache;
    /**
     * Key order to implement FIFO cache
     */
    keyOrder;
    metrics;
    constructor(opts, { metrics }) {
        this.maxStates = opts.maxBlockStates ?? DEFAULT_MAX_BLOCK_STATES;
        this.cache = new MapTracker(metrics?.stateCache);
        if (metrics) {
            this.metrics = metrics.stateCache;
            metrics.stateCache.size.addCollect(() => metrics.stateCache.size.set(this.cache.size));
        }
        this.keyOrder = new LinkedList();
    }
    /**
     * Set a state as head, happens when importing a block and head block is changed.
     */
    setHeadState(item) {
        if (item !== null) {
            this.add(item, true);
        }
    }
    /**
     * Get a seed state for state reload, this could be any states. The goal is to have the same
     * base merkle tree for all BeaconState objects across application.
     * See packages/state-transition/src/util/loadState/loadState.ts for more detail
     */
    getSeedState() {
        const firstValue = this.cache.values().next();
        if (firstValue.done) {
            // should not happen
            throw Error("No state in FIFOBlockStateCache");
        }
        const firstState = firstValue.value;
        // consumers should not mutate the returned state
        return firstState;
    }
    /**
     * Get a state from this cache given a state root hex.
     */
    get(rootHex) {
        this.metrics?.lookups.inc();
        const item = this.cache.get(rootHex);
        if (!item) {
            return null;
        }
        this.metrics?.hits.inc();
        this.metrics?.stateClonedCount.observe(item.clonedCount);
        return item;
    }
    /**
     * Add a state to this cache.
     * @param isHead if true, move it to the head of the list. Otherwise add to the 2nd position.
     * In importBlock() steps, normally it'll call add() with isHead = false first. Then call setHeadState() to set the head.
     */
    add(item, isHead = false) {
        const key = toRootHex(item.hashTreeRoot());
        if (this.cache.get(key) != null) {
            if (!this.keyOrder.has(key)) {
                throw Error(`State exists but key not found in keyOrder: ${key}`);
            }
            if (isHead) {
                this.keyOrder.moveToHead(key);
            }
            else {
                this.keyOrder.moveToSecond(key);
            }
            // same size, no prune
            return;
        }
        // new state
        this.metrics?.adds.inc();
        this.cache.set(key, item);
        if (isHead) {
            this.keyOrder.unshift(key);
        }
        else {
            // insert after head
            const head = this.keyOrder.first();
            if (head == null) {
                // should not happen, however handle just in case
                this.keyOrder.unshift(key);
            }
            else {
                this.keyOrder.insertAfter(head, key);
            }
        }
        this.prune(key);
    }
    get size() {
        return this.cache.size;
    }
    /**
     * Prune the cache from tail to keep the most recent n states consistently.
     * The tail of the list is the oldest state, in case regen adds back the same state,
     * it should stay next to head so that it won't be pruned right away.
     * The FIFO cache helps with this.
     */
    prune(lastAddedKey) {
        while (this.keyOrder.length > this.maxStates) {
            const key = this.keyOrder.last();
            // it does not make sense to prune the last added state
            // this only happens when max state is 1 in a short period of time
            if (key === lastAddedKey) {
                break;
            }
            if (!key) {
                // should not happen
                throw new Error("No key");
            }
            this.keyOrder.pop();
            this.cache.delete(key);
        }
    }
    /**
     * No need for this implementation
     * This is only to conform to the old api
     */
    deleteAllBeforeEpoch() { }
    /**
     * ONLY FOR DEBUGGING PURPOSES. For lodestar debug API.
     */
    clear() {
        this.cache.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: false,
        }));
    }
    getStates() {
        return this.cache.values();
    }
    /**
     * For unit test only.
     */
    dumpKeyOrder() {
        return this.keyOrder.toArray();
    }
}
//# sourceMappingURL=fifoBlockStateCache.js.map