import { resolver } from "@rocicorp/resolver";
import { pid } from "node:process";
import { singleProcessMode } from "../types/processes.js";
import { RunningState } from "./running-state.js";
const GRACEFUL_SHUTDOWN = ["SIGTERM", "SIGINT"];
const FORCEFUL_SHUTDOWN = ["SIGQUIT"];
class ProcessManager {
  #lc;
  #userFacing = /* @__PURE__ */ new Set();
  #all = /* @__PURE__ */ new Set();
  #exitImpl;
  #start = Date.now();
  #ready = [];
  #runningState = new RunningState("process-manager");
  #drainStart = 0;
  constructor(lc, proc) {
    this.#lc = lc.withContext("component", "process-manager");
    for (const signal of GRACEFUL_SHUTDOWN) {
      proc.on(signal, () => this.#startDrain(signal));
    }
    proc.on(
      "exit",
      (code) => this.#kill(
        this.#all,
        code === 0 ? GRACEFUL_SHUTDOWN[0] : FORCEFUL_SHUTDOWN[0]
      )
    );
    for (const signal of FORCEFUL_SHUTDOWN) {
      proc.on(signal, () => this.#exit(-1));
    }
    this.#exitImpl = (code) => {
      if (singleProcessMode()) {
        return proc.emit("exit", code);
      }
      process.exit(code);
    };
  }
  done() {
    return this.#runningState.stopped();
  }
  #exit(code) {
    this.#lc.info?.("exiting with code", code);
    this.#runningState.stop(this.#lc);
    void this.#lc.flush().finally(() => this.#exitImpl(code));
  }
  #startDrain(signal = "SIGTERM") {
    this.#lc.info?.(`initiating drain (${signal})`);
    this.#drainStart = Date.now();
    if (this.#userFacing.size) {
      this.#kill(this.#userFacing, signal);
    } else {
      this.#kill(this.#all, signal);
    }
  }
  addSubprocess(proc, type, name) {
    if (type === "user-facing") {
      this.#userFacing.add(proc);
    }
    this.#all.add(proc);
    proc.on(
      "error",
      (err) => this.#lc.error?.(`error from ${name} ${proc.pid}`, err)
    );
    proc.on(
      "close",
      (code, signal) => this.#onExit(code, signal, null, type, name, proc)
    );
  }
  #initializing = /* @__PURE__ */ new Map();
  #nextID = 0;
  addWorker(worker, type, name) {
    this.addSubprocess(worker, type, name);
    const id = ++this.#nextID;
    this.#initializing.set(id, name);
    const { promise, resolve } = resolver();
    this.#ready.push(promise);
    worker.onceMessageType("ready", () => {
      this.#lc.debug?.(`${name} ready (${Date.now() - this.#start} ms)`);
      this.#initializing.delete(id);
      resolve();
    });
    return worker;
  }
  initializing() {
    return [...this.#initializing.values()];
  }
  async allWorkersReady() {
    await Promise.all(this.#ready);
  }
  logErrorAndExit(err, name) {
    this.#onExit(-1, null, err, "user-facing", name, void 0);
  }
  #onExit(code, sig, err, type, name, worker) {
    if (worker) {
      this.#userFacing.delete(worker);
      this.#all.delete(worker);
    }
    const pid2 = worker?.pid ?? process.pid;
    if (type === "supporting") {
      const log2 = code === 0 && this.#userFacing.size === 0 ? "info" : "warn";
      this.#lc[log2]?.(`${name} (${pid2}) exited with code (${code})`, err ?? "");
      return this.#exit(log2 === "info" ? code : -1);
    }
    const log = this.#drainStart === 0 ? "error" : "warn";
    if (sig) {
      this.#lc[log]?.(`${name} (${pid2}) killed with (${sig})`, err ?? "");
    } else if (code !== 0) {
      this.#lc[log]?.(`${name} (${pid2}) exited with code (${code})`, err ?? "");
    } else {
      this.#lc.info?.(`${name} (${pid2}) exited with code (${code})`);
    }
    if (this.#userFacing.size === 0) {
      this.#lc.info?.(
        this.#drainStart ? `all user-facing workers drained (${Date.now() - this.#drainStart} ms)` : `all user-facing workers exited`
      );
      return this.#exit(0);
    }
    if (log === "error") {
      return this.#exit(code || -1);
    }
    return void 0;
  }
  #kill(workers, signal) {
    for (const worker of workers) {
      try {
        worker.kill(signal);
      } catch (e) {
        this.#lc.error?.(e);
      }
    }
  }
}
async function runUntilKilled(lc, parent, ...services) {
  if (services.length === 0) {
    return;
  }
  for (const signal of [...GRACEFUL_SHUTDOWN, ...FORCEFUL_SHUTDOWN]) {
    parent.once(signal, () => {
      const GRACEFUL_SIGNALS = GRACEFUL_SHUTDOWN;
      services.forEach(async (svc) => {
        if (GRACEFUL_SIGNALS.includes(signal) && svc.drain) {
          lc.info?.(`draining ${svc.constructor.name} ${svc.id} (${signal})`);
          await svc.drain();
        }
        lc.info?.(`stopping ${svc.constructor.name} ${svc.id} (${signal})`);
        await svc.stop();
      });
    });
  }
  try {
    const svc = await Promise.race(
      services.map((svc2) => svc2.run().then(() => svc2))
    );
    lc.info?.(`${svc.constructor.name} (${svc.id}) stopped`);
  } catch (e) {
    lc.error?.(`exiting on error`, e);
    throw e;
  }
}
async function exitAfter(run) {
  try {
    await run();
    console.info(`pid ${pid} exiting normally`);
    process.exit(0);
  } catch (e) {
    console.error(`pid ${pid} exiting with error`, e);
    process.exit(-1);
  }
}
const DEFAULT_STOP_INTERVAL_MS = 2e4;
class HeartbeatMonitor {
  #stopInterval;
  #lc;
  #checkIntervalTimer;
  #checkImmediateTimer;
  #lastHeartbeat = 0;
  constructor(lc, stopInterval = DEFAULT_STOP_INTERVAL_MS) {
    this.#lc = lc;
    this.#stopInterval = stopInterval;
  }
  onHeartbeat(reqHeaders) {
    this.#lastHeartbeat = Date.now();
    if (this.#checkIntervalTimer === void 0) {
      this.#lc.info?.(
        `starting heartbeat monitor at ${this.#stopInterval / 1e3} second interval`,
        reqHeaders
      );
      this.#checkIntervalTimer = setInterval(
        this.#checkStopInterval,
        this.#stopInterval / 4
      );
    }
  }
  #checkStopInterval = () => {
    this.#checkImmediateTimer = setImmediate(() => {
      this.#checkImmediateTimer = void 0;
      const timeSinceLastHeartbeat = Date.now() - this.#lastHeartbeat;
      if (timeSinceLastHeartbeat >= this.#stopInterval) {
        this.#lc.info?.(
          `last heartbeat received ${timeSinceLastHeartbeat / 1e3} seconds ago. draining.`
        );
        process.kill(process.pid, GRACEFUL_SHUTDOWN[0]);
      }
    });
  };
  stop() {
    clearTimeout(this.#checkIntervalTimer);
    if (this.#checkImmediateTimer) {
      clearImmediate(this.#checkImmediateTimer);
    }
  }
}
export {
  FORCEFUL_SHUTDOWN,
  GRACEFUL_SHUTDOWN,
  HeartbeatMonitor,
  ProcessManager,
  exitAfter,
  runUntilKilled
};
//# sourceMappingURL=life-cycle.js.map
