Skip to content

@btravstack/core


@btravstack/core

Classes

RuntimeStartFailed

Defined in: packages/core/src/runtime.ts:6

Extends

  • TaggedErrorInstance<"RuntimeStartFailed", { cause: unknown; runtime: string; }>

Constructors

Constructor
ts
new RuntimeStartFailed(args): RuntimeStartFailed;

Defined in: node_modules/.pnpm/unthrown@5.5.0/node_modules/unthrown/dist/index.d.mts:2034

Parameters
ParameterType
argsobject & object
Returns

RuntimeStartFailed

Inherited from
ts
TaggedError("RuntimeStartFailed")<{
  readonly runtime: string;
  readonly cause: unknown;
}>.constructor

Properties

PropertyModifierTypeOverridesInherited fromDefined in
_tagreadonly"RuntimeStartFailed"-TaggedError("RuntimeStartFailed")._tagnode_modules/.pnpm/unthrown@5.5.0/node_modules/unthrown/dist/index.d.mts:2011
causepublicunknown-TaggedError("RuntimeStartFailed").causenode_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.error.d.ts:24
messagepublicstringTaggedError("RuntimeStartFailed").message-packages/core/src/runtime.ts:10
namepublicstring-TaggedError("RuntimeStartFailed").namenode_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es5.d.ts:1074
runtimereadonlystring-TaggedError("RuntimeStartFailed").runtimepackages/core/src/runtime.ts:7
stack?publicstring-TaggedError("RuntimeStartFailed").stacknode_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es5.d.ts:1076

Type Aliases

Clock

ts
type Clock = object;

Defined in: packages/core/src/clock.ts:3

Properties

PropertyModifierTypeDefined in
nowreadonly() => numberpackages/core/src/clock.ts:4
sleepreadonly(ms, signal?) => AsyncResult<void, never>packages/core/src/clock.ts:5

DrainReport

ts
type DrainReport = object;

Defined in: packages/core/src/drain.ts:7

Properties

PropertyModifierTypeDescriptionDefined in
abandonedreadonlynumberUnits still open at the deadline. The exit-code decision reads this.packages/core/src/drain.ts:18
completedreadonlynumberUnits 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
inFlightAtStartreadonlynumberUnits in flight when the drain began.packages/core/src/drain.ts:9

EventSink

ts
type EventSink = (event) => void;

Defined in: packages/core/src/events.ts:14

Parameters

ParameterType
eventKernelEvent

Returns

void


ExitReport

ts
type ExitReport = object;

Defined in: packages/core/src/start.ts:37

Properties

PropertyModifierTypeDefined in
drainreadonlyDrainReport | undefinedpackages/core/src/start.ts:39
reasonreadonly"signal" | "runtimeStopped" | "uncaught"packages/core/src/start.ts:38
teardownErrorsreadonlyreadonly TeardownError[]packages/core/src/start.ts:40
uptimeMsreadonlynumberpackages/core/src/start.ts:41

KernelEvent

ts
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

ts
type Phase = "building" | "starting" | "serving" | "draining" | "stopping" | "exited";

Defined in: packages/core/src/phase.ts:1


RunningApp

ts
type RunningApp<E, Info> = object;

Defined in: packages/core/src/start.ts:88

Type Parameters

Type ParameterDefault type
E-
Infonever

Properties

PropertyModifierTypeDescriptionDefined in
exitedreadonlyAsyncResult<ExitReport, E | RuntimeStartFailed>-packages/core/src/start.ts:89
phasereadonly() => Phase-packages/core/src/start.ts:92
probePortreadonly() => 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
readyreadonly() => booleanThe 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
requestDrainreadonly() => void-packages/core/src/start.ts:91
runtimeInforeadonly() => 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
stopreadonly() => void-packages/core/src/start.ts:90

Runtime

ts
type Runtime<Needs, Info> = object;

Defined in: packages/core/src/runtime.ts:90

Type Parameters

Type ParameterDefault type
Needs extends AnyPortnever
Infonever

Properties

PropertyModifierTypeDefined in
namereadonlystringpackages/core/src/runtime.ts:91
needsreadonlyreadonly Needs[]packages/core/src/runtime.ts:92
startreadonly(host) => AsyncResult<Serving<Info>, RuntimeStartFailed>packages/core/src/runtime.ts:93

RuntimeHost

ts
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

PropertyModifierTypeDefined in
ctxreadonlyContext<InstanceType<Needs>>packages/core/src/runtime.ts:65
runreadonlyRunUnit<Needs>packages/core/src/runtime.ts:66

RuntimeInfoOf

ts
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

ts
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

