import EventEmitter from "node:events";
import { computeEpochAtSlot, computeTimeAtSlot, getCurrentSlot } from "@lodestar/state-transition";
import { ErrorAborted } from "@lodestar/utils";
export { ClockEvent };
var ClockEvent;
(function (ClockEvent) {
    /**
     * This event signals the start of a new slot, and that subsequent calls to `clock.currentSlot` will equal `slot`.
     * This event is guaranteed to be emitted every `SLOT_DURATION_MS` milliseconds.
     */
    ClockEvent["slot"] = "clock:slot";
    /**
     * This event signals the start of a new epoch, and that subsequent calls to `clock.currentEpoch` will return `epoch`.
     * This event is guaranteed to be emitted every `SLOT_DURATION_MS * SLOTS_PER_EPOCH` milliseconds.
     */
    ClockEvent["epoch"] = "clock:epoch";
})(ClockEvent || (ClockEvent = {}));
/**
 * A local clock, the clock time is assumed to be trusted
 */
export class Clock extends EventEmitter {
    genesisTime;
    config;
    timeoutId;
    signal;
    _currentSlot;
    constructor({ config, genesisTime, signal }) {
        super();
        this.config = config;
        this.genesisTime = genesisTime;
        this.timeoutId = setTimeout(this.onNextSlot, this.msUntilNextSlot());
        this.signal = signal;
        this._currentSlot = getCurrentSlot(this.config, this.genesisTime);
        this.signal.addEventListener("abort", () => clearTimeout(this.timeoutId), { once: true });
    }
    get currentSlot() {
        const slot = getCurrentSlot(this.config, this.genesisTime);
        if (slot > this._currentSlot) {
            clearTimeout(this.timeoutId);
            this.onNextSlot(slot);
        }
        return slot;
    }
    /**
     * If it's too close to next slot given MAXIMUM_GOSSIP_CLOCK_DISPARITY, return currentSlot + 1.
     * Otherwise return currentSlot
     *
     * Spec: phase0/p2p-interface.md - gossip validation uses `current_time + MAXIMUM_GOSSIP_CLOCK_DISPARITY < message_time`
     * to reject future messages (strict `<`), so the boundary (exactly equal) is accepted, hence `<=` here.
     */
    get currentSlotWithGossipDisparity() {
        const currentSlot = this.currentSlot;
        const nextSlotTime = computeTimeAtSlot(this.config, currentSlot + 1, this.genesisTime) * 1000;
        return nextSlotTime - Date.now() <= this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY ? currentSlot + 1 : currentSlot;
    }
    get currentEpoch() {
        return computeEpochAtSlot(this.currentSlot);
    }
    /** Returns the slot if the internal clock were advanced by `toleranceSec`. */
    slotWithFutureTolerance(toleranceSec) {
        // this is the same to getting slot at now + toleranceSec
        return getCurrentSlot(this.config, this.genesisTime - toleranceSec);
    }
    /** Returns the slot if the internal clock were reversed by `toleranceSec`. */
    slotWithPastTolerance(toleranceSec) {
        // this is the same to getting slot at now - toleranceSec
        return getCurrentSlot(this.config, this.genesisTime + toleranceSec);
    }
    /**
     * Check if a slot is current slot given MAXIMUM_GOSSIP_CLOCK_DISPARITY.
     *
     * Uses `<=` for disparity checks because the spec rejects with strict `<`
     * (phase0/p2p-interface.md), meaning the boundary (exactly equal) is accepted.
     */
    isCurrentSlotGivenGossipDisparity(slot) {
        const currentSlot = this.currentSlot;
        if (currentSlot === slot) {
            return true;
        }
        const nextSlotTime = computeTimeAtSlot(this.config, currentSlot + 1, this.genesisTime) * 1000;
        // we're too close to next slot, accept next slot
        if (nextSlotTime - Date.now() <= this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY) {
            return slot === currentSlot + 1;
        }
        const currentSlotTime = computeTimeAtSlot(this.config, currentSlot, this.genesisTime) * 1000;
        // we've just passed the current slot, accept previous slot
        if (Date.now() - currentSlotTime <= this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY) {
            return slot === currentSlot - 1;
        }
        return false;
    }
    async waitForSlot(slot) {
        if (this.signal.aborted) {
            throw new ErrorAborted();
        }
        if (this.currentSlot >= slot) {
            return;
        }
        return new Promise((resolve, reject) => {
            const onSlot = (clockSlot) => {
                if (clockSlot >= slot) {
                    onDone();
                }
            };
            const onDone = () => {
                this.off(ClockEvent.slot, onSlot);
                this.signal.removeEventListener("abort", onAbort);
                resolve();
            };
            const onAbort = () => {
                this.off(ClockEvent.slot, onSlot);
                reject(new ErrorAborted());
            };
            this.on(ClockEvent.slot, onSlot);
            this.signal.addEventListener("abort", onAbort, { once: true });
        });
    }
    secFromSlot(slot, toSec = Date.now() / 1000) {
        return toSec - computeTimeAtSlot(this.config, slot, this.genesisTime);
    }
    msFromSlot(slot, toMs = Date.now()) {
        return toMs - computeTimeAtSlot(this.config, slot, this.genesisTime) * 1000;
    }
    onNextSlot = (slot) => {
        const clockSlot = slot ?? getCurrentSlot(this.config, this.genesisTime);
        // process multiple clock slots in the case the main thread has been saturated for > SLOT_DURATION_MS
        while (this._currentSlot < clockSlot && !this.signal.aborted) {
            const previousSlot = this._currentSlot;
            this._currentSlot++;
            this.emit(ClockEvent.slot, this._currentSlot);
            const previousEpoch = computeEpochAtSlot(previousSlot);
            const currentEpoch = computeEpochAtSlot(this._currentSlot);
            if (previousEpoch < currentEpoch) {
                this.emit(ClockEvent.epoch, currentEpoch);
            }
        }
        if (!this.signal.aborted) {
            //recursively invoke onNextSlot
            this.timeoutId = setTimeout(this.onNextSlot, this.msUntilNextSlot());
        }
    };
    msUntilNextSlot() {
        const milliSecondsPerSlot = this.config.SLOT_DURATION_MS;
        const diffInMilliSeconds = Date.now() - this.genesisTime * 1000;
        return milliSecondsPerSlot - (diffInMilliSeconds % milliSecondsPerSlot);
    }
}
//# sourceMappingURL=clock.js.map