@btravstack/core 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,458 @@
1
+ import { AsyncResult, Result } from "unthrown";
2
+ import { AnyPort, Context, Module, PortClass, Scope, ServiceOf } from "@btravstack/di";
3
+ import { Env, Environment } from "@btravstack/config";
4
+ //#region src/clock.d.ts
5
+ type Clock = {
6
+ readonly now: () => number;
7
+ readonly sleep: (ms: number, signal?: AbortSignal) => AsyncResult<void, never>;
8
+ };
9
+ declare const systemClock: Clock;
10
+ //#endregion
11
+ //#region src/units.d.ts
12
+ /**
13
+ * What the kernel opens per unit and `currentUnit()` reads: a small, fixed
14
+ * record of **data about this unit**, and never a service.
15
+ *
16
+ * `signal` is the same `AbortSignal` the work callback receives — aborted at
17
+ * the drain deadline, or at once on a path that skips the drain. It is here
18
+ * because the callback is not always where the work is: a middleware-shaped
19
+ * runtime opens the unit around a call whose arguments it does not own. A
20
+ * transport's own cancellation is a different clock, not this one.
21
+ */
22
+ type UnitRecord = {
23
+ readonly unitId: string;
24
+ readonly traceId: string;
25
+ readonly tenantId: string | undefined;
26
+ readonly deadline: number | undefined;
27
+ readonly signal: AbortSignal;
28
+ };
29
+ declare const currentUnit: () => UnitRecord | undefined;
30
+ /**
31
+ * What a runtime says about one piece of work as it submits it. `kind` is the
32
+ * category (`"http"`, `"tick"`, `"job"`); `id` identifies **this** unit.
33
+ *
34
+ * @remarks
35
+ * **`id` must be unique per unit unless a `traceId` is supplied**, because
36
+ * `traceId` defaults to it. A runtime passing a CATEGORY as the id — a route
37
+ * template such as `"POST /orders"` — gives every request the same trace id and
38
+ * silently defeats the ambient record. A route template is a `kind`.
39
+ *
40
+ * The kernel cannot check this, so uniqueness is the runtime's to guarantee.
41
+ * What it does guarantee is {@link UnitRecord}'s `unitId`, minted per unit;
42
+ * `traceId` is the CORRELATION id, which is why it is the one a runtime may
43
+ * supply — it carries an id from outside the process.
44
+ */
45
+ type UnitMeta = {
46
+ readonly kind: string;
47
+ readonly id: string;
48
+ readonly traceId?: string;
49
+ readonly tenantId?: string;
50
+ readonly deadline?: number;
51
+ };
52
+ type UnitWork<T, E> = (signal: AbortSignal) => AsyncResult<T, E> | Promise<Result<T, E>> | Result<T, E>;
53
+ type UnitRegistry = {
54
+ readonly run: <T, E>(meta: UnitMeta, work: UnitWork<T, E>) => AsyncResult<T, E>;
55
+ readonly inFlight: () => number;
56
+ readonly closed: () => number;
57
+ readonly abortAll: () => void;
58
+ readonly awaitIdle: () => AsyncResult<void, never>;
59
+ };
60
+ //#endregion
61
+ //#region src/runtime.d.ts
62
+ declare const RuntimeStartFailed_base: import("unthrown").TaggedErrorConstructor<"RuntimeStartFailed">;
63
+ declare class RuntimeStartFailed extends RuntimeStartFailed_base<{
64
+ readonly runtime: string;
65
+ readonly cause: unknown;
66
+ }> {
67
+ message: string;
68
+ }
69
+ /**
70
+ * Submit one piece of work as a **unit**: the kernel counts it towards the
71
+ * drain, hands it an `AbortSignal` and an ambient record, and gives the work's
72
+ * own `Result` straight back — mapping that outcome to a transport is the
73
+ * runtime's job, never the kernel's.
74
+ *
75
+ * @remarks
76
+ * **Everything the client must receive has to be flushed INSIDE `work`, never
77
+ * after the returned `AsyncResult` settles.** A unit closes the instant its
78
+ * `Result` settles, and an idle registry is the drain's permission to call
79
+ * `Serving.stop()` — so a runtime that writes after resolving is racing the
80
+ * transport being torn down.
81
+ *
82
+ * `UnitMeta.id` must be unique per unit unless a `traceId` is supplied — see
83
+ * {@link UnitMeta}.
84
+ *
85
+ * With a `StartOptions.unit` module in play, `work` runs only once the fork is
86
+ * built. A runtime that subscribes to an event from inside `work` must first
87
+ * check whether it has already fired.
88
+ */
89
+ type RunUnit<Resolves extends AnyPort> = <T, E>(meta: UnitMeta, work: (ctx: Context<InstanceType<Resolves>>, signal: AbortSignal) => ReturnType<UnitWork<T, E>>) => AsyncResult<T, E>;
90
+ /**
91
+ * What a runtime is handed at `start`: the application services **and** the
92
+ * kernel's {@link RunUnit}. Handing it a bare `Context` would leave every
93
+ * runtime inventing its own unit tracking — the thing the kernel exists to own.
94
+ *
95
+ * @remarks
96
+ * The two contracts a runtime author owes, neither checkable by the kernel: a
97
+ * unit's response must be flushed **inside** the work callback (see
98
+ * {@link RunUnit}), and `UnitMeta.id` must be unique per unit unless a
99
+ * `traceId` is supplied (see {@link UnitMeta}).
100
+ *
101
+ * `ctx` is the **application** context: a port a `StartOptions.unit` module
102
+ * provides exists only while a unit is open, so a runtime naming it in
103
+ * `resolves` is rejected at `start`'s call site.
104
+ */
105
+ type RuntimeHost<Resolves extends AnyPort> = {
106
+ readonly ctx: Context<InstanceType<Resolves>>;
107
+ readonly run: RunUnit<Resolves>;
108
+ };
109
+ /**
110
+ * What a runtime is, once it is up — plus, optionally, what it wants to say
111
+ * about itself, read back through `RunningApp.runtimeInfo()`.
112
+ *
113
+ * `Info` is the runtime's own shape rather than a port number: a queue consumer
114
+ * has no port and might publish `{ queue, prefetch }`. It defaults to `never`,
115
+ * so `info` is unwritable for a runtime with nothing to publish.
116
+ */
117
+ type Serving<Info = never> = {
118
+ readonly drain: (signal: AbortSignal) => AsyncResult<void, never>;
119
+ readonly stop: () => AsyncResult<void, never>;
120
+ readonly info?: Info;
121
+ };
122
+ type Runtime<Resolves extends AnyPort = never, Info = never> = {
123
+ readonly name: string;
124
+ readonly resolves: readonly Resolves[];
125
+ readonly start: (host: RuntimeHost<Resolves>) => AsyncResult<Serving<Info>, RuntimeStartFailed>;
126
+ };
127
+ /**
128
+ * The port the kernel resolves its runtime from. A runtime is a **service the
129
+ * module provides**, not an option handed to `start`: a runtime package
130
+ * declares its own port over this one — `class HttpRuntime extends
131
+ * RuntimePort<Runtime<never, HttpInfo>> {}` — so it is built by di like
132
+ * everything else, and the kernel owns nothing but the lifecycle.
133
+ *
134
+ * Left generic on purpose: every runtime port is one id at runtime, since a
135
+ * process boots exactly one, while each carries its own `Resolves`/`Info` in
136
+ * the type.
137
+ */
138
+ declare const RuntimePort: PortClass<"Runtime">;
139
+ /** The instance type every runtime port shares — `Extract` this from a module's exports to find its runtime. */
140
+ type RuntimeInstance = InstanceType<PortClass<"Runtime">>;
141
+ /** The `Runtime<Resolves, Info>` a module exports, or `never` when it exports none. */
142
+ type RuntimeOf<X> = ServiceOf<Extract<X, RuntimeInstance>>;
143
+ type RuntimeResolvesOf<X> = RuntimeOf<X> extends Runtime<infer Resolves, unknown> ? Resolves : never;
144
+ type RuntimeInfoOf<X> = RuntimeOf<X> extends Runtime<AnyPort, infer Info> ? Info : never;
145
+ /**
146
+ * `running`, but no later than the kernel's drain deadline — the primitive a
147
+ * `Serving.drain` needs when the work it awaits settles on somebody else's
148
+ * clock (Temporal's `shutdownForceTime`, a broker library's close) and so
149
+ * cannot honour `signal` itself.
150
+ *
151
+ * **The losing branch's `Result` is dropped**, which is the point rather than
152
+ * an oversight: once the deadline wins, the kernel has already moved on and
153
+ * the eventual outcome has no consumer left. What that costs is the runtime's
154
+ * own business — an un-acked AMQP delivery is redelivered, so abandoning one
155
+ * repeats work rather than losing it, while a Temporal activity is retried on
156
+ * another worker.
157
+ *
158
+ * Deliberately **`Clock`-agnostic**: there is no duration here, only a signal,
159
+ * so it behaves identically under `@btravstack/testing`'s fake clock. Racing
160
+ * work against a *timeout* is a different primitive and belongs on `Clock`
161
+ * (`drain.ts` uses `clock.sleep` for exactly that, so a fake clock can control
162
+ * it) — do not fold the two together.
163
+ */
164
+ declare const releasedBy: (signal: AbortSignal, running: AsyncResult<void, never>) => AsyncResult<void, never>;
165
+ //#endregion
166
+ //#region src/drain.d.ts
167
+ type DrainReport = {
168
+ /** Units in flight when the drain began. */
169
+ readonly inFlightAtStart: number;
170
+ /**
171
+ * Units that closed during the drain, counted from a monotonic total. It may
172
+ * exceed `inFlightAtStart` when in-flight work spawned more units — honest
173
+ * reporting, where `inFlightAtStart - abandoned` can go negative.
174
+ */
175
+ readonly completed: number;
176
+ /** Units still open at the deadline. The exit-code decision reads this. */
177
+ readonly abandoned: number;
178
+ };
179
+ //#endregion
180
+ //#region src/events.d.ts
181
+ type KernelEvent = {
182
+ readonly type: "building";
183
+ } | {
184
+ readonly type: "startFailed";
185
+ readonly cause: unknown;
186
+ } | {
187
+ readonly type: "serving";
188
+ readonly runtime: string;
189
+ /**
190
+ * Whatever the runtime published on `Serving.info`. `unknown` because the
191
+ * kernel does not know a runtime's `Info` at the event union — it is read
192
+ * off the module at `start`'s call site, not here — and a sink is
193
+ * serialising it anyway. A generic `KernelEvent<Info>` would infect
194
+ * `EventSink`, `stderrSink` and every adapter for one field none of them
195
+ * reads structurally.
196
+ */
197
+ readonly info: unknown;
198
+ /**
199
+ * The port the kernel's own probe listener bound, `undefined` when probes
200
+ * are off. Its own field rather than folded into `info`: the probe server
201
+ * is the kernel's, not the runtime's, so publishing it as something the
202
+ * runtime said would be a small lie.
203
+ */
204
+ readonly probePort: number | undefined;
205
+ } | {
206
+ readonly type: "draining";
207
+ readonly inFlight: number;
208
+ } | {
209
+ readonly type: "drained";
210
+ readonly report: DrainReport;
211
+ } | {
212
+ readonly type: "stopping";
213
+ } | {
214
+ readonly type: "exited";
215
+ } | {
216
+ readonly type: "teardownError";
217
+ readonly port: string;
218
+ readonly cause: unknown;
219
+ } | {
220
+ readonly type: "uncaught";
221
+ readonly cause: unknown;
222
+ };
223
+ type EventSink = (event: KernelEvent) => void;
224
+ declare const stderrSink: EventSink;
225
+ //#endregion
226
+ //#region src/observability.d.ts
227
+ /**
228
+ * The severity of one line, and the whole of the set: six levels, ordered, with
229
+ * no caller-defined additions — which is what lets `LOG_LEVEL` be validated at
230
+ * startup and `isEnabled` be a comparison rather than a lookup.
231
+ */
232
+ type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
233
+ /** The levels in order, least severe first — what `isEnabled` compares through. */
234
+ declare const LEVELS: readonly Level[];
235
+ /**
236
+ * What a line, a span or a measurement carries besides itself: a flat record of
237
+ * scalars.
238
+ *
239
+ * Flat and scalar on purpose. A nested object is where a field's name stops
240
+ * being stable, and an `unknown` value is where a logger starts stringifying
241
+ * whatever it is handed — which is how a log call becomes the thing that
242
+ * throws. A failure has a channel of its own, `cause`.
243
+ */
244
+ type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;
245
+ /**
246
+ * Every method takes the same three arguments in the same order:
247
+ * `(message, attributes?, cause?)`.
248
+ *
249
+ * Uniform on purpose. A failure is not a property of severity — an `info` line
250
+ * reporting a recovered fault carries one too — so every level can take a
251
+ * `cause`, at the cost of `logger.error("boom", undefined, cause)` for a
252
+ * failure with nothing else to say.
253
+ */
254
+ type LoggerService = {
255
+ readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
256
+ readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
257
+ readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
258
+ readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
259
+ readonly warn: (message: string, attributes?: Attributes, cause?: unknown) => void;
260
+ /** `cause` is the failure itself — an `Error`, an `unthrown` `Err`'s error, a rejected value. */
261
+ readonly error: (message: string, attributes?: Attributes, cause?: unknown) => void;
262
+ readonly fatal: (message: string, attributes?: Attributes, cause?: unknown) => void;
263
+ /** A logger carrying `attributes` on every line it writes, on top of this one's. Never mutates this one. */
264
+ readonly with: (attributes: Attributes) => LoggerService;
265
+ /** Whether a line at `level` would be written — for a payload expensive enough to be worth not building. */
266
+ readonly isEnabled: (level: Level) => boolean;
267
+ };
268
+ declare const Logger_base: import("@btravstack/di").PortClass<"Logger">;
269
+ /**
270
+ * The application's logger, as a port — declared here and implemented in
271
+ * `@btravstack/observability`, because a contract every package may depend on
272
+ * has to be reachable without installing an implementation, and because the
273
+ * correlation an implementation stamps per line is the kernel's own ambient
274
+ * record.
275
+ *
276
+ * An implementation must not throw: a logger that throws turns an observability
277
+ * problem into an outage. Synchronous `void`, not an `AsyncResult` — thesis 6's
278
+ * one deliberate exemption, since a lost line is not a modeled error.
279
+ */
280
+ declare class Logger extends Logger_base<LoggerService> {}
281
+ /** A span's outcome: unset until something says otherwise, then `ok` or `error`. */
282
+ declare const SPAN_STATUS: {
283
+ readonly unset: 0;
284
+ readonly ok: 1;
285
+ readonly error: 2;
286
+ };
287
+ /** The three codes {@link SPAN_STATUS} names, as the type a {@link Span} takes. */
288
+ type SpanStatusCode = (typeof SPAN_STATUS)[keyof typeof SPAN_STATUS];
289
+ /**
290
+ * One span, narrowed to what a framework package does with it: label it, say
291
+ * how it ended, end it. The numbers are OpenTelemetry's own, so an OTel span
292
+ * satisfies this structurally with no translation in between.
293
+ */
294
+ type Span = {
295
+ readonly setAttributes: (attributes: Attributes) => unknown;
296
+ readonly setStatus: (status: {
297
+ readonly code: SpanStatusCode;
298
+ readonly message?: string;
299
+ }) => unknown;
300
+ readonly end: () => void;
301
+ };
302
+ /** What a {@link Tracer} does: start a span, by name. */
303
+ type TracerService = {
304
+ readonly startSpan: (name: string) => Span;
305
+ };
306
+ declare const Tracer_base: import("@btravstack/di").PortClass<"Tracer">;
307
+ /**
308
+ * The application's tracer, as a port — declared without naming OpenTelemetry,
309
+ * because a port typed as a vendor's type points the dependency arrow outwards
310
+ * and makes the whole family install that vendor.
311
+ */
312
+ declare class Tracer extends Tracer_base<TracerService> {}
313
+ /** A monotonic count. */
314
+ type Counter = {
315
+ readonly add: (value: number, attributes?: Attributes) => void;
316
+ };
317
+ /** A distribution of measurements. */
318
+ type Histogram = {
319
+ readonly record: (value: number, attributes?: Attributes) => void;
320
+ };
321
+ /**
322
+ * What a {@link Meter} does: mint the two instruments a framework package needs
323
+ * — it counts what happened and measures how long it took. A gauge is something
324
+ * an application declares about its own domain, reaching the vendor's meter for
325
+ * it as it would any other adapter.
326
+ */
327
+ type MeterService = {
328
+ readonly createCounter: (name: string, options?: {
329
+ readonly description?: string;
330
+ readonly unit?: string;
331
+ }) => Counter;
332
+ readonly createHistogram: (name: string, options?: {
333
+ readonly description?: string;
334
+ readonly unit?: string;
335
+ }) => Histogram;
336
+ };
337
+ declare const Meter_base: import("@btravstack/di").PortClass<"Meter">;
338
+ /** The application's meter, as a port. See {@link Tracer} for why it does not name a vendor. */
339
+ declare class Meter extends Meter_base<MeterService> {}
340
+ //#endregion
341
+ //#region src/phase.d.ts
342
+ type Phase = "building" | "starting" | "serving" | "draining" | "stopping" | "exited";
343
+ //#endregion
344
+ //#region src/start.d.ts
345
+ type TeardownError = {
346
+ readonly port: string;
347
+ readonly cause: unknown;
348
+ };
349
+ type ExitReport = {
350
+ readonly reason: "signal" | "runtimeStopped" | "uncaught";
351
+ readonly drain: DrainReport | undefined;
352
+ readonly teardownErrors: readonly TeardownError[];
353
+ readonly uptimeMs: number;
354
+ };
355
+ type StartOptions<UnitX = never, UnitNeeds = never> = {
356
+ /**
357
+ * The environment the graph is configured from, provided to it as the `Env`
358
+ * port and read for the kernel's own `PROBE_PORT`. Defaults to
359
+ * `process.env`.
360
+ */
361
+ readonly env?: Environment;
362
+ /**
363
+ * A module forked around **every unit**: built as the unit opens, torn down
364
+ * as it closes — while the unit's ambient record is still open — reading
365
+ * anything the application context carries.
366
+ *
367
+ * A failing unit finaliser is reported as a `teardownError` event and never
368
+ * in `ExitReport.teardownErrors`. With this option the unit's work runs only
369
+ * once the fork is built, so a runtime that subscribes to an event from
370
+ * inside its work must be ready for it to have already fired.
371
+ */
372
+ readonly unit?: Module<UnitX, never, UnitNeeds>;
373
+ readonly clock?: Clock;
374
+ readonly signals?: boolean;
375
+ /**
376
+ * The probe server's port. Unset, it is bound from `PROBE_PORT` in `env`
377
+ * (default `9000`); `false` disables the probe server.
378
+ */
379
+ readonly probes?: {
380
+ readonly port: number;
381
+ } | false;
382
+ readonly preDrainDelayMs?: number;
383
+ readonly drainTimeoutMs?: number;
384
+ readonly onEvent?: EventSink;
385
+ };
386
+ type RunningApp<E, Info = never> = {
387
+ readonly exited: AsyncResult<ExitReport, E | RuntimeStartFailed>;
388
+ readonly stop: () => void;
389
+ readonly requestDrain: () => void;
390
+ readonly phase: () => Phase;
391
+ /**
392
+ * The predicate `/readyz` answers from — serving, and not forced unready by
393
+ * a drain or an uncaught exception — read synchronously, which the probe
394
+ * endpoint is not.
395
+ */
396
+ readonly ready: () => boolean;
397
+ /**
398
+ * The port the probe server actually bound, once the bind attempt has
399
+ * settled; `undefined` when probes are disabled or the bind failed.
400
+ */
401
+ readonly probePort: () => AsyncResult<number | undefined, never>;
402
+ /**
403
+ * Whatever the runtime published on `Serving.info` once it is serving;
404
+ * `undefined` when it publishes nothing or never reached `serving`.
405
+ */
406
+ readonly runtimeInfo: () => AsyncResult<Info | undefined, never>;
407
+ };
408
+ /**
409
+ * The phantom marker `start`, `runMain` and `Boot` all intersect onto their
410
+ * `module` parameter: `unknown` — and invisible — when the module exports a
411
+ * runtime, its exports cover what that runtime resolves and they cover the
412
+ * unit module's needs; one of three sentences otherwise, printed at the call
413
+ * site as the parameter type the argument did not match.
414
+ *
415
+ * `unknown` is the satisfied arm because intersecting it leaves the module
416
+ * type untouched. A runtime's `resolves` is checked against the module's
417
+ * exports only, never the unit module's: `RuntimeHost.ctx` is the application
418
+ * context, so a unit-only port would resolve to nothing there.
419
+ */
420
+ type StartGate<X, UnitNeeds = never> = [Extract<X, RuntimeInstance>] extends [never] ? "NO RUNTIME — the module exports no port declared over RuntimePort" : [InstanceType<RuntimeResolvesOf<X>>] extends [X] ? [Exclude<UnitNeeds, X | Scope | Env>] extends [never] ? unknown : "UNSATISFIED UNIT NEEDS — the unit module needs a port the module does not export" : "UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export";
421
+ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X, E, Scope | Env> & StartGate<X, UnitNeeds>, options?: StartOptions<UnitX, UnitNeeds>) => RunningApp<E, RuntimeInfoOf<X>>;
422
+ //#endregion
423
+ //#region src/run-main.d.ts
424
+ /**
425
+ * Boot a module and turn its outcome into a process exit code — the front
426
+ * door, and the one sanctioned place this package decides a process's fate.
427
+ * `start` composed with the wait for `exited`: use `start` instead when the
428
+ * `RunningApp` itself is wanted (a test, an embedder, a dev runner booting
429
+ * two applications — none of which may claim `process.exitCode`).
430
+ *
431
+ * `exit` is injectable and defaults to setting `process.exitCode`: `runMain`
432
+ * never calls `process.exit()`, so pending output is flushed, an embedding
433
+ * host keeps control of its own lifetime, and a test can observe the code
434
+ * without ending the run.
435
+ *
436
+ * | outcome | code |
437
+ * | --- | --- |
438
+ * | exited cleanly | `0` |
439
+ * | startup failure (a modeled `Err`) | `1` |
440
+ * | a configuration port that could not be bound (`ConfigInvalid`, `PROBE_PORT` included) | `78` |
441
+ * | drained with work abandoned | `2` |
442
+ * | exited with teardown errors | `2` |
443
+ * | stopped by an uncaught exception or unhandled rejection | `70` |
444
+ * | a defect | `70` |
445
+ *
446
+ * The two `70`s are the same statement reached through the two channels a bug
447
+ * can take. A crash takes precedence over abandoned work.
448
+ *
449
+ * @example
450
+ * ```ts
451
+ * // `OrderApi` imports the application next to `http()` and exports
452
+ * // `HttpRuntime`. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
453
+ * await runMain(OrderApi);
454
+ * ```
455
+ */
456
+ declare const runMain: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X, E, Scope | Env> & StartGate<X, UnitNeeds>, options?: StartOptions<UnitX, UnitNeeds>, exit?: (code: number) => void) => Promise<void>;
457
+ //#endregion
458
+ export { type Attributes, type Clock, type Counter, type DrainReport, type EventSink, type ExitReport, type Histogram, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, releasedBy, runMain, start, stderrSink, systemClock };