# closeout

> Every export of closeout, with its signature and doc comment: alternateScreen, assertDeadline, createRegistry, DEADLINE_ERROR_CODE, DeadlineError, DEFAULT_DEADLINE and 17 more, plus 16 types.

Source: https://closeout.interlace.tools/docs/api

<!-- Generated by scripts/api-reference.ts from the built dist/*.d.ts. Do not edit; run `npx tsx scripts/api-reference.ts`. -->

closeout — close everything out.

Exit handlers that run **exactly once on every path**, terminal restore, and a bounded
deadline so shutdown cannot hang.

"Every path" is the hard part and the reason this is a package. A program leaves by
several doors — returning from main, `process.exit`, SIGINT, SIGTERM, SIGHUP — and a
handler registered on `'exit'` alone misses most of them, which is why a Ctrl-C so often
leaves a hidden cursor or a half-written file behind. Registering on all of them is
easy; registering on all of them and running the handlers exactly once when two fire
together is where the bugs live.

Zero dependencies; Node builtins only.

```ts
import { alternateScreen, assertDeadline, createRegistry, … } from 'closeout';
```

## Functions

### alternateScreen

Enter the alternate screen and register leaving it in `restore`; the returned function
leaves it. Registers on the process-wide instance unless `closeout` names another.

```ts
function alternateScreen(stream: OutputStream, closeout?: Closeout): () => void;
```

| Parameter | Type |
| :-- | :-- |
| `stream` | `OutputStream` |
| `closeout` (optional) | `Closeout` |

**Returns** `() => void`

### assertDeadline

Check a deadline at the moment it is registered, not at the moment it would have fired.

**`Infinity` and `0` are both rejected, and they are the same mistake wearing two hats.**
`Infinity` is "wait forever", which is the unbounded shutdown this package exists to
remove. `0` reads like "do not wait", and what it actually buys is a shutdown where no
asynchronous handler ever gets a turn — every one of them abandoned mid-flight, which is
the *other* half of the same failure: work that silently did not happen. A caller who
genuinely wants either wants a different package, and being told so at `install()` is
worth far more than finding out during the one shutdown that mattered.

Returns the number so a caller can use it in an expression, which is what makes it hard
to call and then ignore.

```ts
function assertDeadline(ms: number): number;
```

| Parameter | Type |
| :-- | :-- |
| `ms` | `number` |

**Returns** `number`

### createRegistry

```ts
function createRegistry(options?: RegistryOptions): Registry;
```

| Parameter | Type |
| :-- | :-- |
| `options` (optional) | `RegistryOptions` |

**Returns** `Registry`

### hideCursor

Hide the cursor and register its restore; the returned function shows it again.

```ts
function hideCursor(stream: OutputStream): () => void;
```

| Parameter | Type |
| :-- | :-- |
| `stream` | `OutputStream` |

**Returns** `() => void`

### install

Wire a registry to a process.

Exposed rather than kept internal because a test — or a program that owns its own
lifecycle, like a runner hosting other programs — needs to install this against
something that is not the global process.

```ts
function install(options?: InstallOptions): Closeout;
```

| Parameter | Type |
| :-- | :-- |
| `options` (optional) | `InstallOptions` |

**Returns** `Closeout`

### onExit

Register a handler that runs exactly once, on every path out of the program.

```ts
function onExit(handler: ExitHandler, spec?: HandlerSpec): () => void;
```

| Parameter | Type |
| :-- | :-- |
| `handler` | `ExitHandler` |
| `spec` (optional) | `HandlerSpec` |

**Returns** `() => void`

### rawMode

Turn raw mode on and register turning it off in `restore`; the returned function turns it
off. An input that was already raw belongs to somebody else and is left alone, now and at
exit. Registers on the process-wide instance unless `closeout` names another.

```ts
function rawMode(input: InputStream, closeout?: Closeout): () => void;
```

| Parameter | Type |
| :-- | :-- |
| `input` | `InputStream` |
| `closeout` (optional) | `Closeout` |

**Returns** `() => void`

### reportToEvent

The agent rendering. Same values, one key added, still a projection of the one record.

```ts
function reportToEvent(report: ShutdownReport): ExitEvent;
```

| Parameter | Type |
| :-- | :-- |
| `report` | `ShutdownReport` |

**Returns** `ExitEvent`

### reportToJson

The `--json` rendering: one line, machine-first, `error` flattened to text.

`JSON.stringify` of the record itself would emit `"error": {}` for every `Error` — they
have no enumerable own properties — which is a line that looks like a report and carries
nothing. That is why this is a projection rather than a stringify.

```ts
function reportToJson(report: ShutdownReport): string;
```

| Parameter | Type |
| :-- | :-- |
| `report` | `ShutdownReport` |

**Returns** `string`

### showCursor

Show the cursor. Idempotent, and a no-op on a non-TTY.

```ts
function showCursor(stream: OutputStream): void;
```

| Parameter | Type |
| :-- | :-- |
| `stream` | `OutputStream` |

**Returns** `void`

### timeoutMessage

The line the deadline prints when it fires.

Kept here beside the projections rather than in the registry because it *is* a rendering
of the record, and because the sentence is the product: a hang that used to be silence
becomes a named handler a reader can go and look at.

```ts
function timeoutMessage(report: ShutdownReport, deadline: number): string;
```

| Parameter | Type |
| :-- | :-- |
| `report` | `ShutdownReport` |
| `deadline` | `number` |

**Returns** `string`

## Classes

### DeadlineError

```ts
class DeadlineError extends TypeError {
    readonly code = "USAGE";
    /** What to do instead. Always present, always actionable — contract R8's shape. */
    readonly fix: string;
    constructor(message: string, fix: string);
}
```

## Constants

### DEADLINE_ERROR_CODE

A caller error, in the family's one vocabulary: a `code` that classifies it and a `fix`
that says what to do instead.

`USAGE` rather than an `E_…` code: the `E_…` names belong to the plugin contract, where a
code travels between a plugin author and a host. This is a caller passing a number that
cannot mean what they wanted it to mean, which is the same class caique's binding reports
and the same class a CLI exits 2 for.

```ts
const DEADLINE_ERROR_CODE = "USAGE";
```

### DEFAULT_DEADLINE

Two seconds: long enough to flush a file, short enough that nobody reaches for the
keyboard.

**Provisional, and the measurement that was supposed to settle it is recorded rather than
quietly re-run until it agreed.** `measure/deadline.mjs` runs the five cleanup shapes this
layer actually sees — flush a write stream, close a listening server, kill a child, remove
a temp directory of 100 files, restore the terminal — 100x each and prints each p99. Run
2026-09-14 on darwin arm64, node 24.13:

    flush a write stream   p99     67.1 ms
    close a server         p99      1.5 ms
    kill a child           p99      1.3 ms
    remove a temp dir      p99 17 818.5 ms
    restore the terminal   p99      0.2 ms

Four of the five are inside 70 ms and the fifth is four orders of magnitude out, which is
not a fact about removing a directory: the machine was at **load average 19-22 across 14
cores**, the same condition that makes `exit-hook`'s four signal cases a race. Re-run
alone, the removal is p50 161 ms / p99 2 166 ms — still the shape that decides the answer,
and still measured through the load.

So the default stays 2 000 ms and stays *stated as provisional*, because rounding a p99
that has another process's disk queue inside it would be a number with a decimal point
and no meaning. The honest reading of the run is that everything except filesystem
removal finishes inside 70 ms, and that the deadline is there for the shape that does not.
`closeout/intent.md`'s open question is not closed; what changed is that it now has a
repeatable instrument and one recorded run instead of an argument.

```ts
const DEFAULT_DEADLINE = 2000;
```

### DEFAULT_PHASE

Where a handler goes when its author did not say.

`release` rather than `flush`: an unphased handler is far more often closing something
than writing something, and either way it lands before `restore` — the property every
caller of this package is relying on whether or not they know the word.

```ts
const DEFAULT_PHASE: Phase;
```

### EXIT_PATHS

Every path, in the order `install()` wires them. Exported so a matrix can iterate it.

```ts
const EXIT_PATHS: readonly ["exit", "beforeExit", "signal", "uncaught", "rejection"];
```

### PHASES

The order shutdown happens in, as **data** rather than as registration order.

Registration order is the wrong ordering for a shutdown, and it is the ordering every
incumbent gives you. The handler that hands the terminal back is registered by whichever
renderer hid the cursor, at whatever moment it first drew — so anything registered a line
later runs *after* the cursor is back and raw mode is off, which is to say it cleans up
nothing it was registered to clean up. An ordering that depends on import order is not an
ordering; it is a coincidence that happens to hold until someone moves an import.

Three phases, and the names are the sequence:

- `flush` — get the data out: write the file, drain the log, post the last event.
- `release` — let go: locks, sockets, children, temp directories. The default.
- `restore` — hand the terminal back: cursor shown, raw mode off. Last, always.

Phases run **in sequence** — an async handler in `flush` is awaited before `release`
begins — and handlers within one phase run together, in registration order. The
sequencing is the half that makes the guarantee worth stating: sorting the calls but
starting them all at once would put a plugin's `await` after the cursor was already back,
which is the bug with the ordering merely rearranged.

```ts
const PHASES: readonly ["flush", "release", "restore"];
```

### SIGNALS

The signals a CLI is expected to survive politely (design R1).

SIGINT is Ctrl-C. SIGTERM is what an orchestrator sends before it loses patience. SIGHUP
is the terminal closing out from under you, which is the one people forget and the one
that most often strands a lock file. SIGQUIT is Ctrl-\\, which a shell sends expecting a
core dump and which otherwise leaves every lock this package exists to release.

SIGBREAK is Windows' Ctrl-Break and exists nowhere else; listening for it on POSIX costs
one listener that can never fire, which is a better trade than a `platform` read inside
the one package whose whole design is that it does not read the process.

**SIGKILL is deliberately absent, and cannot be added.** It is not deliverable to a
listener by design; a package that claimed it would be claiming something no program can
do. The README says so in those words rather than leaving a reader to infer a guarantee.

```ts
const SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT", "SIGBREAK"];
```

## Interfaces

### Closeout

```ts
interface Closeout {
    /**
     * Register a handler. Returns the function that unregisters it.
     *
     * The second argument is a phase (default `release`), or `{ phase, label }` when the
     * handler wants a name in the deadline's report that is better than an arrow's empty one.
     */
    onExit(handler: ExitHandler, spec?: HandlerSpec): () => void;
    /** Hide the cursor and register its restore; the returned function shows it again. */
    hideCursor(stream: OutputStream): () => void;
    /** Show the cursor now. Idempotent, and a no-op on a non-TTY. */
    showCursor(stream: OutputStream): void;
    /** The registry, for a caller that wants to drive shutdown itself. */
    readonly registry: Registry;
}
```

### ExitEvent

The shape an agent event carries: the same values, under the family's event key (Y6).

```ts
interface ExitEvent {
    type: 'closeout.shutdown';
    path: ExitPath;
    signal: string | null;
    code: number | null;
    error: string | null;
    timedOut: boolean;
    unfinished: string[];
}
```

### ExitInfo

What a trigger supplies. The path is inferred from the signal when it is not stated.

```ts
interface ExitInfo {
    code: number | null;
    signal: string | null;
    path?: ExitPath;
    error?: unknown;
}
```

### ExitReport

What a handler is handed. The same record `--json` and an agent event project from.

```ts
interface ExitReport {
    /** Which door: a normal exit, an emptied loop, a signal, a throw, a rejected promise. */
    path: ExitPath;
    /** The signal that ended it, when one did. */
    signal: string | null;
    /** The code the process is leaving with, when it is leaving by a code. */
    code: number | null;
    /** What was thrown or rejected, on the two paths that have one. `null` on the others. */
    error: unknown;
}
```

### HandlerOptions

How a handler is registered when a bare phase is not enough to say it.

```ts
interface HandlerOptions {
    /** Which phase it runs in. Defaults to {@link DEFAULT_PHASE}. */
    phase?: Phase;
    /**
     * What to call it in a report.
     *
     * Defaults to the function's own `name`, which is right for `onExit(releaseTheLock)` and
     * useless for `onExit(async () => …)` — and the anonymous arrow is the shape that hangs.
     * A label is the caller's chance to make the deadline's sentence name something they can
     * go and look at.
     */
    label?: string;
}
```

### InstallOptions

```ts
interface InstallOptions extends RegistryOptions {
    /** Defaults to the real `process`. */
    process?: ProcessLike;
}
```

### ProcessLike

The half of `process` this package needs, so the wiring can be tested without one.

```ts
interface ProcessLike {
    on(event: string, listener: (...args: never[]) => void): unknown;
    removeListener(event: string, listener: (...args: never[]) => void): unknown;
    listenerCount(event: string): number;
    exit(code?: number): never;
    /**
     * Send a signal to a process. closeout passes exactly one pid — {@link ProcessLike.pid},
     * its own — because the only thing it ever does with this is re-raise the signal it just
     * handled, so that a process killed by SIGINT *dies of SIGINT* instead of reporting 130.
     *
     * Required rather than optional, which is a deliberate cost. A `ProcessLike` without it
     * would still compile and would silently take the exit path, and a seam that lets a fake
     * quietly opt out of the behaviour under test is how `leaveAfter` stayed wrong through
     * 21 / 21 and 6 / 6. A process that cannot raise a signal cannot honour this package's
     * contract, so the type says so.
     */
    kill(pid: number, signal: string): unknown;
    /** This process's own id — the only one {@link ProcessLike.kill} is ever given. */
    pid: number;
    stderr: OutputStream;
    /**
     * Optional because the signal wiring has never needed it and its tests do not supply one:
     * only the terminal-restore façade picks between the two streams.
     */
    stdout?: OutputStream;
    /**
     * The code the process will leave with, which is a property of the process and not of any
     * handler. `exit-hook`'s contract is written in terms of it — a hook is handed the code the
     * program is about to exit with, and a signal overrules whatever was set — so the drop-in
     * path reads and writes it here. Typed as Node types it (`string` is legal and coerced).
     */
    exitCode?: number | string | undefined;
}
```

### Registry

```ts
interface Registry {
    /**
     * Register a handler in a phase. Returns the function that unregisters it.
     *
     * The phase defaults to {@link DEFAULT_PHASE}, so a caller that never heard of phases
     * keeps the behaviour it had: unphased handlers share one phase and run in registration
     * order within it — ahead of `restore`, which is the part that was not true before.
     */
    add(handler: ExitHandler, spec?: HandlerSpec): () => void;
    /**
     * Run every handler, once, phase by phase, inside the deadline. A later call runs nothing
     * and resolves with the first: it waits on a shutdown still in flight.
     */
    run(info: ExitInfo): Promise<ShutdownReport>;
    /** Run every handler synchronously, in phase order; a returned promise is abandoned. */
    runSync(info: ExitInfo): ShutdownReport;
    /** How many handlers are registered — for a caller that wants to know if any are. */
    readonly size: number;
    /** How many are registered in one phase. */
    count(phase: Phase): number;
    /** Whether shutdown has already happened. */
    readonly settled: boolean;
    /**
     * The report of the shutdown that happened, or `undefined` before one has.
     *
     * Readable after `runSync`, which cannot return a promise, and after a second trigger
     * that found the work already done — the path that would otherwise have nothing to say.
     */
    readonly report: ShutdownReport | undefined;
}
```

### RegistryOptions

```ts
interface RegistryOptions {
    /**
     * How long shutdown may take before it stops waiting, in milliseconds.
     *
     * A deadline is not a nicety. A handler that awaits something that never resolves — a
     * socket that will not close, a lock nobody releases — turns Ctrl-C into a process the
     * user has to kill twice, and the second one is SIGKILL with no cleanup at all. Better
     * to abandon a slow handler than to strand the person at the keyboard.
     *
     * `Infinity` and `0` are rejected here, at registration — see `deadline.ts`.
     */
    deadline?: number;
    /** Where a handler's own failure is reported. Defaults to stderr. */
    onError?: (error: unknown) => void;
    /**
     * Where a breached deadline is reported. Defaults to stderr, naming every handler that
     * had not returned. Takes the whole report, so a caller can project it as JSON or as an
     * agent event instead of a line of prose.
     */
    onTimeout?: (report: ShutdownReport) => void;
}
```

### ShutdownReport

What shutdown *was*, once it has run: the record plus what the deadline found.

`unfinished` is the sentence this package exists to be able to say. It is empty on every
ordinary shutdown, and on a breach it names each handler that had not returned — by the
label its caller gave it, or by the function's own `name`.

```ts
interface ShutdownReport extends ExitReport {
    /** Whether the deadline expired before the handlers were done. */
    timedOut: boolean;
    /**
     * The handlers that had not returned when shutdown stopped waiting — by the label their
     * caller gave them, or by the function's own `name`.
     *
     * Empty on an ordinary shutdown. Non-empty in two cases, and they are different: the
     * deadline fired (`timedOut`), or the trigger was `'exit'`, where Node gives no time at
     * all and an asynchronous handler never had a turn to lose. The second is why this is
     * not simply "what the deadline caught".
     */
    unfinished: readonly string[];
}
```

## Types

### ExitHandler

A handler is handed the one record: `{ path, signal, code, error }` (design R2).

```ts
type ExitHandler = (report: ExitReport) => void | Promise<void>;
```

### ExitPath

Which door the process left by.

`signal` covers all of them rather than naming each: the signal's own name is in
`signal`, and a consumer switching on the path wants the *class* of exit — "somebody
asked us to stop" is one case whether it arrived as SIGINT or SIGHUP.

```ts
type ExitPath = 'exit' | 'beforeExit' | 'signal' | 'uncaught' | 'rejection';
```

### HandlerSpec

A phase, or the options object. The bare phase is the common case and stays spellable.

```ts
type HandlerSpec = Phase | HandlerOptions;
```

### Phase

One of {@link PHASES}.

```ts
type Phase = (typeof PHASES)[number];
```

## Re-exported

Documented on the page of the entry point that declares them.

| Export | Kind | Documented in |
| :-- | :-- | :-- |
| `once` | function | [`closeout/once`](/docs/api/once#once) |
| `ENTER_ALTERNATE_SCREEN` | const | [`closeout/cursor`](/docs/api/cursor#enter_alternate_screen) |
| `HIDE_CURSOR` | const | [`closeout/cursor`](/docs/api/cursor#hide_cursor) |
| `LEAVE_ALTERNATE_SCREEN` | const | [`closeout/cursor`](/docs/api/cursor#leave_alternate_screen) |
| `SHOW_CURSOR` | const | [`closeout/cursor`](/docs/api/cursor#show_cursor) |
| `InputStream` | interface | [`closeout/cursor`](/docs/api/cursor#inputstream) |
| `OutputStream` | interface | [`closeout/cursor`](/docs/api/cursor#outputstream) |
