/** * DaemonMode server management. * * DaemonManager handles the lifecycle of a persistent Julia process that serves * code-execution requests via DaemonMode.jl, eliminating TTFX on every call. * * ensureDaemonInstalled checks whether DaemonMode.jl is available in the global * Julia environment and installs it if not. */ import { spawn, type ChildProcess } from "node:child_process"; import * as net from "node:net"; import { projectPort } from "./utils.ts"; // ─── Types ──────────────────────────────────────────────────────────────────── interface DaemonState { process: ChildProcess; projectPath: string | undefined; // undefined = Julia global environment port: number; } // ─── DaemonManager ──────────────────────────────────────────────────────────── export class DaemonManager { private _state: DaemonState | undefined; get isRunning(): boolean { return !!this._state; } get port(): number | undefined { return this._state?.port; } get projectPath(): string | undefined { return this._state?.projectPath; } stop(): void { if (this._state) { try { this._state.process.kill("SIGTERM"); } catch {} this._state = undefined; } } launch(projectPath: string | undefined): void { this.stop(); const port = projectPort(projectPath ?? "@global"); const initCode = [ "try; using Revise; catch; end", "using DaemonMode", `serve(${port})`, ].join("; "); const args = ["--startup-file=no"]; if (projectPath) args.push(`--project=${projectPath}`); args.push("-e", initCode); const proc = spawn("julia", args, { detached: false, stdio: "ignore" }); proc.unref(); this._state = { process: proc, projectPath, port }; } waitUntilReady(timeoutMs = 120_000): Promise { if (!this._state) return Promise.resolve(false); const port = this._state.port; return new Promise((resolve) => { const deadline = Date.now() + timeoutMs; function attempt() { if (Date.now() > deadline) { resolve(false); return; } const socket = new net.Socket(); socket.setTimeout(1_000); socket.on("connect", () => { socket.destroy(); resolve(true); }); socket.on("error", () => { socket.destroy(); setTimeout(attempt, 2_000); }); socket.on("timeout", () => { socket.destroy(); setTimeout(attempt, 2_000); }); socket.connect(port, "127.0.0.1"); } attempt(); }); } } // ─── Installation ───────────────────────────────────────────────────────────── type ExecFn = ( cmd: string, args: string[], opts: { timeout: number } ) => Promise<{ stdout: string; code: number }>; export async function ensureDaemonInstalled(execFn: ExecFn): Promise { const probe = await execFn( "julia", ["--startup-file=no", "-e", 'using DaemonMode; println("ok")'], { timeout: 30_000 } ); if (probe.stdout.trim() === "ok") return true; const install = await execFn( "julia", ["--startup-file=no", "-e", 'import Pkg; Pkg.add("DaemonMode")'], { timeout: 120_000 } ); return install.code === 0; }