ParameterType
metaUnitMeta
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

ts
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 ParameterDefault type
Infonever

Properties

PropertyModifierTypeDefined in
drainreadonly(signal) => AsyncResult<void, never>packages/core/src/runtime.ts:85
info?readonlyInfopackages/core/src/runtime.ts:87
stopreadonly() => AsyncResult<void, never>packages/core/src/runtime.ts:86

StartGate

ts
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 EnvforkScope'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 ParameterDefault type
X-
UnitNeedsnever

StartOptions

ts
type StartOptions<UnitX, UnitNeeds> = object;

Defined in: packages/core/src/start.ts:44

Type Parameters

Type ParameterDefault type
UnitXnever
UnitNeedsnever

Properties

PropertyModifierTypeDescriptionDefined in
clock?readonlyClock-packages/core/src/start.ts:76
drainTimeoutMs?readonlynumber-packages/core/src/start.ts:84
env?readonlyEnvironmentThe 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?readonlyEventSink-packages/core/src/start.ts:85
preDrainDelayMs?readonlynumber-packages/core/src/start.ts:83
probes?readonly| { port: number; } | falseThe 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?readonlyboolean-packages/core/src/start.ts:77
unit?readonlyModule<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

ts
type TeardownError = object;

Defined in: packages/core/src/start.ts:31

Properties

PropertyModifierTypeDefined in
causereadonlyunknownpackages/core/src/start.ts:31
portreadonlystringpackages/core/src/start.ts:31

UnitMeta

ts
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

PropertyModifierTypeDefined in
deadline?readonlynumberpackages/core/src/units.ts:61
idreadonlystringpackages/core/src/units.ts:58
kindreadonlystringpackages/core/src/units.ts:57
tenantId?readonlystringpackages/core/src/units.ts:60
traceId?readonlystringpackages/core/src/units.ts:59

UnitRecord

ts
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

PropertyModifierTypeDefined in
deadlinereadonlynumber | undefinedpackages/core/src/units.ts:24
signalreadonlyAbortSignalpackages/core/src/units.ts:25
tenantIdreadonlystring | undefinedpackages/core/src/units.ts:23
traceIdreadonlystringpackages/core/src/units.ts:22
unitIdreadonlystringpackages/core/src/units.ts:21

UnitRegistry

ts
type UnitRegistry = object;

Defined in: packages/core/src/units.ts:69

Properties

PropertyModifierTypeDefined in
abortAllreadonly() => voidpackages/core/src/units.ts:77
awaitIdlereadonly() => AsyncResult<void, never>packages/core/src/units.ts:78
closedreadonly() => numberpackages/core/src/units.ts:76
inFlightreadonly() => numberpackages/core/src/units.ts:71
runreadonly<T, E>(meta, work) => AsyncResult<T, E>packages/core/src/units.ts:70

UnitWork

ts
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

ParameterType
signalAbortSignal

Returns

| AsyncResult<T, E> | Promise<Result<T, E>> | Result<T, E>

Variables

RuntimePort

ts
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 startservingdrainstop.

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

ts
const stderrSink: EventSink;

Defined in: packages/core/src/events.ts:38


systemClock

ts
const systemClock: Clock;

Defined in: packages/core/src/clock.ts:8

Functions

currentUnit()

ts
function currentUnit(): UnitRecord | undefined;

Defined in: packages/core/src/units.ts:32

Returns

UnitRecord | undefined


runMain()

ts
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.

outcomecode
exited cleanly0
startup failure (a modeled Err)1
a configuration port that could not be bound (ConfigInvalid, PROBE_PORT included)78
drained with work abandoned2
exited with teardown errors2
stopped by an uncaught exception or unhandled rejection70
a defect70

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 ParameterDefault type
X-
E-
UnitXnever
UnitNeedsnever

Parameters

ParameterType
moduleModule<X, E, Scope | Env>
optionsStartOptions<UnitX, UnitNeeds>
exit(code) => void
...gateStartGate<X, UnitNeeds>

Returns

Promise<void>

Example

ts
// `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()

ts
function start<X, E, UnitX, UnitNeeds>(
   module, 
   options?, ...
   gate): RunningApp<E, RuntimeInfoOf<X>>;

Defined in: packages/core/src/start.ts:161

Type Parameters

Type ParameterDefault type
X-
E-
UnitXnever
UnitNeedsnever

Parameters

ParameterType
moduleModule<X, E, Scope | Env>
optionsStartOptions<UnitX, UnitNeeds>
...gateStartGate<X, UnitNeeds>

Returns

RunningApp<E, RuntimeInfoOf<X>>

Released under the MIT License.