Skip to content

Kernel events

Reference. The nine events the kernel emits, the sink type that receives them, and the default sink's output format. For where the sink is set, see start and StartOptions; for why the kernel emits events rather than logging, see Design decisions.

KernelEvent

ts
type KernelEvent =
  | { readonly type: "building" }
  | { readonly type: "startFailed"; readonly cause: unknown }
  | { readonly type: "serving"; readonly runtime: string }
  | { readonly type: "draining"; readonly inFlight: number }
  | { readonly type: "drained"; readonly report: DrainReport }
  | { readonly type: "stopping" }
  | { readonly type: "exited" }
  | {
      readonly type: "teardownError";
      readonly port: string;
      readonly cause: unknown;
    }
  | { readonly type: "uncaught"; readonly cause: unknown };
EventFieldsEmitted when
buildingstart is called, before the probe server binds or the graph is built. Always the first event.
startFailedcauseAnything failed before serving: a construction Err (a ConfigInvalid naming its variables), a runtime's RuntimeStartFailed, a probe bind failure, or a defect. cause is the Err's error or the defect's cause. Emitted before stopping, so a process that never came up says why.
servingruntime — the runtime's nameThe runtime answered Ok(serving).
draininginFlightA signal (or requestDrain()) arrived while serving. Emitted in the same synchronous turn readiness flips false, so inFlight equals the report's inFlightAtStart.
drainedreport: DrainReportThe drain finished — by the registry going idle or by the deadline.
stoppingThe phase reached stopping: after the drain, or straight away for stop(), an uncaught exception or a startup failure.
exitedThe phase reached exited. Always the last event.
teardownErrorport, causeA finaliser failed as a scope closed — the application scope's (also recorded in ExitReport.teardownErrors) or a StartOptions.unit module's (recorded nowhere else).
uncaughtcauseAn uncaughtException or unhandledRejection was caught by the kernel's handlers (signals: true). Only the first is reported; the shutdown it triggers may produce more noise, and the report names one cause.

serving, stopping and exited are emitted by the phase tracker as it advances, so they can never be emitted twice or out of order.

EventSink

ts
type EventSink = (event: KernelEvent) => void;

Set through StartOptions.onEvent; default stderrSink. The kernel wraps whatever it is given so that a throwing sink is swallowed — a broken reporter must not take the process down mid-shutdown, and there is nowhere left to report a broken reporter to. Two consequences: a sink that throws loses that one event silently, and a sink that wants to fail loudly cannot.

stderrSink

Writes one JSON line per event to process.stderr, JSON.stringify of the event with two adjustments:

  • An Error anywhere in the value — a cause, or a nested cause of one — is normalised to { name, message, stack, cause }. JSON.stringify skips non-enumerable properties, and an Error's message and stack are both non-enumerable, so a bare Error would render the two cause-carrying events as {"cause":{}} and the default crash report would name no error at all.
  • A value JSON.stringify refuses outright (a circular object) does not cost the whole event: the line is written as {"type":"<type>","cause":"[unserialisable]"} instead. Left to throw, safeSink would swallow it and the event would be reported nowhere.

A sample transcript

A signal-driven shutdown of an HTTP process with one request in flight, as stderrSink writes it:

json
{"type":"building"}
{"type":"serving","runtime":"http"}
{"type":"draining","inFlight":1}
{"type":"drained","report":{"inFlightAtStart":1,"completed":1,"abandoned":0}}
{"type":"stopping"}
{"type":"exited"}

The same process failing to configure — PORT=abc — never reaches serving (the stack trace is elided here):

json
{"type":"building"}
{"type":"startFailed","cause":{"name":"ConfigInvalid","message":"HttpConfig could not be configured:\n  PORT: is not a whole number: \"abc\"","stack":"Error\n    at …"}}
{"type":"stopping"}
{"type":"exited"}

The normalisation replaces the whole Error with the four-field object, so a TaggedError's own fields (port, issues) do not appear on the line; its message — one line per variable — is what carries them. And a cause that cannot be serialised at all:

json
{ "type": "uncaught", "cause": "[unserialisable]" }

Writing a sink

ts
import { start, type EventSink } from "@btravstack/core";

const events: string[] = [];
const collect: EventSink = (event) => {
  events.push(event.type);
};

const app = start(OrderApi, { onEvent: collect, probes: false });

A sink is synchronous and returns void; anything it must await it schedules itself. Under @btravstack/testing's bootFixture the default sink is silent and a call's own onEvent wins. Only signals is forced off; probes is merely defaulted off, so a call may still ask for { probes: { port: 0 } }.

Released under the MIT License.