@btravstack/core 0.11.0 → 0.13.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.
package/README.md CHANGED
@@ -68,8 +68,8 @@ const ticker: Runtime<typeof Greeter> = {
68
68
  // and stays out of it. A timer has nowhere to return one, so it observes
69
69
  // it instead; dropping it would hide the work's `Err` *and* a `Defect`.
70
70
  void host
71
- .run({ kind: "tick", id: `${Date.now()}` }, (ctx, signal) =>
72
- signal.aborted ? Ok("") : Ok(ctx.get(Greeter).greet("world")),
71
+ .run({ kind: "tick", id: `${Date.now()}` }, (unit, signal) =>
72
+ signal.aborted ? Ok("") : Ok(unit.ctx.get(Greeter).greet("world")),
73
73
  )
74
74
  .tapFailure((failure) => {
75
75
  process.stderr.write(`${JSON.stringify({ tick: failure.tag })}\n`);
@@ -112,6 +112,22 @@ and turns the outcome into a process exit code — one call, and the whole of a
112
112
  `TickerApp` that does not export `Greeter` is a type error at the call, and so
113
113
  is a module that exports no runtime port at all.
114
114
 
115
+ ## For a runtime author
116
+
117
+ Two helpers sit beside the `Runtime` contract, because every transport needs
118
+ them and none should own a private copy:
119
+
120
+ - **`releasedBy(signal, running)`** — `running`, but no later than the kernel's
121
+ drain deadline, for a `Serving.drain` whose work settles on somebody else's
122
+ clock.
123
+ - **`traceIdOfTraceparent(header)`** — the trace-id field of a W3C
124
+ `traceparent`, and nothing else of it: the parent's span id is dropped, since
125
+ `UnitMeta.traceId` is a correlation id rather than a span context. Answers
126
+ `undefined` for anything the specification calls invalid — a malformed
127
+ header, an all-zero trace id, an all-zero parent id, the reserved version
128
+ `ff` — so your runtime falls back to its own id instead of adopting one that
129
+ means nothing. A higher version's extra fields are read rather than refused.
130
+
115
131
  ## What you get
116
132
 
117
133
  - **A drain that survives Kubernetes.** On SIGTERM: readiness flips false, the
@@ -129,8 +145,9 @@ is a module that exports no runtime port at all.
129
145
  bad value is a `ConfigInvalid` naming every fault at once, and exit `78`.
130
146
  - **Teardown on every path**, with failing finalisers in
131
147
  `ExitReport.teardownErrors`, never masking the exit reason.
132
- - **A per-unit scope no handler manages** — pass a module as
133
- `StartOptions.unit` and the kernel forks it around every unit.
148
+ - **A per-unit scope no handler manages** — the runtime calls
149
+ `unit.fork(module, seed)` from inside `host.run`'s work callback, and the
150
+ kernel closes the scope when the unit settles.
134
151
  - **An ambient record, not an ambient container** — `currentUnit()` reads
135
152
  `{ unitId, traceId, tenantId, signal }`; services never travel
136
153
  there. `signal` is the very `AbortSignal` the unit's work callback is handed,
package/dist/index.cjs CHANGED
@@ -206,6 +206,39 @@ var RuntimeStartFailed = class extends (0, unthrown.TaggedError)("RuntimeStartFa
206
206
  */
207
207
  const RuntimePort = (0, _btravstack_di.Port)("Runtime");
208
208
  /**
209
+ * The trace id inside a W3C `traceparent` header, and nothing else of it.
210
+ *
211
+ * @remarks
212
+ * The parent's **span id is dropped**, deliberately: `UnitMeta.traceId` is a
213
+ * correlation id rather than a span context, and half-carrying one would
214
+ * suggest a parent-child link nothing here maintains.
215
+ *
216
+ * Three things the specification calls invalid are refused like a malformed
217
+ * header, because adopting one would replace a runtime's own usable id with a
218
+ * value that means nothing: an **all-zero trace id**, an **all-zero parent
219
+ * id** (the header is well-formed and names no span), and **version `ff`**,
220
+ * which is reserved and never a version a caller may send.
221
+ *
222
+ * It is here rather than in a runtime because **every** transport carrying an
223
+ * inbound trace needs the same answer, and two copies of a parser is two
224
+ * places for the all-zero rule to be forgotten. A runtime pairs it with the
225
+ * adopt-only-a-non-blank-inbound-id rule its own headers need:
226
+ * `UnitMeta.traceId` defaults to `meta.id` when it is nullish and `""` is not,
227
+ * so an empty header would hand a caller's every unit the same blank id.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * const parent = request.headers["traceparent"];
232
+ * const traceId = typeof parent === "string" ? traceIdOfTraceparent(parent) : undefined;
233
+ * ```
234
+ */
235
+ const traceIdOfTraceparent = (header) => {
236
+ const [, version, traceId, parentId, extra] = /^(?!ff)([\da-f]{2})-([\da-f]{32})-([\da-f]{16})-[\da-f]{2}(-.*)?$/.exec(header.trim()) ?? [];
237
+ if (traceId === void 0 || parentId === void 0) return void 0;
238
+ if (version === "00" && extra !== void 0) return void 0;
239
+ return /^0+$/.test(traceId) || /^0+$/.test(parentId) ? void 0 : traceId;
240
+ };
241
+ /**
209
242
  * `running`, but no later than the kernel's drain deadline — the primitive a
210
243
  * `Serving.drain` needs when the work it awaits settles on somebody else's
211
244
  * clock (Temporal's `shutdownForceTime`, a broker library's close) and so
@@ -580,15 +613,43 @@ const start = (module, options = {}) => {
580
613
  runtimeName = runtime.name;
581
614
  healthChecks = ctx.get(HealthChecks);
582
615
  const runtimeCtx = ctx;
583
- const unit = options.unit;
584
616
  const run = (meta, work) => registry.run(meta, (signal) => {
585
- if (unit === void 0) return work(runtimeCtx, signal);
586
- const fork = _btravstack_di.Module.forkScope;
587
- return fork(ctx, unit, (forked) => (0, unthrown.fromSafePromise)((async () => await work(forked, signal))()).flatMap((result) => result), { onTeardownError: (port, cause) => emit({
588
- type: "teardownError",
589
- port,
590
- cause
591
- }) });
617
+ const settled = Promise.withResolvers();
618
+ let closing;
619
+ let hasSettled = false;
620
+ const fork = (module, seed) => {
621
+ if (hasSettled) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
622
+ if (closing !== void 0) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
623
+ const ready = Promise.withResolvers();
624
+ closing = _btravstack_di.Module.forkScope(ctx, module, (forked) => {
625
+ ready.resolve(forked);
626
+ return (0, unthrown.fromSafePromise)(settled.promise);
627
+ }, {
628
+ seed,
629
+ onTeardownError: (port, cause) => emit({
630
+ type: "teardownError",
631
+ port,
632
+ cause
633
+ })
634
+ }).recoverDefect((cause) => {
635
+ ready.reject(cause);
636
+ return (0, unthrown.Ok)();
637
+ }).get();
638
+ return (0, unthrown.fromSafePromise)(ready.promise);
639
+ };
640
+ const outcome = (async () => {
641
+ try {
642
+ return await work({
643
+ ctx: runtimeCtx,
644
+ fork
645
+ }, signal);
646
+ } finally {
647
+ hasSettled = true;
648
+ settled.resolve();
649
+ if (closing !== void 0) await closing;
650
+ }
651
+ })();
652
+ return (0, unthrown.fromSafePromise)(outcome).flatMap((result) => result);
592
653
  });
593
654
  const host = {
594
655
  ctx: runtimeCtx,
@@ -709,3 +770,4 @@ exports.runMain = runMain;
709
770
  exports.start = start;
710
771
  exports.stderrSink = stderrSink;
711
772
  exports.systemClock = systemClock;
773
+ exports.traceIdOfTraceparent = traceIdOfTraceparent;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AsyncResult, Result } from "unthrown";
2
- import { AnyPort, Context, Module, PortClass, PortInstance, Scope, ServiceOf } from "@btravstack/di";
2
+ import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
3
3
  import { Env, Environment } from "@btravstack/config";
4
4
  //#region src/instrumentation.d.ts
5
5
  /**
@@ -372,11 +372,28 @@ declare class RuntimeStartFailed extends RuntimeStartFailed_base<{
372
372
  * `UnitMeta.id` must be unique per unit unless a `traceId` is supplied — see
373
373
  * {@link UnitMeta}.
374
374
  *
375
- * With a `StartOptions.unit` module in play, `work` runs only once the fork is
376
- * built. A runtime that subscribes to an event from inside `work` must first
377
- * check whether it has already fired.
375
+ * A runtime forks the unit's scope itself, through `UnitHost.fork`, at the
376
+ * moment it holds the unit's input; the kernel closes that scope when the
377
+ * unit settles, after the response is flushed. Work that subscribes to an
378
+ * event after a `fork` must first check whether it already fired.
378
379
  */
379
- type RunUnit<Resolves extends AnyPort> = <T, E>(meta: UnitMeta, work: (ctx: Context<InstanceType<Resolves>>, signal: AbortSignal) => ReturnType<UnitWork<T, E>>) => AsyncResult<T, E>;
380
+ type RunUnit<Resolves extends AnyPort> = <T, E>(meta: UnitMeta, work: (unit: UnitHost<Resolves>, signal: AbortSignal) => ReturnType<UnitWork<T, E>>) => AsyncResult<T, E>;
381
+ /**
382
+ * What the kernel hands a unit's work: the application context, and the one
383
+ * way to open the unit's own scope.
384
+ *
385
+ * `fork` builds `module` over the application context plus `seed` and hands
386
+ * the forked context back. The scope is torn down when the unit closes —
387
+ * inside the registry's unit, so the unit is not counted closed until its
388
+ * finalisers have run, and inside the unit's ambient record, so a teardown
389
+ * log line carries the unit's ids. A construction failure rides the unit's
390
+ * defect path. A unit forks once; a second call is a defect, and so is a call
391
+ * made after the unit has settled — nothing awaits that scope's teardown.
392
+ */
393
+ type UnitHost<Resolves extends AnyPort> = {
394
+ readonly ctx: Context<InstanceType<Resolves>>;
395
+ readonly fork: <UnitX, N, Seeded extends AnyPort = never>(module: Module<UnitX, never, N> & DependencyGate<Exclude<N, InstanceType<Resolves> | InstanceType<Seeded> | Scope>>, seed: readonly SeedEntry<Seeded>[]) => AsyncResult<Context<InstanceType<Resolves> | UnitX | InstanceType<Seeded>>, never>;
396
+ };
380
397
  /**
381
398
  * What a runtime is handed at `start`: the application services **and** the
382
399
  * kernel's {@link RunUnit}. Handing it a bare `Context` would leave every
@@ -388,8 +405,8 @@ type RunUnit<Resolves extends AnyPort> = <T, E>(meta: UnitMeta, work: (ctx: Cont
388
405
  * {@link RunUnit}), and `UnitMeta.id` must be unique per unit unless a
389
406
  * `traceId` is supplied (see {@link UnitMeta}).
390
407
  *
391
- * `ctx` is the **application** context: a port a `StartOptions.unit` module
392
- * provides exists only while a unit is open, so a runtime naming it in
408
+ * `ctx` is the **application** context: a port a unit's `fork` module provides
409
+ * exists only in the `Context` `fork` hands back, so a runtime naming it in
393
410
  * `resolves` is rejected at `start`'s call site.
394
411
  */
395
412
  type RuntimeHost<Resolves extends AnyPort> = {
@@ -430,8 +447,40 @@ declare const RuntimePort: PortClass<"Runtime">;
430
447
  type RuntimeInstance = InstanceType<PortClass<"Runtime">>;
431
448
  /** The `Runtime<Resolves, Info>` a module exports, or `never` when it exports none. */
432
449
  type RuntimeOf<X> = ServiceOf<Extract<X, RuntimeInstance>>;
433
- type RuntimeResolvesOf<X> = RuntimeOf<X> extends Runtime<infer Resolves, unknown> ? Resolves : never;
434
- type RuntimeInfoOf<X> = RuntimeOf<X> extends Runtime<AnyPort, infer Info> ? Info : never;
450
+ type RuntimeResolvesOf<X> = RuntimeOf<X> extends {
451
+ readonly resolves: readonly (infer Resolves extends AnyPort)[];
452
+ } ? Resolves : never;
453
+ type RuntimeInfoOf<X> = RuntimeOf<X> extends {
454
+ readonly start: (host: never) => AsyncResult<Serving<infer Info>, RuntimeStartFailed>;
455
+ } ? Info : never;
456
+ /**
457
+ * The trace id inside a W3C `traceparent` header, and nothing else of it.
458
+ *
459
+ * @remarks
460
+ * The parent's **span id is dropped**, deliberately: `UnitMeta.traceId` is a
461
+ * correlation id rather than a span context, and half-carrying one would
462
+ * suggest a parent-child link nothing here maintains.
463
+ *
464
+ * Three things the specification calls invalid are refused like a malformed
465
+ * header, because adopting one would replace a runtime's own usable id with a
466
+ * value that means nothing: an **all-zero trace id**, an **all-zero parent
467
+ * id** (the header is well-formed and names no span), and **version `ff`**,
468
+ * which is reserved and never a version a caller may send.
469
+ *
470
+ * It is here rather than in a runtime because **every** transport carrying an
471
+ * inbound trace needs the same answer, and two copies of a parser is two
472
+ * places for the all-zero rule to be forgotten. A runtime pairs it with the
473
+ * adopt-only-a-non-blank-inbound-id rule its own headers need:
474
+ * `UnitMeta.traceId` defaults to `meta.id` when it is nullish and `""` is not,
475
+ * so an empty header would hand a caller's every unit the same blank id.
476
+ *
477
+ * @example
478
+ * ```ts
479
+ * const parent = request.headers["traceparent"];
480
+ * const traceId = typeof parent === "string" ? traceIdOfTraceparent(parent) : undefined;
481
+ * ```
482
+ */
483
+ declare const traceIdOfTraceparent: (header: string) => string | undefined;
435
484
  /**
436
485
  * `running`, but no later than the kernel's drain deadline — the primitive a
437
486
  * `Serving.drain` needs when the work it awaits settles on somebody else's
@@ -527,26 +576,13 @@ type ExitReport = {
527
576
  readonly teardownErrors: readonly TeardownError[];
528
577
  readonly uptimeMs: number;
529
578
  };
530
- type StartOptions<UnitX = never, UnitNeeds = never> = {
579
+ type StartOptions = {
531
580
  /**
532
581
  * The environment the graph is configured from, provided to it as the `Env`
533
582
  * port and read for the kernel's own `PROBE_PORT`. Defaults to
534
583
  * `process.env`.
535
584
  */
536
585
  readonly env?: Environment;
537
- /**
538
- * A module forked around **every unit** — a unit being one bounded piece of
539
- * work, whatever the transport calls it: an HTTP request, a Temporal
540
- * activity, an AMQP delivery. Built as the unit opens, torn down as it
541
- * closes — while the unit's ambient record is still open — reading anything
542
- * the application context carries.
543
- *
544
- * A failing unit finaliser is reported as a `teardownError` event and never
545
- * in `ExitReport.teardownErrors`. With this option the unit's work runs only
546
- * once the fork is built, so a runtime that subscribes to an event from
547
- * inside its work must be ready for it to have already fired.
548
- */
549
- readonly unit?: Module<UnitX, never, UnitNeeds>;
550
586
  readonly clock?: Clock;
551
587
  readonly signals?: boolean;
552
588
  /**
@@ -599,15 +635,18 @@ type RunningApp<E, Info = never> = {
599
635
  /**
600
636
  * The phantom marker `start`, `runMain` and `Boot` all intersect onto their
601
637
  * `module` parameter: `unknown` — and invisible — when nothing the module
602
- * needs is unprovided, it exports a runtime, its exports cover what that
603
- * runtime resolves and they cover the unit module's needs; a diagnostic
604
- * otherwise, printed at the call site as the parameter type the argument did
605
- * not match.
638
+ * needs is unprovided, it exports a runtime and its exports cover what that
639
+ * runtime resolves; a diagnostic otherwise, printed at the call site as the
640
+ * parameter type the argument did not match.
606
641
  *
607
642
  * `unknown` is the satisfied arm because intersecting it leaves the module
608
643
  * type untouched. A runtime's `resolves` is checked against the module's
609
- * exports only, never the unit module's: `RuntimeHost.ctx` is the application
610
- * context, so a unit-only port would resolve to nothing there.
644
+ * exports only: `RuntimeHost.ctx` is the application context, so a port only
645
+ * a `fork` provides would resolve to nothing there. A `fork`'s own needs are
646
+ * not this marker's business at all — a `fork` module is forked over the
647
+ * application context, so its needs are exactly what a starter's own `needs`
648
+ * channel already asks the composition root to supply, and di's ordinary
649
+ * `UNSATISFIED DEPENDENCIES` gate is what refuses a root that does not.
611
650
  *
612
651
  * **Unmet needs are checked FIRST, and answered in di's own words rather than
613
652
  * a sentence of the kernel's.** A root that forgot `provides: [router]` used
@@ -626,10 +665,10 @@ type RunningApp<E, Info = never> = {
626
665
  * name is reached.
627
666
  */
628
667
  type PortIdOf<P> = P extends PortInstance<infer Id, infer _Service> ? Id : P;
629
- type StartGate<X, UnitNeeds = never, N = never> = [Exclude<N, Scope | Env>] extends [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" : {
668
+ type StartGate<X, N = never> = [Exclude<N, Scope | Env>] extends [never] ? [Extract<X, RuntimeInstance>] extends [never] ? "NO RUNTIME — the module exports no port declared over RuntimePort" : [InstanceType<RuntimeResolvesOf<X>>] extends [X] ? unknown : "UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export" : {
630
669
  readonly "UNSATISFIED DEPENDENCIES — nothing provides": PortIdOf<Exclude<N, Scope | Env>>;
631
670
  };
632
- declare const start: <X, E, N, UnitX = never, UnitNeeds = never>(module: Module<X, E, N> & StartGate<X, UnitNeeds, N>, options?: StartOptions<UnitX, UnitNeeds>) => RunningApp<E, RuntimeInfoOf<X>>;
671
+ declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions) => RunningApp<E, RuntimeInfoOf<X>>;
633
672
  //#endregion
634
673
  //#region src/run-main.d.ts
635
674
  /**
@@ -664,6 +703,6 @@ declare const start: <X, E, N, UnitX = never, UnitNeeds = never>(module: Module<
664
703
  * await runMain(OrderApi);
665
704
  * ```
666
705
  */
667
- declare const runMain: <X, E, N, UnitX = never, UnitNeeds = never>(module: Module<X, E, N> & StartGate<X, UnitNeeds, N>, options?: StartOptions<UnitX, UnitNeeds>, exit?: (code: number) => void) => Promise<void>;
706
+ declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
668
707
  //#endregion
669
- export { type Attributes, type Clock, type ComponentHealth, type Counter, type DrainReport, type EventSink, type ExitReport, type HealthCheck, HealthCheckFailed, HealthChecks, type HealthReport, type Histogram, type InstrumentationLoader, Instrumentations, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, Observers, type Operation, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Settle, type Settled, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, noObserver, observe, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
708
+ export { type Attributes, type Clock, type ComponentHealth, type Counter, type DrainReport, type EventSink, type ExitReport, type HealthCheck, HealthCheckFailed, HealthChecks, type HealthReport, type Histogram, type InstrumentationLoader, Instrumentations, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, Observers, type Operation, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Settle, type Settled, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitHost, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, noObserver, observe, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock, traceIdOfTraceparent };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AnyPort, Context, Module, PortClass, PortInstance, Scope, ServiceOf } from "@btravstack/di";
1
+ import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
2
2
  import { AsyncResult, Result } from "unthrown";
3
3
  import { Env, Environment } from "@btravstack/config";
4
4
  //#region src/instrumentation.d.ts
@@ -372,11 +372,28 @@ declare class RuntimeStartFailed extends RuntimeStartFailed_base<{
372
372
  * `UnitMeta.id` must be unique per unit unless a `traceId` is supplied — see
373
373
  * {@link UnitMeta}.
374
374
  *
375
- * With a `StartOptions.unit` module in play, `work` runs only once the fork is
376
- * built. A runtime that subscribes to an event from inside `work` must first
377
- * check whether it has already fired.
375
+ * A runtime forks the unit's scope itself, through `UnitHost.fork`, at the
376
+ * moment it holds the unit's input; the kernel closes that scope when the
377
+ * unit settles, after the response is flushed. Work that subscribes to an
378
+ * event after a `fork` must first check whether it already fired.
378
379
  */
379
- type RunUnit<Resolves extends AnyPort> = <T, E>(meta: UnitMeta, work: (ctx: Context<InstanceType<Resolves>>, signal: AbortSignal) => ReturnType<UnitWork<T, E>>) => AsyncResult<T, E>;
380
+ type RunUnit<Resolves extends AnyPort> = <T, E>(meta: UnitMeta, work: (unit: UnitHost<Resolves>, signal: AbortSignal) => ReturnType<UnitWork<T, E>>) => AsyncResult<T, E>;
381
+ /**
382
+ * What the kernel hands a unit's work: the application context, and the one
383
+ * way to open the unit's own scope.
384
+ *
385
+ * `fork` builds `module` over the application context plus `seed` and hands
386
+ * the forked context back. The scope is torn down when the unit closes —
387
+ * inside the registry's unit, so the unit is not counted closed until its
388
+ * finalisers have run, and inside the unit's ambient record, so a teardown
389
+ * log line carries the unit's ids. A construction failure rides the unit's
390
+ * defect path. A unit forks once; a second call is a defect, and so is a call
391
+ * made after the unit has settled — nothing awaits that scope's teardown.
392
+ */
393
+ type UnitHost<Resolves extends AnyPort> = {
394
+ readonly ctx: Context<InstanceType<Resolves>>;
395
+ readonly fork: <UnitX, N, Seeded extends AnyPort = never>(module: Module<UnitX, never, N> & DependencyGate<Exclude<N, InstanceType<Resolves> | InstanceType<Seeded> | Scope>>, seed: readonly SeedEntry<Seeded>[]) => AsyncResult<Context<InstanceType<Resolves> | UnitX | InstanceType<Seeded>>, never>;
396
+ };
380
397
  /**
381
398
  * What a runtime is handed at `start`: the application services **and** the
382
399
  * kernel's {@link RunUnit}. Handing it a bare `Context` would leave every
@@ -388,8 +405,8 @@ type RunUnit<Resolves extends AnyPort> = <T, E>(meta: UnitMeta, work: (ctx: Cont
388
405
  * {@link RunUnit}), and `UnitMeta.id` must be unique per unit unless a
389
406
  * `traceId` is supplied (see {@link UnitMeta}).
390
407
  *
391
- * `ctx` is the **application** context: a port a `StartOptions.unit` module
392
- * provides exists only while a unit is open, so a runtime naming it in
408
+ * `ctx` is the **application** context: a port a unit's `fork` module provides
409
+ * exists only in the `Context` `fork` hands back, so a runtime naming it in
393
410
  * `resolves` is rejected at `start`'s call site.
394
411
  */
395
412
  type RuntimeHost<Resolves extends AnyPort> = {
@@ -430,8 +447,40 @@ declare const RuntimePort: PortClass<"Runtime">;
430
447
  type RuntimeInstance = InstanceType<PortClass<"Runtime">>;
431
448
  /** The `Runtime<Resolves, Info>` a module exports, or `never` when it exports none. */
432
449
  type RuntimeOf<X> = ServiceOf<Extract<X, RuntimeInstance>>;
433
- type RuntimeResolvesOf<X> = RuntimeOf<X> extends Runtime<infer Resolves, unknown> ? Resolves : never;
434
- type RuntimeInfoOf<X> = RuntimeOf<X> extends Runtime<AnyPort, infer Info> ? Info : never;
450
+ type RuntimeResolvesOf<X> = RuntimeOf<X> extends {
451
+ readonly resolves: readonly (infer Resolves extends AnyPort)[];
452
+ } ? Resolves : never;
453
+ type RuntimeInfoOf<X> = RuntimeOf<X> extends {
454
+ readonly start: (host: never) => AsyncResult<Serving<infer Info>, RuntimeStartFailed>;
455
+ } ? Info : never;
456
+ /**
457
+ * The trace id inside a W3C `traceparent` header, and nothing else of it.
458
+ *
459
+ * @remarks
460
+ * The parent's **span id is dropped**, deliberately: `UnitMeta.traceId` is a
461
+ * correlation id rather than a span context, and half-carrying one would
462
+ * suggest a parent-child link nothing here maintains.
463
+ *
464
+ * Three things the specification calls invalid are refused like a malformed
465
+ * header, because adopting one would replace a runtime's own usable id with a
466
+ * value that means nothing: an **all-zero trace id**, an **all-zero parent
467
+ * id** (the header is well-formed and names no span), and **version `ff`**,
468
+ * which is reserved and never a version a caller may send.
469
+ *
470
+ * It is here rather than in a runtime because **every** transport carrying an
471
+ * inbound trace needs the same answer, and two copies of a parser is two
472
+ * places for the all-zero rule to be forgotten. A runtime pairs it with the
473
+ * adopt-only-a-non-blank-inbound-id rule its own headers need:
474
+ * `UnitMeta.traceId` defaults to `meta.id` when it is nullish and `""` is not,
475
+ * so an empty header would hand a caller's every unit the same blank id.
476
+ *
477
+ * @example
478
+ * ```ts
479
+ * const parent = request.headers["traceparent"];
480
+ * const traceId = typeof parent === "string" ? traceIdOfTraceparent(parent) : undefined;
481
+ * ```
482
+ */
483
+ declare const traceIdOfTraceparent: (header: string) => string | undefined;
435
484
  /**
436
485
  * `running`, but no later than the kernel's drain deadline — the primitive a
437
486
  * `Serving.drain` needs when the work it awaits settles on somebody else's
@@ -527,26 +576,13 @@ type ExitReport = {
527
576
  readonly teardownErrors: readonly TeardownError[];
528
577
  readonly uptimeMs: number;
529
578
  };
530
- type StartOptions<UnitX = never, UnitNeeds = never> = {
579
+ type StartOptions = {
531
580
  /**
532
581
  * The environment the graph is configured from, provided to it as the `Env`
533
582
  * port and read for the kernel's own `PROBE_PORT`. Defaults to
534
583
  * `process.env`.
535
584
  */
536
585
  readonly env?: Environment;
537
- /**
538
- * A module forked around **every unit** — a unit being one bounded piece of
539
- * work, whatever the transport calls it: an HTTP request, a Temporal
540
- * activity, an AMQP delivery. Built as the unit opens, torn down as it
541
- * closes — while the unit's ambient record is still open — reading anything
542
- * the application context carries.
543
- *
544
- * A failing unit finaliser is reported as a `teardownError` event and never
545
- * in `ExitReport.teardownErrors`. With this option the unit's work runs only
546
- * once the fork is built, so a runtime that subscribes to an event from
547
- * inside its work must be ready for it to have already fired.
548
- */
549
- readonly unit?: Module<UnitX, never, UnitNeeds>;
550
586
  readonly clock?: Clock;
551
587
  readonly signals?: boolean;
552
588
  /**
@@ -599,15 +635,18 @@ type RunningApp<E, Info = never> = {
599
635
  /**
600
636
  * The phantom marker `start`, `runMain` and `Boot` all intersect onto their
601
637
  * `module` parameter: `unknown` — and invisible — when nothing the module
602
- * needs is unprovided, it exports a runtime, its exports cover what that
603
- * runtime resolves and they cover the unit module's needs; a diagnostic
604
- * otherwise, printed at the call site as the parameter type the argument did
605
- * not match.
638
+ * needs is unprovided, it exports a runtime and its exports cover what that
639
+ * runtime resolves; a diagnostic otherwise, printed at the call site as the
640
+ * parameter type the argument did not match.
606
641
  *
607
642
  * `unknown` is the satisfied arm because intersecting it leaves the module
608
643
  * type untouched. A runtime's `resolves` is checked against the module's
609
- * exports only, never the unit module's: `RuntimeHost.ctx` is the application
610
- * context, so a unit-only port would resolve to nothing there.
644
+ * exports only: `RuntimeHost.ctx` is the application context, so a port only
645
+ * a `fork` provides would resolve to nothing there. A `fork`'s own needs are
646
+ * not this marker's business at all — a `fork` module is forked over the
647
+ * application context, so its needs are exactly what a starter's own `needs`
648
+ * channel already asks the composition root to supply, and di's ordinary
649
+ * `UNSATISFIED DEPENDENCIES` gate is what refuses a root that does not.
611
650
  *
612
651
  * **Unmet needs are checked FIRST, and answered in di's own words rather than
613
652
  * a sentence of the kernel's.** A root that forgot `provides: [router]` used
@@ -626,10 +665,10 @@ type RunningApp<E, Info = never> = {
626
665
  * name is reached.
627
666
  */
628
667
  type PortIdOf<P> = P extends PortInstance<infer Id, infer _Service> ? Id : P;
629
- type StartGate<X, UnitNeeds = never, N = never> = [Exclude<N, Scope | Env>] extends [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" : {
668
+ type StartGate<X, N = never> = [Exclude<N, Scope | Env>] extends [never] ? [Extract<X, RuntimeInstance>] extends [never] ? "NO RUNTIME — the module exports no port declared over RuntimePort" : [InstanceType<RuntimeResolvesOf<X>>] extends [X] ? unknown : "UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export" : {
630
669
  readonly "UNSATISFIED DEPENDENCIES — nothing provides": PortIdOf<Exclude<N, Scope | Env>>;
631
670
  };
632
- declare const start: <X, E, N, UnitX = never, UnitNeeds = never>(module: Module<X, E, N> & StartGate<X, UnitNeeds, N>, options?: StartOptions<UnitX, UnitNeeds>) => RunningApp<E, RuntimeInfoOf<X>>;
671
+ declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions) => RunningApp<E, RuntimeInfoOf<X>>;
633
672
  //#endregion
634
673
  //#region src/run-main.d.ts
635
674
  /**
@@ -664,6 +703,6 @@ declare const start: <X, E, N, UnitX = never, UnitNeeds = never>(module: Module<
664
703
  * await runMain(OrderApi);
665
704
  * ```
666
705
  */
667
- declare const runMain: <X, E, N, UnitX = never, UnitNeeds = never>(module: Module<X, E, N> & StartGate<X, UnitNeeds, N>, options?: StartOptions<UnitX, UnitNeeds>, exit?: (code: number) => void) => Promise<void>;
706
+ declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
668
707
  //#endregion
669
- export { type Attributes, type Clock, type ComponentHealth, type Counter, type DrainReport, type EventSink, type ExitReport, type HealthCheck, HealthCheckFailed, HealthChecks, type HealthReport, type Histogram, type InstrumentationLoader, Instrumentations, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, Observers, type Operation, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Settle, type Settled, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, noObserver, observe, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
708
+ export { type Attributes, type Clock, type ComponentHealth, type Counter, type DrainReport, type EventSink, type ExitReport, type HealthCheck, HealthCheckFailed, HealthChecks, type HealthReport, type Histogram, type InstrumentationLoader, Instrumentations, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, Observers, type Operation, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Settle, type Settled, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitHost, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, noObserver, observe, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock, traceIdOfTraceparent };
package/dist/index.mjs CHANGED
@@ -205,6 +205,39 @@ var RuntimeStartFailed = class extends TaggedError("RuntimeStartFailed") {
205
205
  */
206
206
  const RuntimePort = Port("Runtime");
207
207
  /**
208
+ * The trace id inside a W3C `traceparent` header, and nothing else of it.
209
+ *
210
+ * @remarks
211
+ * The parent's **span id is dropped**, deliberately: `UnitMeta.traceId` is a
212
+ * correlation id rather than a span context, and half-carrying one would
213
+ * suggest a parent-child link nothing here maintains.
214
+ *
215
+ * Three things the specification calls invalid are refused like a malformed
216
+ * header, because adopting one would replace a runtime's own usable id with a
217
+ * value that means nothing: an **all-zero trace id**, an **all-zero parent
218
+ * id** (the header is well-formed and names no span), and **version `ff`**,
219
+ * which is reserved and never a version a caller may send.
220
+ *
221
+ * It is here rather than in a runtime because **every** transport carrying an
222
+ * inbound trace needs the same answer, and two copies of a parser is two
223
+ * places for the all-zero rule to be forgotten. A runtime pairs it with the
224
+ * adopt-only-a-non-blank-inbound-id rule its own headers need:
225
+ * `UnitMeta.traceId` defaults to `meta.id` when it is nullish and `""` is not,
226
+ * so an empty header would hand a caller's every unit the same blank id.
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * const parent = request.headers["traceparent"];
231
+ * const traceId = typeof parent === "string" ? traceIdOfTraceparent(parent) : undefined;
232
+ * ```
233
+ */
234
+ const traceIdOfTraceparent = (header) => {
235
+ const [, version, traceId, parentId, extra] = /^(?!ff)([\da-f]{2})-([\da-f]{32})-([\da-f]{16})-[\da-f]{2}(-.*)?$/.exec(header.trim()) ?? [];
236
+ if (traceId === void 0 || parentId === void 0) return void 0;
237
+ if (version === "00" && extra !== void 0) return void 0;
238
+ return /^0+$/.test(traceId) || /^0+$/.test(parentId) ? void 0 : traceId;
239
+ };
240
+ /**
208
241
  * `running`, but no later than the kernel's drain deadline — the primitive a
209
242
  * `Serving.drain` needs when the work it awaits settles on somebody else's
210
243
  * clock (Temporal's `shutdownForceTime`, a broker library's close) and so
@@ -579,15 +612,43 @@ const start = (module, options = {}) => {
579
612
  runtimeName = runtime.name;
580
613
  healthChecks = ctx.get(HealthChecks);
581
614
  const runtimeCtx = ctx;
582
- const unit = options.unit;
583
615
  const run = (meta, work) => registry.run(meta, (signal) => {
584
- if (unit === void 0) return work(runtimeCtx, signal);
585
- const fork = Module.forkScope;
586
- return fork(ctx, unit, (forked) => fromSafePromise((async () => await work(forked, signal))()).flatMap((result) => result), { onTeardownError: (port, cause) => emit({
587
- type: "teardownError",
588
- port,
589
- cause
590
- }) });
616
+ const settled = Promise.withResolvers();
617
+ let closing;
618
+ let hasSettled = false;
619
+ const fork = (module, seed) => {
620
+ if (hasSettled) return fromSafePromise(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
621
+ if (closing !== void 0) return fromSafePromise(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
622
+ const ready = Promise.withResolvers();
623
+ closing = Module.forkScope(ctx, module, (forked) => {
624
+ ready.resolve(forked);
625
+ return fromSafePromise(settled.promise);
626
+ }, {
627
+ seed,
628
+ onTeardownError: (port, cause) => emit({
629
+ type: "teardownError",
630
+ port,
631
+ cause
632
+ })
633
+ }).recoverDefect((cause) => {
634
+ ready.reject(cause);
635
+ return Ok();
636
+ }).get();
637
+ return fromSafePromise(ready.promise);
638
+ };
639
+ const outcome = (async () => {
640
+ try {
641
+ return await work({
642
+ ctx: runtimeCtx,
643
+ fork
644
+ }, signal);
645
+ } finally {
646
+ hasSettled = true;
647
+ settled.resolve();
648
+ if (closing !== void 0) await closing;
649
+ }
650
+ })();
651
+ return fromSafePromise(outcome).flatMap((result) => result);
591
652
  });
592
653
  const host = {
593
654
  ctx: runtimeCtx,
@@ -688,4 +749,4 @@ const runMain = async (module, options = {}, exit = (code) => {
688
749
  await awaitExit(start(module, options), exit);
689
750
  };
690
751
  //#endregion
691
- export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, Observers, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, noObserver, observe, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
752
+ export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, Observers, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, noObserver, observe, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock, traceIdOfTraceparent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/core",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "The application kernel: boot a di module into a running process, and stop it without losing work",
5
5
  "keywords": [
6
6
  "application",
@@ -45,7 +45,7 @@
45
45
  "./package.json": "./package.json"
46
46
  },
47
47
  "devDependencies": {
48
- "@btravstack/tsconfig": "0.2.0",
48
+ "@btravstack/tsconfig": "0.4.0",
49
49
  "@types/node": "26.4.0",
50
50
  "@unthrown/vitest": "5.7.0",
51
51
  "@vitest/coverage-v8": "4.1.11",
@@ -53,13 +53,13 @@
53
53
  "typescript": "7.0.2",
54
54
  "unthrown": "5.7.0",
55
55
  "vitest": "4.1.11",
56
- "@btravstack/di": "0.11.0",
57
- "@btravstack/config": "0.11.0"
56
+ "@btravstack/config": "0.13.0",
57
+ "@btravstack/di": "0.13.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "unthrown": "^5.0.0",
61
- "@btravstack/config": "^0.11.0",
62
- "@btravstack/di": "^0.11.0"
61
+ "@btravstack/config": "^0.13.0",
62
+ "@btravstack/di": "^0.13.0"
63
63
  },
64
64
  "engines": {
65
65
  "node": ">=22"