import { HealthReceipt, ReadyProof, TriggerInput } from "../health" import { CheckResult } from "../health/checkFns" import { Trigger } from "../health/trigger" import { defaultTrigger } from "../health/trigger/defaultTrigger" import { DaemonReturned, Effects, ValidIfNoStupidEscape } from "../types" import { InterfaceReceipt } from "./interfaceReceipt" type Daemon = { id: "" extends Id ? never : Id command: ValidIfNoStupidEscape | [string, ...string[]] env?: Record ready: { display: string | null fn: () => Promise | CheckResult trigger?: Trigger } requires: Exclude[] } type ErrorDuplicateId = `The id '${Id}' is already used` /** * Used during the main of a function, it allows us to describe and ensure a set of daemons are running. * With the dependency, we are using this like an init system, where we can ensure that a daemon is running * The return type of this is used in the runningMain ```ts Daemons.with({ effects, started, interfacesReceipt, }) .addDaemon({ id: "nostr", command: "./nostr-rs-relay --db /data", ready: { display: { name: "Websocket Live", message: "The websocket is live", }, fn: () => checkPortListening(effects, 8080), }, }) .build() ``` */ export class Daemons { private constructor( readonly effects: Effects, readonly started: (onTerm: () => void) => null, readonly daemons?: Daemon[], ) {} static of(config: { effects: Effects started: (onTerm: () => void) => null interfaceReceipt: InterfaceReceipt healthReceipts: HealthReceipt[] }) { return new Daemons(config.effects, config.started) } addDaemon( // prettier-ignore id: "" extends Id ? never : ErrorDuplicateId extends Id ? never : Id extends Ids ? ErrorDuplicateId : Id, newDaemon: Omit, "id">, ) { const daemons = ((this?.daemons ?? []) as any[]).concat({ ...newDaemon, id, }) return new Daemons(this.effects, this.started, daemons) } async build() { const daemonsStarted = {} as Record> const { effects } = this const daemons = this.daemons ?? [] for (const daemon of daemons) { const requiredPromise = Promise.all( daemon.requires?.map((id) => daemonsStarted[id]) ?? [], ) daemonsStarted[daemon.id] = requiredPromise.then(async () => { const { command } = daemon const child = effects.runDaemon(command, { env: daemon.env }) let currentInput: TriggerInput = {} const getCurrentInput = () => currentInput const trigger = (daemon.ready.trigger ?? defaultTrigger)( getCurrentInput, ) return new Promise(async (resolve) => { for ( let res = await trigger.next(); !res.done; res = await trigger.next() ) { const response = await Promise.resolve(daemon.ready.fn()).catch( (err) => ({ status: "failing", message: "message" in err ? err.message : String(err), } as CheckResult), ) currentInput.lastResult = response.status || null if (!currentInput.hadSuccess && response.status === "passing") { currentInput.hadSuccess = true resolve(child) } } resolve(child) }) }) } return { async term() { await Promise.all( Object.values>(daemonsStarted).map((x) => x.then((x) => x.term()), ), ) }, async wait() { await Promise.all( Object.values>(daemonsStarted).map((x) => x.then((x) => x.wait()), ), ) }, } } }