import { MapDef } from "@lodestar/utils";
/**
 * To prevent our node from having to reprocess while struggling to sync,
 * we only want to reprocess attestations if block reaches our node before this time.
 */
export const REPROCESS_MIN_TIME_TO_NEXT_SLOT_SEC = 2;
export { ReprocessStatus };
/**
 * Reprocess status for metrics
 */
var ReprocessStatus;
(function (ReprocessStatus) {
    /**
     * There are too many attestations that have unknown block root.
     */
    ReprocessStatus["reached_limit"] = "reached_limit";
    /**
     * The awaiting attestation is pruned per clock slot.
     */
    ReprocessStatus["expired"] = "expired";
})(ReprocessStatus || (ReprocessStatus = {}));
// How many attestations (aggregate + unaggregate) we keep before new ones get dropped.
const MAXIMUM_QUEUED_ATTESTATIONS = 16_384;
/**
 * Some attestations may reach our node before the voted block, so we manage a cache to reprocess them
 * when the block come.
 * (n)                                               (n + 1)
 *  |----------------|----------------|----------|------|
 *                   |                |          |
 *                  att           agg att        |
 *                                              block
 * Since the gossip handler has to return validation result to js-libp2p-gossipsub, this class should not
 * reprocess attestations, it should control when the attestations are ready to reprocess instead.
 */
export class ReprocessController {
    metrics;
    awaitingPromisesByRootBySlot;
    awaitingPromisesCount = 0;
    constructor(metrics) {
        this.metrics = metrics;
        this.awaitingPromisesByRootBySlot = new MapDef(() => new Map());
    }
    /**
     * Returns Promise that resolves either on block found or once 1 slot passes.
     * Used to handle unknown block root for both unaggregated and aggregated attestations.
     * @returns true if blockFound
     */
    waitForBlockOfAttestation(slot, root) {
        this.metrics?.reprocessApiAttestations.total.inc();
        if (this.awaitingPromisesCount >= MAXIMUM_QUEUED_ATTESTATIONS) {
            this.metrics?.reprocessApiAttestations.reject.inc({ reason: ReprocessStatus.reached_limit });
            return Promise.resolve(false);
        }
        this.awaitingPromisesCount++;
        const awaitingPromisesByRoot = this.awaitingPromisesByRootBySlot.getOrDefault(slot);
        const promiseCached = awaitingPromisesByRoot.get(root);
        if (promiseCached) {
            promiseCached.awaitingAttestationsCount++;
            return promiseCached.promise;
        }
        // Capture both the promise and its callbacks.
        // It is not spec'ed but in tests in Firefox and NodeJS the promise constructor is run immediately
        let resolve = null;
        const promise = new Promise((resolveCB) => {
            resolve = resolveCB;
        });
        if (resolve === null) {
            throw Error("Promise Constructor was not executed immediately");
        }
        awaitingPromisesByRoot.set(root, {
            promise,
            awaitingAttestationsCount: 1,
            resolve,
            addedTimeMs: Date.now(),
        });
        return promise;
    }
    /**
     * It's important to make sure our node is synced before we reprocess,
     * it means the processed slot is same to clock slot
     * Note that we want to use clock advanced by REPROCESS_MIN_TIME_TO_NEXT_SLOT instead of
     * clockSlot because we want to make sure our node is healthy while reprocessing attestations.
     * If a block reach our node 1s before the next slot, for example, then probably node
     * is struggling and we don't want to reprocess anything at that time.
     */
    onBlockImported({ slot: blockSlot, root }, advancedSlot) {
        // we are probably resyncing, don't want to reprocess attestations here
        if (blockSlot < advancedSlot)
            return;
        // resolve all related promises
        const awaitingPromisesBySlot = this.awaitingPromisesByRootBySlot.getOrDefault(blockSlot);
        const awaitingPromise = awaitingPromisesBySlot.get(root);
        if (awaitingPromise) {
            const { resolve, addedTimeMs, awaitingAttestationsCount } = awaitingPromise;
            resolve(true);
            this.awaitingPromisesCount -= awaitingAttestationsCount;
            this.metrics?.reprocessApiAttestations.resolve.inc(awaitingAttestationsCount);
            this.metrics?.reprocessApiAttestations.waitSecBeforeResolve.set((Date.now() - addedTimeMs) / 1000);
        }
        // prune
        awaitingPromisesBySlot.delete(root);
    }
    /**
     * It's important to make sure our node is synced before reprocessing attestations,
     * it means clockSlot is the same to last processed block's slot, and we don't reprocess
     * attestations of old slots.
     * So we reject and prune all old awaiting promises per clock slot.
     * @param clockSlot
     */
    onSlot(clockSlot) {
        const now = Date.now();
        for (const [key, awaitingPromisesByRoot] of this.awaitingPromisesByRootBySlot.entries()) {
            if (key < clockSlot) {
                // reject all related promises
                for (const awaitingPromise of awaitingPromisesByRoot.values()) {
                    const { resolve, addedTimeMs } = awaitingPromise;
                    resolve(false);
                    this.metrics?.reprocessApiAttestations.waitSecBeforeReject.set({ reason: ReprocessStatus.expired }, (now - addedTimeMs) / 1000);
                    this.metrics?.reprocessApiAttestations.reject.inc({ reason: ReprocessStatus.expired });
                }
                // prune
                this.awaitingPromisesByRootBySlot.delete(key);
            }
            else {
                break;
            }
        }
        // in theory there are maybe some awaiting promises waiting for a slot > clockSlot
        // in reality this never happens so reseting awaitingPromisesCount to 0 to make it simple
        this.awaitingPromisesCount = 0;
    }
}
//# sourceMappingURL=reprocess.js.map