closeout
API reference

closeout/plugin

Every export of closeout/plugin, with its signature and doc comment: validate, register, reset, registered, contributions, attach and 3 more, plus 5 types.

The plugin host for closeout's half of the contract (plugin-contract R1, R5a, R6, R8).

A plugin is one plain object shared by the whole family. This file keeps the key closeout understands — handlers, cleanup with a declared phase — and ignores every other key without complaining, which is what makes the same object work on any subset of the family that is installed. A plugin written for flagstaff registers here and contributes nothing; its spinners and components are not closeout's business and are not an error.

Nothing here imports another layer, and the plugin shape is declared rather than imported (R3). It imports ./registry.js because that is this package's own module — the phase vocabulary has to have exactly one home or phase: 'restore' means one thing in the validator and another in the runner.

What phase buys, and why it is the whole point

The load-bearing sentence in R5a is "so a plugin's cleanup runs before terminal restore and never after it". A handler that runs after the cursor is back and raw mode is off cannot clean up what it was registered to clean up — it writes its "releasing lock…" line into a terminal that has already been handed back, and whatever it was guarding is released after the user got their prompt.

Registration order cannot give that guarantee to a plugin, because a plugin's registration moment is decided by whoever imported it, and the restore's registration moment is decided by whichever renderer hid the cursor first. Both are import order wearing a lanyard. So the order is data: a handler declares a phase, {@link PHASES} declares the sequence, and the runner reads the sequence rather than the arrival times.

R7, honestly

plugin-contract R7 says no key may require a function except a component's frame and burgee's hooks. handlers requires one: an exit handler is behaviour, and there is no data encoding of "close this socket". What R5a asks for, and what this delivers, is that the ordering is data — inspectable, diffable, and printable by a plugin check without running anything. R7's exemption list owes handlers.run an entry; that edit belongs to the lane that owns plugin-contract, and is recorded in this package's spec.md rather than left in a commit message.

import { validate, register, reset, … } from 'closeout/plugin';

Functions

attach

Hand every contributed handler to a closeout's registry, each in its own phase.

Returns the function that takes them all back off, so a test — or a program that reloads its plugins — can undo a registration without rebuilding the registry.

Registering does not run. A plugin contributing cleanup must not decide when shutdown happens; attach() wires, and the process (or the caller) triggers.

function attach(host: HandlerHost): () => void;
ParameterType
hostHandlerHost

Returns () => void

contributions

Every handler every registered plugin contributed, in the order they will run.

This is the static projection of the ordering, and the reason phase is worth having: a caller — or burgee plugin check — reads the shutdown sequence without triggering one.

function contributions(): Contribution[];

Returns Contribution[]

register

Register a plugin. Later wins, like ESLint flat config — but "wins" is a weaker word here than it is for a colour token: handlers do not shadow each other, they accumulate. Order is kept because two handlers in the same phase run in it, and because a plugin check wants to print the list a reader will see.

function register(plugin: unknown): void;
ParameterType
pluginunknown

Returns void

registered

The plugins registered, in registration order.

function registered(): readonly Plugin[];

Returns readonly Plugin[]

reset

Forget every registered plugin. For tests, and for a program that re-plugs at runtime.

function reset(): void;

Returns void

validate

Refuse a plugin that cannot contribute a handler, at the door.

Every refusal here is a refusal rather than a silent drop, for the reason roundel's host gives about a misspelt token: a handler that is quietly ignored looks like it worked, and its author debugs the wrong thing — except that here the thing they are debugging is a lock that was never released.

function validate(plugin: unknown): asserts plugin is Plugin;
ParameterType
pluginunknown

Returns asserts plugin is Plugin

Classes

PluginError

A refused plugin says what is wrong and what to do about it — the family's one vocabulary.

class PluginError extends Error {
    readonly code: PluginErrorCode;
    readonly fix: string;
    constructor(code: PluginErrorCode, message: string, fix: string);
}

Constants

CONTRACT

The plugin contract version. One number for the family — the same 1 flagstaff declares, written out rather than imported for the reason in the file comment above.

const CONTRACT = 1;

PLUGIN_PHASES

The phases a plugin may put a handler in.

restore is deliberately not one of them. It is closeout's own phase, it runs last, and it is where the cursor and raw mode go; a plugin handler admitted to it could land after the terminal was handed back, depending on nothing more than which of the two registered first — which is precisely the coincidence phases exist to replace. R5a says "never after it", and a rule enforced at the door is the only version of "never" a reader can rely on.

const PLUGIN_PHASES: readonly ["flush", "release"];

Interfaces

Contribution

One contributed handler, with the plugin it came from and the phase it resolved to.

interface Contribution {
    /** `"<plugin>:<handler>"` — the name a report prints. */
    id: string;
    phase: Phase;
    from: string;
    run: ExitHandler;
}

HandlerHost

The half of a Registry this needs — declared structurally so a caller can attach to something it built itself, and so this file does not pull the process wiring in.

interface HandlerHost {
    add(handler: ExitHandler, spec?: HandlerSpec): () => void;
}

Plugin

The keys closeout reads. Declared structurally: any object with these fields is a plugin here, whatever else it carries.

interface Plugin {
    name: string;
    contract?: number;
    handlers?: readonly PluginHandler[];
}

PluginHandler

One contributed handler: a name to report it by, a phase to order it by, and the work.

interface PluginHandler {
    /** How the handler is named in a report — including the one the deadline prints. */
    name: string;
    /** Defaults to `release`. Never `restore`; see {@link PLUGIN_PHASES}. */
    phase?: (typeof PLUGIN_PHASES)[number];
    /** The cleanup itself. May be async; the phase is awaited before the next one begins. */
    run: ExitHandler;
}

Types

PluginErrorCode

type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT' | 'E_NO_CONTRIBUTION';

On this page