"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