Coming from signal-exit
A signal-exit alternative with zero dependencies: closeout's onExit runs once on every exit path with a bounded deadline, and reports as --json or an agent event. No signal-exit drop-in yet — its compatibility row is planned, not graded.
closeout is a signal-exit alternative: exit handlers that run exactly once on every path, terminal restore, and a bounded deadline so shutdown cannot hang. It is not yet a signal-exit drop-in, and this page says why.
Migrate from signal-exit: rename the callback's arguments
There is no closeout/signal-exit subpath. Both packages export onExit, and both return
the function that unregisters the handler, but the callback is handed a different shape —
signal-exit passes (code, signal), closeout passes one record:
- import { onExit } from 'signal-exit';
- const off = onExit((code, signal) => {
+ import { onExit } from 'closeout';
+ const off = onExit(({ path, code, signal, error }) => {
releaseTheLock();
});path is 'exit' | 'beforeExit' | 'signal' | 'uncaught' | 'rejection', and error is what
was thrown or rejected on the two paths that have one.
The two neighbours in this layer do have one-import paths, each graded by its incumbent's own suite:
- import exitHook, { asyncExitHook, gracefulExit } from 'exit-hook';
- import restoreCursor from 'restore-cursor';
+ import exitHook, { asyncExitHook, gracefulExit } from 'closeout/exit-hook';
+ import restoreCursor from 'closeout/restore-cursor';Is closeout compatible with signal-exit?
There is no graded signal-exit row. On the Compatibility page,
which npm run compat:page generates from the oracle's last run, signal-exit is listed as
planned: its suite is vendored and runs, but the control does not yet clear its own
reference, and a control that cannot must not publish a rate. Until it is graded, closeout
does not claim signal-exit compatibility. The rows that are graded:
| closeout | control | |
|---|---|---|
closeout/exit-hook | 21 / 21 | 21 / 21 |
closeout/restore-cursor | 6 / 6 | 6 / 6 |
What you gain over signal-exit
- Exactly once, on every door. A normal exit, an emptied event loop, Ctrl-C,
SIGTERM,SIGHUP,SIGQUIT, an uncaught throw and an unhandled rejection all reach the same handler, once, even when two fire at the same moment. - Shutdown cannot hang. A bounded deadline —
DEFAULT_DEADLINEis 2000 ms — ends a shutdown whose handlers do not return, and the result names the ones that had not. - Phases, not registration order. Handlers run in
flush,release,restoreorder, so the cursor comes back after the file is flushed, whichever registered first. - One record, three readers. The record your handler receives is what
reportToJson()andreportToEvent()project, so a--jsonline and an agent event cannot disagree with what the handler was told. - Terminal restore is built in.
hideCursor(stream)registers its own restore.
SIGKILL is not deliverable to a listener, by design, and closeout does not claim it.
When to switch from signal-exit
- Ctrl-C leaves a hidden cursor, a half-written file or a held lock.
- A shutdown handler that never returns has hung your CLI or your CI job.
The API, the phases and the deadline are on closeout.