// createShutdown — the membrane's stop sequence, as a testable unit.
//
// ⛔ THE INCIDENT (0.9.50 OTA, 2026-10-12). release-ready logged "exiting 75"; the process then took
// ~50 s to exit while pds.7rnx.net answered 503, and it exited 0. Two defects:
// 1. shutdown() called process.exit(0) itself, so release-ready's `.then(exit 75)` never ran. The
// supervisor happened to respawn on 0 too, which is the only reason the OTA landed at all.
// 2. Nothing bounded or timed the steps, so a slow one stretched the outage and left no record of
// which it was. (Not server.close: test/bare/shutdown.test.js measures that an in-flight GET does
// not hold it.)
// So: the caller names the exit code, every step is timed, and a deadline exits anyway and names the
// step it was stuck on. The next slow OTA then says where the time went.
export const DEFAULT_SHUTDOWN_DEADLINE_MS = 15000
/**
* @param {object} opts
* @param {Array<[string, Function]>} opts.steps run in order; a throw is logged and skipped past
* @param {Function} opts.exit called exactly once, with the first caller's code
* @param {number} [opts.deadlineMs] overall budget; past it, exit is called with the steps unfinished
* @param {Function} [opts.log] receives one object per line
* @returns {(code: number) => Promise<void>}
*/
export function createShutdown ({ steps, exit, deadlineMs = DEFAULT_SHUTDOWN_DEADLINE_MS, log = defaultLog }) {
let started = null
return function shutdown (code = 0) {
// A second caller (SIGTERM arriving mid-OTA) joins the first: the steps run once, its code wins.
if (started) return started
started = run(code)
return started
}
async function run (code) {
const t0 = Date.now()
let current = null
let exited = false
const finish = () => { if (exited) return; exited = true; clearTimeout(timer); exit(code) }
// NOT unref'd: an unref'd deadline is skipped exactly when nothing else holds the loop open.
const timer = setTimeout(() => {
log({ msg: 'shutdown: deadline reached, exiting with steps unfinished', stuck: current, deadlineMs, code })
finish()
}, deadlineMs)
for (const [name, fn] of steps) {
if (exited) return
current = name
const s = Date.now()
let error = null
try { await fn() } catch (err) { error = err && err.message ? err.message : String(err) }
const line = { msg: 'shutdown: step', step: name, ms: Date.now() - s }
if (error) line.error = error
log(line)
}
current = null
log({ msg: 'shutdown: done', ms: Date.now() - t0, code })
finish()
}
}
function defaultLog (o) { console.log(JSON.stringify(o)) }
export default createShutdown