@btravstack/core
@btravstack/core
Classes
RuntimeStartFailed
Defined in: packages/core/src/runtime.ts:6
Extends
TaggedErrorInstance<"RuntimeStartFailed", {cause:unknown;runtime:string; }>
Constructors
Constructor
new RuntimeStartFailed(args): RuntimeStartFailed;Defined in: node_modules/.pnpm/unthrown@5.5.0/node_modules/unthrown/dist/index.d.mts:2034
Parameters
| Parameter | Type |
|---|---|
args | object & object |
Returns
Inherited from
TaggedError("RuntimeStartFailed")<{
readonly runtime: string;
readonly cause: unknown;
}>.constructorProperties
| Property | Modifier | Type | Overrides | Inherited from | Defined in |
|---|---|---|---|---|---|
_tag | readonly | "RuntimeStartFailed" | - | TaggedError("RuntimeStartFailed")._tag | node_modules/.pnpm/unthrown@5.5.0/node_modules/unthrown/dist/index.d.mts:2011 |
cause | public | unknown | - | TaggedError("RuntimeStartFailed").cause | node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.error.d.ts:24 |
message | public | string | TaggedError("RuntimeStartFailed").message | - | packages/core/src/runtime.ts:10 |
name | public | string | - | TaggedError("RuntimeStartFailed").name | node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es5.d.ts:1074 |
runtime | readonly | string | - | TaggedError("RuntimeStartFailed").runtime | packages/core/src/runtime.ts:7 |
stack? | public | string | - | TaggedError("RuntimeStartFailed").stack | node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es5.d.ts:1076 |
Type Aliases
Clock
type Clock = object;Defined in: packages/core/src/clock.ts:3
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
now | readonly | () => number | packages/core/src/clock.ts:4 |
sleep | readonly | (ms, signal?) => AsyncResult<void, never> | packages/core/src/clock.ts:5 |
DrainReport
type DrainReport = object;Defined in: packages/core/src/drain.ts:7
Properties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
abandoned | readonly | number | Units still open at the deadline. The exit-code decision reads this. | packages/core/src/drain.ts:18 |
completed | readonly | number | Units that closed during the drain. Counted from a monotonic total, not inFlightAtStart - abandoned — it may exceed inFlightAtStart if in-flight work spawned more units during the drain. That is honest reporting, not a bug: the alternative formula can go negative. | packages/core/src/drain.ts:16 |
inFlightAtStart | readonly | number | Units in flight when the drain began. | packages/core/src/drain.ts:9 |
EventSink
type EventSink = (event) => void;Defined in: packages/core/src/events.ts:14
Parameters
| Parameter | Type |
|---|---|
event | KernelEvent |
Returns
void
ExitReport
type ExitReport = object;Defined in: packages/core/src/start.ts:37
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
drain | readonly | DrainReport | undefined | packages/core/src/start.ts:39 |
reason | readonly | "signal" | "runtimeStopped" | "uncaught" | packages/core/src/start.ts:38 |
teardownErrors | readonly | readonly TeardownError[] | packages/core/src/start.ts:40 |
uptimeMs | readonly | number | packages/core/src/start.ts:41 |
KernelEvent
type KernelEvent =
| {
type: "building";
}
| {
cause: unknown;
type: "startFailed";
}
| {
runtime: string;
type: "serving";
}
| {
inFlight: number;
type: "draining";
}
| {
report: DrainReport;
type: "drained";
}
| {
type: "stopping";
}
| {
type: "exited";
}
| {
cause: unknown;
port: string;
type: "teardownError";
}
| {
cause: unknown;
type: "uncaught";
};Defined in: packages/core/src/events.ts:3
Phase
type Phase = "building" | "starting" | "serving" | "draining" | "stopping" | "exited";Defined in: packages/core/src/phase.ts:1
RunningApp
type RunningApp<E, Info> = object;Defined in: packages/core/src/start.ts:88
Type Parameters
| Type Parameter | Default type |
|---|---|
E | - |
Info | never |
Properties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
exited | readonly | AsyncResult<ExitReport, E | RuntimeStartFailed> | - | packages/core/src/start.ts:89 |
phase | readonly | () => Phase | - | packages/core/src/start.ts:92 |
probePort | readonly | () => AsyncResult<number | undefined, never> | The port the probe server actually bound, once the bind attempt has settled — undefined when probes are disabled or the bind failed. Resolves before the graph is built, since the probe server is up first. The point of it is probes: { port: 0 }: the OS picks the port, and this is how the caller learns which one. | packages/core/src/start.ts:111 |
ready | readonly | () => boolean | The predicate /readyz answers from — serving, and not forced unready by a drain or an uncaught exception. Readable synchronously, which the probe endpoint is not: the uncaught path forces it false while the phase is still "serving", a window no HTTP round trip can observe. Also what an embedder wires into a health endpoint of its own when probes is false. | packages/core/src/start.ts:102 |
requestDrain | readonly | () => void | - | packages/core/src/start.ts:91 |
runtimeInfo | readonly | () => AsyncResult<Info | undefined, never> | Whatever the runtime published about itself on Serving.info, once it is serving — undefined when the runtime publishes nothing, or when it never reached serving at all. The same deferred shape as probePort(), one layer up: probePort answers for the kernel's own probe server, this answers for the runtime. It is what a runtime binding an ephemeral port uses to tell the caller which port it got, instead of every such runtime inventing an onListening hook. | packages/core/src/start.ts:122 |
stop | readonly | () => void | - | packages/core/src/start.ts:90 |
Runtime
type Runtime<Needs, Info> = object;Defined in: packages/core/src/runtime.ts:90
Type Parameters
| Type Parameter | Default type |
|---|---|
Needs extends AnyPort | never |
Info | never |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
name | readonly | string | packages/core/src/runtime.ts:91 |
needs | readonly | readonly Needs[] | packages/core/src/runtime.ts:92 |
start | readonly | (host) => AsyncResult<Serving<Info>, RuntimeStartFailed> | packages/core/src/runtime.ts:93 |
RuntimeHost
type RuntimeHost<Needs> = object;Defined in: packages/core/src/runtime.ts:64
What a runtime is handed at start: the application services and the kernel's RunUnit. Handing it a bare Context would leave every runtime inventing its own unit tracking — the thing the kernel exists to own.
Remarks
The two contracts a runtime author owes, both easy to miss and neither checkable by the kernel: a unit's response must be flushed inside the work callback (see RunUnit), and UnitMeta.id must be unique per unit unless a traceId is supplied (see UnitMeta).
ctx is the application context, and start's gate checks a runtime's needs against the application module's exports only — a port a StartOptions.unit module provides exists only while a unit is open, and a runtime naming it as a need is rejected at the call site rather than left to ctx.get(...) throwing at startup.
Type Parameters
| Type Parameter |
|---|
Needs extends AnyPort |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
ctx | readonly | Context<InstanceType<Needs>> | packages/core/src/runtime.ts:65 |
run | readonly | RunUnit<Needs> | packages/core/src/runtime.ts:66 |
RuntimeInfoOf
type RuntimeInfoOf<X> = RuntimeOf<X> extends Runtime<AnyPort, infer Info> ? Info : never;Defined in: packages/core/src/runtime.ts:121
Type Parameters
| Type Parameter |
|---|
X |
RunUnit
type RunUnit<Needs> = <T, E>(meta, work) => AsyncResult<T, E>;Defined in: packages/core/src/runtime.ts:42
Submit one piece of work as a unit: the kernel counts it towards the drain, hands it an AbortSignal and an ambient record, and gives the work's own Result straight back — mapping that outcome to a transport is the runtime's job, never the kernel's.
Type Parameters
| Type Parameter |
|---|
Needs extends AnyPort |
Type Parameters
| Type Parameter |
|---|
T |
E |
Parameters
| Parameter | Type |
|---|---|
meta | UnitMeta |
work | (ctx, signal) => ReturnType<UnitWork<T, E>> |
Returns
AsyncResult<T, E>
Remarks
Everything the client must receive has to be flushed INSIDE work, never after the returned AsyncResult settles. A unit is closed the instant its Result settles; an idle registry is what the drain waits for, and going idle is its permission to call Serving.stop(). A runtime that resolves the unit and then writes its response is racing stop() tearing the transport down — with a small body the write usually wins, and with a large one it does not (measured with an 8 MB body: UND_ERR_SOCKET: other side closed). A unit is not "compute the answer", it is "compute the answer and get it out of the process".
UnitMeta.id must be unique per unit unless a traceId is supplied — see UnitMeta.
With a StartOptions.unit module in play, work runs only once the fork is built — after an await when a unit provider is async — not synchronously inside host.run. A runtime that subscribes to an event from inside work (a response's 'close') must first check whether it has already fired.
Serving
type Serving<Info> = object;Defined in: packages/core/src/runtime.ts:80
What a runtime is, once it is up — plus, optionally, what it wants to say about itself.
Info is the runtime's own shape and is deliberately not modelled as a port number: a runtime that binds port: 0 publishing { port } is the motivating case, but a queue consumer has no port and might publish { queue, prefetch }. It defaults to never, so info is unwritable and Serving reads exactly as it did for every runtime with nothing to publish. The caller reads it back through RunningApp.runtimeInfo().
Type Parameters
| Type Parameter | Default type |
|---|---|
Info | never |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
drain | readonly | (signal) => AsyncResult<void, never> | packages/core/src/runtime.ts:85 |
info? | readonly | Info | packages/core/src/runtime.ts:87 |
stop | readonly | () => AsyncResult<void, never> | packages/core/src/runtime.ts:86 |
StartGate
type StartGate<X, UnitNeeds> = [Extract<X, RuntimeInstance>] extends [never] ? ["NO RUNTIME", "the module exports no port declared over RuntimePort"] : [InstanceType<RuntimeNeedsOf<X>>] extends [X] ? [Exclude<UnitNeeds, X | Scope | Env>] extends [never] ? [] : ["UNSATISFIED UNIT NEEDS", Exclude<UnitNeeds, X | Scope | Env>] : ["UNSATISFIED RUNTIME NEEDS", Exclude<InstanceType<RuntimeNeedsOf<X>>, X>];Defined in: packages/core/src/start.ts:145
The phantom rest tuple start, runMain and Boot all carry: empty — and invisible — when the module exports a runtime and its exports cover that runtime's declared needs, a named error tuple otherwise, so a missing runtime or an unmet need fails to typecheck at the call site. A trailing rest tuple rather than a conditional type on module or options is deliberate: a conditional on an inference-bearing parameter makes TypeScript defer that parameter's inference and can collapse X or E to unknown. Same shape, and the same reasoning, as di's own UNSATISFIED DEPENDENCIES gate on Module.scoped.
With a unit module in play it also checks the fork's own direction: the unit module's needs must be covered by the module's exports, Scope or Env — forkScope's gate stated at start's call site, where the parent is actually known. A runtime's needs are checked against the module's exports ONLY, never the unit's: RuntimeHost.ctx is the application context, and a runtime that resolved a unit-only port at start would find nothing there — so the gate rejects it rather than letting it type-check into a startup defect.
Type Parameters
| Type Parameter | Default type |
|---|---|
X | - |
UnitNeeds | never |
StartOptions
type StartOptions<UnitX, UnitNeeds> = object;Defined in: packages/core/src/start.ts:44
Type Parameters
| Type Parameter | Default type |
|---|---|
UnitX | never |
UnitNeeds | never |
Properties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
clock? | readonly | Clock | - | packages/core/src/start.ts:76 |
drainTimeoutMs? | readonly | number | - | packages/core/src/start.ts:84 |
env? | readonly | Environment | The environment the graph is configured from — provided to it as the Env port, and what the kernel reads its own PROBE_PORT from. Defaults to process.env; a test hands in the record it wants. | packages/core/src/start.ts:50 |
onEvent? | readonly | EventSink | - | packages/core/src/start.ts:85 |
preDrainDelayMs? | readonly | number | - | packages/core/src/start.ts:83 |
probes? | readonly | | { port: number; } | false | The probe server's port. Unset, it is bound from PROBE_PORT in env (default 9000); false disables the probe server. | packages/core/src/start.ts:82 |
signals? | readonly | boolean | - | packages/core/src/start.ts:77 |
unit? | readonly | Module<UnitX, never, UnitNeeds> | A module forked around every unit: its providers are constructed when a unit opens and torn down when it closes, reading anything the application context already carries. This is what makes a per-request scope transparent — the runtime's unit work simply receives the forked context, and no handler ever calls Module.forkScope itself. The error channel is pinned to never: a unit is already inside the running application, so a construction failure here has no modeled channel to land in — it becomes the unit's defect, which each runtime already answers (an HTTP 500, a dead-letter). Its unmet needs must be covered by the module's exports (or Scope, which the fork opens); start's gate checks that at the call site. Teardown runs while the unit is still open, so a finaliser that logs does it under the unit's own trace id. A finaliser that fails is reported as a teardownError event and nowhere else — not in ExitReport.teardownErrors, which is the application scope's. With this option the unit's work runs only once the fork is built — after an await when a unit provider is async — rather than synchronously inside host.run; a runtime that attaches a listener from inside its work must be ready for the event to have already fired. | packages/core/src/start.ts:75 |
TeardownError
type TeardownError = object;Defined in: packages/core/src/start.ts:31
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
cause | readonly | unknown | packages/core/src/start.ts:31 |
port | readonly | string | packages/core/src/start.ts:31 |
UnitMeta
type UnitMeta = object;Defined in: packages/core/src/units.ts:56
What a runtime says about one piece of work as it submits it. kind is the category ("http", "tick", "job"); id identifies this unit.
Remarks
id must be unique per unit unless a traceId is supplied, because traceId defaults to it. A runtime that passes a category as the id — an HTTP runtime using the route template "POST /orders" — gives every request the same trace id, and the ambient record's whole purpose, telling one unit apart from another in a log line, is silently defeated. A broker message id or a queue job id is already unique and needs nothing more; a route template is a kind, not an id.
The kernel cannot check this — it would have to remember every id it had ever seen — so uniqueness is the runtime's to guarantee. What the kernel does guarantee is UnitRecord's unitId, minted per unit and always unique: a reader that only needs to tell two units apart already has it. traceId is the correlation id, which is why it is the one a runtime may supply — it carries an id from outside the process (a traceparent header, a message property) so a line logged here joins a trace that started elsewhere.
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
deadline? | readonly | number | packages/core/src/units.ts:61 |
id | readonly | string | packages/core/src/units.ts:58 |
kind | readonly | string | packages/core/src/units.ts:57 |
tenantId? | readonly | string | packages/core/src/units.ts:60 |
traceId? | readonly | string | packages/core/src/units.ts:59 |
UnitRecord
type UnitRecord = object;Defined in: packages/core/src/units.ts:20
What the kernel opens per unit and currentUnit() reads: a small, fixed record of data about this unit, and never a service. See the root CLAUDE.md's thesis 2 for the line that draws.
signal is the same AbortSignal the work callback receives — aborted at the drain deadline, or at once on a path that skips the drain. It is here because the callback is not always where the work is: a middleware-shaped runtime (@btravstack/temporal, @btravstack/amqp) opens the unit around a call it does not own the arguments of, so an activity or a handler has no parameter to receive it through. It is data, not a capability: there is nothing to substitute in a test, and a deadline nobody can observe is not a deadline. A transport's own cancellation — Temporal's Context.current().cancellationSignal — is a different clock, not this one.
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
deadline | readonly | number | undefined | packages/core/src/units.ts:24 |
signal | readonly | AbortSignal | packages/core/src/units.ts:25 |
tenantId | readonly | string | undefined | packages/core/src/units.ts:23 |
traceId | readonly | string | packages/core/src/units.ts:22 |
unitId | readonly | string | packages/core/src/units.ts:21 |
UnitRegistry
type UnitRegistry = object;Defined in: packages/core/src/units.ts:69
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
abortAll | readonly | () => void | packages/core/src/units.ts:77 |
awaitIdle | readonly | () => AsyncResult<void, never> | packages/core/src/units.ts:78 |
closed | readonly | () => number | packages/core/src/units.ts:76 |
inFlight | readonly | () => number | packages/core/src/units.ts:71 |
run | readonly | <T, E>(meta, work) => AsyncResult<T, E> | packages/core/src/units.ts:70 |
UnitWork
type UnitWork<T, E> = (signal) =>
| AsyncResult<T, E>
| Promise<Result<T, E>>
| Result<T, E>;Defined in: packages/core/src/units.ts:64
Type Parameters
| Type Parameter |
|---|
T |
E |
Parameters
| Parameter | Type |
|---|---|
signal | AbortSignal |
Returns
| AsyncResult<T, E> | Promise<Result<T, E>> | Result<T, E>
Variables
RuntimePort
const RuntimePort: PortClass<"Runtime">;Defined in: packages/core/src/runtime.ts:111
The port the kernel resolves its runtime from. A runtime is a service the module provides, not an option handed to start: a runtime package declares its own port over this one — class HttpRuntime extends RuntimePort<Runtime<never, HttpInfo>> {} — and ships a module providing it, so the runtime is built by di like everything else and reads its collaborators the same way. The kernel then owns nothing but the graph's lifecycle: it builds the module, resolves this port, and drives what it finds through start → serving → drain → stop.
Left generic on purpose (Port("Runtime") without a fixed service): every runtime port is one id at runtime — a process boots exactly one — while each carries its own Needs/Info in the type, which is what start's gate and RunningApp.runtimeInfo() read back out of the module's exports.
stderrSink
const stderrSink: EventSink;Defined in: packages/core/src/events.ts:38
systemClock
const systemClock: Clock;Defined in: packages/core/src/clock.ts:8
Functions
currentUnit()
function currentUnit(): UnitRecord | undefined;Defined in: packages/core/src/units.ts:32
Returns
UnitRecord | undefined
runMain()
function runMain<X, E, UnitX, UnitNeeds>(
module,
options?,
exit?, ...
gate): Promise<void>;Defined in: packages/core/src/run-main.ts:112
Boot a module and turn its outcome into a process exit code — the front door, and the one sanctioned place this package decides a process's fate. start composed with the wait for exited: use start instead when the RunningApp itself is wanted (a test, an embedder, a dev runner booting two applications — none of which may claim process.exitCode).
exit is injectable and defaults to setting process.exitCode: runMain never calls process.exit(), so pending output is flushed, an embedding host keeps control of its own lifetime, and a test can observe the code without ending the run.
| outcome | code |
|---|---|
| exited cleanly | 0 |
startup failure (a modeled Err) | 1 |
a configuration port that could not be bound (ConfigInvalid, PROBE_PORT included) | 78 |
| drained with work abandoned | 2 |
| exited with teardown errors | 2 |
| stopped by an uncaught exception or unhandled rejection | 70 |
| a defect | 70 |
The two 70s are the same statement — sysexits(3)'s EX_SOFTWARE, an internal software error — reached through the two channels a bug can take. A crash takes precedence over abandoned work. 78 is EX_CONFIG: the deployment is wrong, not the code.
Type Parameters
| Type Parameter | Default type |
|---|---|
X | - |
E | - |
UnitX | never |
UnitNeeds | never |
Parameters
| Parameter | Type |
|---|---|
module | Module<X, E, Scope | Env> |
options | StartOptions<UnitX, UnitNeeds> |
exit | (code) => void |
...gate | StartGate<X, UnitNeeds> |
Returns
Promise<void>
Example
// `OrderApi` imports the application next to `@btravstack/http`'s `http()`
// starter and exports `HttpRuntime` — the port `start` resolves the runtime
// from. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
await runMain(OrderApi);start()
function start<X, E, UnitX, UnitNeeds>(
module,
options?, ...
gate): RunningApp<E, RuntimeInfoOf<X>>;Defined in: packages/core/src/start.ts:161
Type Parameters
| Type Parameter | Default type |
|---|---|
X | - |
E | - |
UnitX | never |
UnitNeeds | never |
Parameters
| Parameter | Type |
|---|---|
module | Module<X, E, Scope | Env> |
options | StartOptions<UnitX, UnitNeeds> |
...gate | StartGate<X, UnitNeeds> |
Returns
RunningApp<E, RuntimeInfoOf<X>>