"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HealthDaemon = exports.EXIT_SUCCESS = void 0;
const defaultTrigger_1 = require("../trigger/defaultTrigger");
exports.EXIT_SUCCESS = 'EXIT_SUCCESS';
/**
 * Wanted a structure that deals with controlling daemons by their health status
 * States:
 * -- Waiting for dependencies to be success
 * -- Running: Daemon is running and the status is in the health
 *
 */
class HealthDaemon {
    constructor(daemon, dependencies, id, ready, effects) {
        this.daemon = daemon;
        this.dependencies = dependencies;
        this.id = id;
        this.ready = ready;
        this.effects = effects;
        this._health = { result: 'waiting', message: null };
        this.healthWatchers = [];
        this.running = false;
        this.resolvedReady = false;
        this.session = null;
        this.readyPromise = new Promise((resolve) => (this.resolveReady = () => {
            resolve();
            this.resolvedReady = true;
        }));
        this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus()));
    }
    /** Run after we want to do cleanup */
    async term(termOptions) {
        this.healthWatchers = [];
        this.running = false;
        await this.stopSession();
        await this.daemon?.term({
            ...termOptions,
        });
    }
    /** Want to add another notifier that the health might have changed */
    addWatcher(watcher) {
        this.healthWatchers.push(watcher);
    }
    get health() {
        return Object.freeze(this._health);
    }
    async changeRunning(newStatus) {
        if (this.running === newStatus)
            return;
        this.running = newStatus;
        if (newStatus) {
            console.debug(`Launching ${this.id}...`);
            this.startSession();
            this.daemon?.start();
            this.started = performance.now();
        }
        else {
            console.debug(`Stopping ${this.id}...`);
            await this.stopSession();
            await this.daemon?.term();
        }
    }
    async stopSession() {
        if (!this.session)
            return;
        this.session.abort.abort();
        await this.session.done;
        this.session = null;
        this.resetReady();
    }
    resetReady() {
        this.resolvedReady = false;
        this.readyPromise = new Promise((resolve) => (this.resolveReady = () => {
            resolve();
            this.resolvedReady = true;
        }));
    }
    startSession() {
        this.session?.abort.abort();
        const abort = new AbortController();
        this.daemon?.onExit((success) => {
            if (abort.signal.aborted)
                return;
            if (success && this.ready === 'EXIT_SUCCESS') {
                this.setHealth({ result: 'success', message: null });
            }
            else if (!success) {
                this.setHealth({
                    result: 'failure',
                    message: `${this.id} daemon crashed`,
                });
            }
            else if (!this.daemon?.isOneshot()) {
                this.setHealth({
                    result: 'failure',
                    message: `${this.id} daemon exited`,
                });
            }
        });
        const done = this.ready === 'EXIT_SUCCESS'
            ? Promise.resolve()
            : this.runHealthCheckLoop(abort.signal);
        this.session = { abort, done };
    }
    async runHealthCheckLoop(signal) {
        if (this.ready === 'EXIT_SUCCESS')
            return;
        const trigger = (this.ready.trigger ?? defaultTrigger_1.defaultTrigger)(() => ({
            lastResult: this._health.result,
        }));
        const aborted = new Promise((resolve) => signal.addEventListener('abort', () => resolve({ done: true }), {
            once: true,
        }));
        try {
            for (let res = await Promise.race([aborted, trigger.next()]); !res.done; res = await Promise.race([aborted, trigger.next()])) {
                const response = await Promise.resolve(this.ready.fn()).catch((err) => {
                    return {
                        result: 'failure',
                        message: 'message' in err ? err.message : String(err),
                    };
                });
                if (signal.aborted)
                    break;
                await this.setHealth(response);
            }
        }
        catch (err) {
            if (!signal.aborted) {
                console.error(`Daemon ${this.id} health check failed: ${err}`);
            }
        }
    }
    onReady() {
        return this.readyPromise;
    }
    get isReady() {
        return this.resolvedReady;
    }
    async setHealth(health) {
        const changed = this._health.result !== health.result;
        this._health = health;
        if (this.resolveReady && health.result === 'success') {
            this.resolveReady();
        }
        if (changed)
            this.healthWatchers.forEach((watcher) => watcher());
        if (this.ready === 'EXIT_SUCCESS')
            return;
        const display = this.ready.display;
        if (!display) {
            return;
        }
        let result = health.result;
        if (result === 'failure' &&
            this.started &&
            performance.now() - this.started <= (this.ready.gracePeriod ?? 10_000))
            result = 'starting';
        if (result === 'failure') {
            console.error(`Health Check ${this.id} failed:`, health.message);
        }
        await this.effects.setHealth({
            ...health,
            id: this.id,
            name: display,
            result,
        });
    }
    async updateStatus() {
        const healths = this.dependencies.map((d) => ({
            health: d.running && d._health,
            id: d.id,
            display: typeof d.ready === 'object' ? d.ready.display : null,
        }));
        const waitingOn = healths.filter((h) => !h.health || h.health.result !== 'success');
        if (waitingOn.length)
            console.debug(`daemon ${this.id} waiting on ${waitingOn.map((w) => w.id)}`);
        if (waitingOn.length) {
            const waitingOnNames = waitingOn.flatMap((w) => w.display ? [w.display] : []);
            const message = waitingOnNames.length ? waitingOnNames.join(', ') : null;
            await this.setHealth({ result: 'waiting', message });
        }
        else {
            await this.setHealth({ result: 'starting', message: null });
        }
        await this.changeRunning(!waitingOn.length);
    }
}
exports.HealthDaemon = HealthDaemon;
//# sourceMappingURL=HealthDaemon.js.map