export class RunBusyError extends Error {
    constructor(state) {
        super(`A run is already owned (${state}).`);
        this.name = "RunBusyError";
    }
}
/** Generation-safe ownership and acknowledged bounded cancellation. */
export class RunLifecycle {
    onStateChange;
    currentState = "idle";
    nextGeneration = 0;
    active;
    constructor(onStateChange) {
        this.onStateChange = onStateChange;
    }
    get state() {
        return this.currentState;
    }
    get running() {
        return this.currentState !== "idle";
    }
    get generation() {
        return this.active?.generation ?? this.nextGeneration;
    }
    begin(abort) {
        if (this.active)
            throw new RunBusyError(this.currentState);
        let resolveSettlement;
        const settlement = new Promise((resolve) => {
            resolveSettlement = resolve;
        });
        const generation = ++this.nextGeneration;
        this.active = {
            generation,
            abort,
            cancelRequested: false,
            settlement,
            resolveSettlement,
        };
        this.setState("running");
        return { generation };
    }
    isCancellationRequested(generation) {
        return this.active?.generation === generation && this.active.cancelRequested;
    }
    /** Settle only the matching owner; stale generations cannot release a new run. */
    settle(generation) {
        const active = this.active;
        if (!active || active.generation !== generation) {
            return { settled: false, cancelled: false };
        }
        const cancelled = active.cancelRequested;
        this.active = undefined;
        this.setState("idle");
        active.resolveSettlement();
        return { settled: true, cancelled };
    }
    /** Abort once, then acknowledge only after the owning operation settles. */
    cancel(timeoutMs) {
        const active = this.active;
        if (!active) {
            return Promise.resolve({ status: "idle", generation: this.nextGeneration });
        }
        if (active.cancelPromise)
            return active.cancelPromise;
        active.cancelRequested = true;
        this.setState("cancelling");
        try {
            active.abort();
        }
        catch {
            // Settlement remains authoritative even if one abort hook throws.
        }
        active.cancelPromise = new Promise((resolve) => {
            const timer = setTimeout(() => {
                if (this.active?.generation === active.generation)
                    this.setState("running");
                resolve({ status: "failed", generation: active.generation, reason: "timeout" });
            }, Math.max(0, timeoutMs));
            timer.unref();
            void active.settlement.then(() => {
                clearTimeout(timer);
                resolve({ status: "cancelled", generation: active.generation });
            });
        });
        return active.cancelPromise;
    }
    setState(state) {
        if (this.currentState === state)
            return;
        this.currentState = state;
        this.onStateChange?.(state);
    }
}
//# sourceMappingURL=run-lifecycle.js.map