@btravstack/core 0.9.0 → 0.11.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
@@ -132,7 +132,7 @@ is a module that exports no runtime port at all.
132
132
  - **A per-unit scope no handler manages** — pass a module as
133
133
  `StartOptions.unit` and the kernel forks it around every unit.
134
134
  - **An ambient record, not an ambient container** — `currentUnit()` reads
135
- `{ unitId, traceId, tenantId, deadline, signal }`; services never travel
135
+ `{ unitId, traceId, tenantId, signal }`; services never travel
136
136
  there. `signal` is the very `AbortSignal` the unit's work callback is handed,
137
137
  so a runtime whose work is a library's `next()` — a Temporal activity, an
138
138
  AMQP delivery — can still honour the drain deadline.
package/dist/index.cjs CHANGED
@@ -61,6 +61,50 @@ const runHealthChecks = (checks) => (0, unthrown.allAsync)(checks.map((health) =
61
61
  components
62
62
  }));
63
63
  //#endregion
64
+ //#region src/observation.ts
65
+ /**
66
+ * The set port a starter contributes its observability to, and reads whole.
67
+ *
68
+ * **Called at the START and answering a finisher**, rather than told about a
69
+ * finished operation. That is what lets an observer open a span before the work
70
+ * and end it after — a span reconstructed afterwards from a duration is not the
71
+ * parent of anything that ran inside it.
72
+ *
73
+ * A set port rather than a `Meter` in every starter's `needs`: an observer
74
+ * package an application never composed contributes nothing, and a starter
75
+ * costs a graph no ports to have observability available. That is the
76
+ * difference between "instrumented by default" and "instrumented if you asked
77
+ * for observability", and only the second is free.
78
+ *
79
+ * **A module that reads this port contributes a no-op member of its own**, the
80
+ * way `otel()` does for `Instrumentations`: a collector depending on a set port
81
+ * NOTHING provides is an unmet dependency, at plan time and in `Needs` alike.
82
+ * Several no-ops in one graph cost a call each and nothing else.
83
+ */
84
+ var Observers = class extends _btravstack_di.Port.many("Observers") {};
85
+ /** The no-op member every reader of {@link Observers} contributes, so the set is never empty. */
86
+ const noObserver = () => () => {};
87
+ /**
88
+ * Run every observer around one operation, and hand back the one finisher that
89
+ * settles them all.
90
+ *
91
+ * Observers are started in order and settled in order, and the finisher is
92
+ * **guarded**: a second call is dropped. "Called exactly once" is therefore a
93
+ * property of this function rather than a rule each starter re-keeps — which
94
+ * matters because the settling call sites are error paths, where a `tap` and a
95
+ * `tapFailure` on the same chain, or a retry, is exactly the shape that fires
96
+ * twice and doubles a failure count.
97
+ */
98
+ const observe = (observers, operation) => {
99
+ const settlers = observers.map((observer) => observer(operation));
100
+ let done = false;
101
+ return (settled) => {
102
+ if (done) return;
103
+ done = true;
104
+ for (const settle of settlers) settle(settled);
105
+ };
106
+ };
107
+ //#endregion
64
108
  //#region src/clock.ts
65
109
  const systemClock = {
66
110
  now: () => Date.now(),
@@ -342,7 +386,6 @@ const createUnitRegistry = () => {
342
386
  unitId: nextId(),
343
387
  traceId: meta.traceId ?? meta.id,
344
388
  tenantId: meta.tenantId,
345
- deadline: meta.deadline,
346
389
  signal: controller.signal
347
390
  };
348
391
  return (0, unthrown.fromSafePromise)(runWithUnit(record, async () => {
@@ -652,11 +695,14 @@ exports.Instrumentations = Instrumentations;
652
695
  exports.LEVELS = LEVELS;
653
696
  exports.Logger = Logger;
654
697
  exports.Meter = Meter;
698
+ exports.Observers = Observers;
655
699
  exports.RuntimePort = RuntimePort;
656
700
  exports.RuntimeStartFailed = RuntimeStartFailed;
657
701
  exports.SPAN_STATUS = SPAN_STATUS;
658
702
  exports.Tracer = Tracer;
659
703
  exports.currentUnit = currentUnit;
704
+ exports.noObserver = noObserver;
705
+ exports.observe = observe;
660
706
  exports.releasedBy = releasedBy;
661
707
  exports.runHealthChecks = runHealthChecks;
662
708
  exports.runMain = runMain;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AsyncResult, Result } from "unthrown";
2
- import { AnyPort, Context, Module, PortClass, Scope, ServiceOf } from "@btravstack/di";
2
+ import { AnyPort, Context, Module, PortClass, PortInstance, Scope, ServiceOf } from "@btravstack/di";
3
3
  import { Env, Environment } from "@btravstack/config";
4
4
  //#region src/instrumentation.d.ts
5
5
  /**
@@ -83,6 +83,216 @@ type HealthReport = {
83
83
  */
84
84
  declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
85
85
  //#endregion
86
+ //#region src/observability.d.ts
87
+ /**
88
+ * The severity of one line, and the whole of the set: six levels, ordered, with
89
+ * no caller-defined additions — which is what lets `LOG_LEVEL` be validated at
90
+ * startup and `isEnabled` be a comparison rather than a lookup.
91
+ */
92
+ type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
93
+ /** The levels in order, least severe first — what `isEnabled` compares through. */
94
+ declare const LEVELS: readonly Level[];
95
+ /**
96
+ * What a line, a span or a measurement carries besides itself: a flat record of
97
+ * scalars.
98
+ *
99
+ * Flat and scalar on purpose. A nested object is where a field's name stops
100
+ * being stable, and an `unknown` value is where a logger starts stringifying
101
+ * whatever it is handed — which is how a log call becomes the thing that
102
+ * throws. A failure has a channel of its own, `cause`.
103
+ */
104
+ type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;
105
+ /**
106
+ * Every method takes the same three arguments in the same order:
107
+ * `(message, attributes?, cause?)`.
108
+ *
109
+ * Uniform on purpose. A failure is not a property of severity — an `info` line
110
+ * reporting a recovered fault carries one too — so every level can take a
111
+ * `cause`, at the cost of `logger.error("boom", undefined, cause)` for a
112
+ * failure with nothing else to say.
113
+ */
114
+ type LoggerService = {
115
+ readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
116
+ readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
117
+ readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
118
+ readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
119
+ readonly warn: (message: string, attributes?: Attributes, cause?: unknown) => void;
120
+ /** `cause` is the failure itself — an `Error`, an `unthrown` `Err`'s error, a rejected value. */
121
+ readonly error: (message: string, attributes?: Attributes, cause?: unknown) => void;
122
+ readonly fatal: (message: string, attributes?: Attributes, cause?: unknown) => void;
123
+ /** A logger carrying `attributes` on every line it writes, on top of this one's. Never mutates this one. */
124
+ readonly with: (attributes: Attributes) => LoggerService;
125
+ /** Whether a line at `level` would be written — for a payload expensive enough to be worth not building. */
126
+ readonly isEnabled: (level: Level) => boolean;
127
+ };
128
+ declare const Logger_base: import("@btravstack/di").PortClass<"Logger">;
129
+ /**
130
+ * The application's logger, as a port — declared here and implemented in
131
+ * `@btravstack/observability`, because a contract every package may depend on
132
+ * has to be reachable without installing an implementation, and because the
133
+ * correlation an implementation stamps per line is the kernel's own ambient
134
+ * record.
135
+ *
136
+ * An implementation must not throw: a logger that throws turns an observability
137
+ * problem into an outage. Synchronous `void`, not an `AsyncResult` — thesis 6's
138
+ * one deliberate exemption, since a lost line is not a modeled error.
139
+ */
140
+ declare class Logger extends Logger_base<LoggerService> {}
141
+ /** A span's outcome: unset until something says otherwise, then `ok` or `error`. */
142
+ declare const SPAN_STATUS: {
143
+ readonly unset: 0;
144
+ readonly ok: 1;
145
+ readonly error: 2;
146
+ };
147
+ /** The three codes {@link SPAN_STATUS} names, as the type a {@link Span} takes. */
148
+ type SpanStatusCode = (typeof SPAN_STATUS)[keyof typeof SPAN_STATUS];
149
+ /**
150
+ * One span, narrowed to what a framework package does with it: label it, say
151
+ * how it ended, end it. The numbers are OpenTelemetry's own, so an OTel span
152
+ * satisfies this structurally with no translation in between.
153
+ */
154
+ type Span = {
155
+ readonly setAttributes: (attributes: Attributes) => unknown;
156
+ readonly setStatus: (status: {
157
+ readonly code: SpanStatusCode;
158
+ readonly message?: string;
159
+ }) => unknown;
160
+ readonly end: () => void;
161
+ };
162
+ /** What a {@link Tracer} does: start a span, by name. */
163
+ type TracerService = {
164
+ readonly startSpan: (name: string) => Span;
165
+ };
166
+ declare const Tracer_base: import("@btravstack/di").PortClass<"Tracer">;
167
+ /**
168
+ * The application's tracer, as a port — declared without naming OpenTelemetry,
169
+ * because a port typed as a vendor's type points the dependency arrow outwards
170
+ * and makes the whole family install that vendor.
171
+ */
172
+ declare class Tracer extends Tracer_base<TracerService> {}
173
+ /** A monotonic count. */
174
+ type Counter = {
175
+ readonly add: (value: number, attributes?: Attributes) => void;
176
+ };
177
+ /** A distribution of measurements. */
178
+ type Histogram = {
179
+ readonly record: (value: number, attributes?: Attributes) => void;
180
+ };
181
+ /**
182
+ * What a {@link Meter} does: mint the two instruments a framework package needs
183
+ * — it counts what happened and measures how long it took. A gauge is something
184
+ * an application declares about its own domain, reaching the vendor's meter for
185
+ * it as it would any other adapter.
186
+ */
187
+ type MeterService = {
188
+ readonly createCounter: (name: string, options?: {
189
+ readonly description?: string;
190
+ readonly unit?: string;
191
+ }) => Counter;
192
+ readonly createHistogram: (name: string, options?: {
193
+ readonly description?: string;
194
+ readonly unit?: string;
195
+ }) => Histogram;
196
+ };
197
+ declare const Meter_base: import("@btravstack/di").PortClass<"Meter">;
198
+ /** The application's meter, as a port. See {@link Tracer} for why it does not name a vendor. */
199
+ declare class Meter extends Meter_base<MeterService> {}
200
+ //#endregion
201
+ //#region src/observation.d.ts
202
+ /**
203
+ * One operation worth observing, named before it runs.
204
+ *
205
+ * `component` is the starter's own word for itself — `"http"`, `"cache"`,
206
+ * `"amqp"` — and it is what lets an observer mint per-component instrument and
207
+ * span names without knowing anything about the component. `name` is the
208
+ * operation within it (`"get"`, `"unit"`), and `attributes` are the dimensions
209
+ * known before the call.
210
+ *
211
+ * The dimensions are the contributor's business and their **cardinality is its
212
+ * responsibility**: a request path or a workflow id here mints a time series
213
+ * per value, which is how a metrics bill becomes the incident.
214
+ */
215
+ type Operation = {
216
+ readonly component: string;
217
+ readonly name: string;
218
+ /**
219
+ * The DIMENSIONS: bounded, and what a metrics observer may put on an
220
+ * instrument. A cache operation, an HTTP method, an activity type.
221
+ *
222
+ * Their cardinality is the contributor's responsibility — a request path or a
223
+ * cache key here mints a time series per value, which is how a metrics bill
224
+ * becomes the incident. That is what {@link Operation.details} is for.
225
+ */
226
+ readonly attributes: Attributes;
227
+ /**
228
+ * Unbounded context: a cache key, a mail subject, a URL. It rides spans and
229
+ * log lines, where one more field costs one more field — and **never a
230
+ * metric**, where it would cost a time series per value.
231
+ *
232
+ * The split is the whole reason an observer can be shared: without it every
233
+ * contributor would have to choose between a useful span and a safe metric.
234
+ */
235
+ readonly details?: Attributes;
236
+ /**
237
+ * Whether a tracing observer should open a span (default `true`).
238
+ *
239
+ * `false` is for a component whose spans already come from somewhere better:
240
+ * `@btravstack/prisma` says so because `@prisma/instrumentation` traces at the
241
+ * ENGINE level — the real SQL, the connection acquisition — all of it below
242
+ * what a client-level wrapper can see, so a second span would cost one more
243
+ * per query for strictly less information. Counting and timing still happen.
244
+ *
245
+ * Whether a span is worth opening is the CONTRIBUTOR's knowledge, which is
246
+ * why it rides the operation rather than being configured on the observer.
247
+ */
248
+ readonly traced?: boolean;
249
+ };
250
+ /** How an operation came out, plus whatever was only knowable at the end. */
251
+ type Settled = {
252
+ readonly outcome: "ok" | "error";
253
+ /** Dimensions known only at the end, merged over the operation's own — an HTTP status, a cache hit or miss. */
254
+ readonly attributes?: Attributes;
255
+ /** The failure itself, for an observer that writes a line about it. */
256
+ readonly cause?: unknown;
257
+ };
258
+ /** What an observer answers: a finisher, called exactly once when the operation settles. */
259
+ type Settle = (settled: Settled) => void;
260
+ declare const Observers_base: import("@btravstack/di").ManyPortClass<"Observers">;
261
+ /**
262
+ * The set port a starter contributes its observability to, and reads whole.
263
+ *
264
+ * **Called at the START and answering a finisher**, rather than told about a
265
+ * finished operation. That is what lets an observer open a span before the work
266
+ * and end it after — a span reconstructed afterwards from a duration is not the
267
+ * parent of anything that ran inside it.
268
+ *
269
+ * A set port rather than a `Meter` in every starter's `needs`: an observer
270
+ * package an application never composed contributes nothing, and a starter
271
+ * costs a graph no ports to have observability available. That is the
272
+ * difference between "instrumented by default" and "instrumented if you asked
273
+ * for observability", and only the second is free.
274
+ *
275
+ * **A module that reads this port contributes a no-op member of its own**, the
276
+ * way `otel()` does for `Instrumentations`: a collector depending on a set port
277
+ * NOTHING provides is an unmet dependency, at plan time and in `Needs` alike.
278
+ * Several no-ops in one graph cost a call each and nothing else.
279
+ */
280
+ declare class Observers extends Observers_base<(operation: Operation) => Settle> {}
281
+ /** The no-op member every reader of {@link Observers} contributes, so the set is never empty. */
282
+ declare const noObserver: () => Settle;
283
+ /**
284
+ * Run every observer around one operation, and hand back the one finisher that
285
+ * settles them all.
286
+ *
287
+ * Observers are started in order and settled in order, and the finisher is
288
+ * **guarded**: a second call is dropped. "Called exactly once" is therefore a
289
+ * property of this function rather than a rule each starter re-keeps — which
290
+ * matters because the settling call sites are error paths, where a `tap` and a
291
+ * `tapFailure` on the same chain, or a retry, is exactly the shape that fires
292
+ * twice and doubles a failure count.
293
+ */
294
+ declare const observe: (observers: readonly ((operation: Operation) => Settle)[], operation: Operation) => Settle;
295
+ //#endregion
86
296
  //#region src/clock.d.ts
87
297
  type Clock = {
88
298
  readonly now: () => number;
@@ -105,7 +315,6 @@ type UnitRecord = {
105
315
  readonly unitId: string;
106
316
  readonly traceId: string;
107
317
  readonly tenantId: string | undefined;
108
- readonly deadline: number | undefined;
109
318
  readonly signal: AbortSignal;
110
319
  };
111
320
  declare const currentUnit: () => UnitRecord | undefined;
@@ -129,7 +338,6 @@ type UnitMeta = {
129
338
  readonly id: string;
130
339
  readonly traceId?: string;
131
340
  readonly tenantId?: string;
132
- readonly deadline?: number;
133
341
  };
134
342
  type UnitWork<T, E> = (signal: AbortSignal) => AsyncResult<T, E> | Promise<Result<T, E>> | Result<T, E>;
135
343
  type UnitRegistry = {
@@ -305,121 +513,6 @@ type KernelEvent = {
305
513
  type EventSink = (event: KernelEvent) => void;
306
514
  declare const stderrSink: EventSink;
307
515
  //#endregion
308
- //#region src/observability.d.ts
309
- /**
310
- * The severity of one line, and the whole of the set: six levels, ordered, with
311
- * no caller-defined additions — which is what lets `LOG_LEVEL` be validated at
312
- * startup and `isEnabled` be a comparison rather than a lookup.
313
- */
314
- type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
315
- /** The levels in order, least severe first — what `isEnabled` compares through. */
316
- declare const LEVELS: readonly Level[];
317
- /**
318
- * What a line, a span or a measurement carries besides itself: a flat record of
319
- * scalars.
320
- *
321
- * Flat and scalar on purpose. A nested object is where a field's name stops
322
- * being stable, and an `unknown` value is where a logger starts stringifying
323
- * whatever it is handed — which is how a log call becomes the thing that
324
- * throws. A failure has a channel of its own, `cause`.
325
- */
326
- type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;
327
- /**
328
- * Every method takes the same three arguments in the same order:
329
- * `(message, attributes?, cause?)`.
330
- *
331
- * Uniform on purpose. A failure is not a property of severity — an `info` line
332
- * reporting a recovered fault carries one too — so every level can take a
333
- * `cause`, at the cost of `logger.error("boom", undefined, cause)` for a
334
- * failure with nothing else to say.
335
- */
336
- type LoggerService = {
337
- readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
338
- readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
339
- readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
340
- readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
341
- readonly warn: (message: string, attributes?: Attributes, cause?: unknown) => void;
342
- /** `cause` is the failure itself — an `Error`, an `unthrown` `Err`'s error, a rejected value. */
343
- readonly error: (message: string, attributes?: Attributes, cause?: unknown) => void;
344
- readonly fatal: (message: string, attributes?: Attributes, cause?: unknown) => void;
345
- /** A logger carrying `attributes` on every line it writes, on top of this one's. Never mutates this one. */
346
- readonly with: (attributes: Attributes) => LoggerService;
347
- /** Whether a line at `level` would be written — for a payload expensive enough to be worth not building. */
348
- readonly isEnabled: (level: Level) => boolean;
349
- };
350
- declare const Logger_base: import("@btravstack/di").PortClass<"Logger">;
351
- /**
352
- * The application's logger, as a port — declared here and implemented in
353
- * `@btravstack/observability`, because a contract every package may depend on
354
- * has to be reachable without installing an implementation, and because the
355
- * correlation an implementation stamps per line is the kernel's own ambient
356
- * record.
357
- *
358
- * An implementation must not throw: a logger that throws turns an observability
359
- * problem into an outage. Synchronous `void`, not an `AsyncResult` — thesis 6's
360
- * one deliberate exemption, since a lost line is not a modeled error.
361
- */
362
- declare class Logger extends Logger_base<LoggerService> {}
363
- /** A span's outcome: unset until something says otherwise, then `ok` or `error`. */
364
- declare const SPAN_STATUS: {
365
- readonly unset: 0;
366
- readonly ok: 1;
367
- readonly error: 2;
368
- };
369
- /** The three codes {@link SPAN_STATUS} names, as the type a {@link Span} takes. */
370
- type SpanStatusCode = (typeof SPAN_STATUS)[keyof typeof SPAN_STATUS];
371
- /**
372
- * One span, narrowed to what a framework package does with it: label it, say
373
- * how it ended, end it. The numbers are OpenTelemetry's own, so an OTel span
374
- * satisfies this structurally with no translation in between.
375
- */
376
- type Span = {
377
- readonly setAttributes: (attributes: Attributes) => unknown;
378
- readonly setStatus: (status: {
379
- readonly code: SpanStatusCode;
380
- readonly message?: string;
381
- }) => unknown;
382
- readonly end: () => void;
383
- };
384
- /** What a {@link Tracer} does: start a span, by name. */
385
- type TracerService = {
386
- readonly startSpan: (name: string) => Span;
387
- };
388
- declare const Tracer_base: import("@btravstack/di").PortClass<"Tracer">;
389
- /**
390
- * The application's tracer, as a port — declared without naming OpenTelemetry,
391
- * because a port typed as a vendor's type points the dependency arrow outwards
392
- * and makes the whole family install that vendor.
393
- */
394
- declare class Tracer extends Tracer_base<TracerService> {}
395
- /** A monotonic count. */
396
- type Counter = {
397
- readonly add: (value: number, attributes?: Attributes) => void;
398
- };
399
- /** A distribution of measurements. */
400
- type Histogram = {
401
- readonly record: (value: number, attributes?: Attributes) => void;
402
- };
403
- /**
404
- * What a {@link Meter} does: mint the two instruments a framework package needs
405
- * — it counts what happened and measures how long it took. A gauge is something
406
- * an application declares about its own domain, reaching the vendor's meter for
407
- * it as it would any other adapter.
408
- */
409
- type MeterService = {
410
- readonly createCounter: (name: string, options?: {
411
- readonly description?: string;
412
- readonly unit?: string;
413
- }) => Counter;
414
- readonly createHistogram: (name: string, options?: {
415
- readonly description?: string;
416
- readonly unit?: string;
417
- }) => Histogram;
418
- };
419
- declare const Meter_base: import("@btravstack/di").PortClass<"Meter">;
420
- /** The application's meter, as a port. See {@link Tracer} for why it does not name a vendor. */
421
- declare class Meter extends Meter_base<MeterService> {}
422
- //#endregion
423
516
  //#region src/phase.d.ts
424
517
  type Phase = "building" | "starting" | "serving" | "draining" | "stopping" | "exited";
425
518
  //#endregion
@@ -505,18 +598,38 @@ type RunningApp<E, Info = never> = {
505
598
  };
506
599
  /**
507
600
  * The phantom marker `start`, `runMain` and `Boot` all intersect onto their
508
- * `module` parameter: `unknown` — and invisible — when the module exports a
509
- * runtime, its exports cover what that runtime resolves and they cover the
510
- * unit module's needs; one of three sentences otherwise, printed at the call
511
- * site as the parameter type the argument did not match.
601
+ * `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.
512
606
  *
513
607
  * `unknown` is the satisfied arm because intersecting it leaves the module
514
608
  * type untouched. A runtime's `resolves` is checked against the module's
515
609
  * exports only, never the unit module's: `RuntimeHost.ctx` is the application
516
610
  * context, so a unit-only port would resolve to nothing there.
611
+ *
612
+ * **Unmet needs are checked FIRST, and answered in di's own words rather than
613
+ * a sentence of the kernel's.** A root that forgot `provides: [router]` used
614
+ * to be told `Type 'HttpRouterPort' is not assignable to type 'Scope'` — an
615
+ * internal phantom nobody has heard of — or, when the port was missing from
616
+ * `exports` too, `UNSATISFIED RUNTIME PORTS`, which is a correct diagnosis of
617
+ * the second mistake that reads as a wrong one of the first and steers the fix
618
+ * into the wrong list. It is the same defect `Module.build` reports, so it
619
+ * prints the same sentence and ends on the port that is missing.
620
+ */
621
+ /**
622
+ * A port's declared id, which is what a marker names it by: an id is the short
623
+ * string the application wrote in `Port("…")`, where the port TYPE of a
624
+ * starter's own generic port — `AmqpHandlers` over a contract — expands to
625
+ * hundreds of characters of the caller's own schema and truncates before the
626
+ * name is reached.
517
627
  */
518
- 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";
519
- 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>>;
628
+ 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" : {
630
+ readonly "UNSATISFIED DEPENDENCIES — nothing provides": PortIdOf<Exclude<N, Scope | Env>>;
631
+ };
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>>;
520
633
  //#endregion
521
634
  //#region src/run-main.d.ts
522
635
  /**
@@ -551,6 +664,6 @@ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X,
551
664
  * await runMain(OrderApi);
552
665
  * ```
553
666
  */
554
- 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>;
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>;
555
668
  //#endregion
556
- 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, 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, runHealthChecks, runMain, start, stderrSink, systemClock };
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 };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AnyPort, Context, Module, PortClass, Scope, ServiceOf } from "@btravstack/di";
1
+ import { AnyPort, Context, Module, PortClass, PortInstance, Scope, 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
@@ -83,6 +83,216 @@ type HealthReport = {
83
83
  */
84
84
  declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
85
85
  //#endregion
86
+ //#region src/observability.d.ts
87
+ /**
88
+ * The severity of one line, and the whole of the set: six levels, ordered, with
89
+ * no caller-defined additions — which is what lets `LOG_LEVEL` be validated at
90
+ * startup and `isEnabled` be a comparison rather than a lookup.
91
+ */
92
+ type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
93
+ /** The levels in order, least severe first — what `isEnabled` compares through. */
94
+ declare const LEVELS: readonly Level[];
95
+ /**
96
+ * What a line, a span or a measurement carries besides itself: a flat record of
97
+ * scalars.
98
+ *
99
+ * Flat and scalar on purpose. A nested object is where a field's name stops
100
+ * being stable, and an `unknown` value is where a logger starts stringifying
101
+ * whatever it is handed — which is how a log call becomes the thing that
102
+ * throws. A failure has a channel of its own, `cause`.
103
+ */
104
+ type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;
105
+ /**
106
+ * Every method takes the same three arguments in the same order:
107
+ * `(message, attributes?, cause?)`.
108
+ *
109
+ * Uniform on purpose. A failure is not a property of severity — an `info` line
110
+ * reporting a recovered fault carries one too — so every level can take a
111
+ * `cause`, at the cost of `logger.error("boom", undefined, cause)` for a
112
+ * failure with nothing else to say.
113
+ */
114
+ type LoggerService = {
115
+ readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
116
+ readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
117
+ readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
118
+ readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
119
+ readonly warn: (message: string, attributes?: Attributes, cause?: unknown) => void;
120
+ /** `cause` is the failure itself — an `Error`, an `unthrown` `Err`'s error, a rejected value. */
121
+ readonly error: (message: string, attributes?: Attributes, cause?: unknown) => void;
122
+ readonly fatal: (message: string, attributes?: Attributes, cause?: unknown) => void;
123
+ /** A logger carrying `attributes` on every line it writes, on top of this one's. Never mutates this one. */
124
+ readonly with: (attributes: Attributes) => LoggerService;
125
+ /** Whether a line at `level` would be written — for a payload expensive enough to be worth not building. */
126
+ readonly isEnabled: (level: Level) => boolean;
127
+ };
128
+ declare const Logger_base: import("@btravstack/di").PortClass<"Logger">;
129
+ /**
130
+ * The application's logger, as a port — declared here and implemented in
131
+ * `@btravstack/observability`, because a contract every package may depend on
132
+ * has to be reachable without installing an implementation, and because the
133
+ * correlation an implementation stamps per line is the kernel's own ambient
134
+ * record.
135
+ *
136
+ * An implementation must not throw: a logger that throws turns an observability
137
+ * problem into an outage. Synchronous `void`, not an `AsyncResult` — thesis 6's
138
+ * one deliberate exemption, since a lost line is not a modeled error.
139
+ */
140
+ declare class Logger extends Logger_base<LoggerService> {}
141
+ /** A span's outcome: unset until something says otherwise, then `ok` or `error`. */
142
+ declare const SPAN_STATUS: {
143
+ readonly unset: 0;
144
+ readonly ok: 1;
145
+ readonly error: 2;
146
+ };
147
+ /** The three codes {@link SPAN_STATUS} names, as the type a {@link Span} takes. */
148
+ type SpanStatusCode = (typeof SPAN_STATUS)[keyof typeof SPAN_STATUS];
149
+ /**
150
+ * One span, narrowed to what a framework package does with it: label it, say
151
+ * how it ended, end it. The numbers are OpenTelemetry's own, so an OTel span
152
+ * satisfies this structurally with no translation in between.
153
+ */
154
+ type Span = {
155
+ readonly setAttributes: (attributes: Attributes) => unknown;
156
+ readonly setStatus: (status: {
157
+ readonly code: SpanStatusCode;
158
+ readonly message?: string;
159
+ }) => unknown;
160
+ readonly end: () => void;
161
+ };
162
+ /** What a {@link Tracer} does: start a span, by name. */
163
+ type TracerService = {
164
+ readonly startSpan: (name: string) => Span;
165
+ };
166
+ declare const Tracer_base: import("@btravstack/di").PortClass<"Tracer">;
167
+ /**
168
+ * The application's tracer, as a port — declared without naming OpenTelemetry,
169
+ * because a port typed as a vendor's type points the dependency arrow outwards
170
+ * and makes the whole family install that vendor.
171
+ */
172
+ declare class Tracer extends Tracer_base<TracerService> {}
173
+ /** A monotonic count. */
174
+ type Counter = {
175
+ readonly add: (value: number, attributes?: Attributes) => void;
176
+ };
177
+ /** A distribution of measurements. */
178
+ type Histogram = {
179
+ readonly record: (value: number, attributes?: Attributes) => void;
180
+ };
181
+ /**
182
+ * What a {@link Meter} does: mint the two instruments a framework package needs
183
+ * — it counts what happened and measures how long it took. A gauge is something
184
+ * an application declares about its own domain, reaching the vendor's meter for
185
+ * it as it would any other adapter.
186
+ */
187
+ type MeterService = {
188
+ readonly createCounter: (name: string, options?: {
189
+ readonly description?: string;
190
+ readonly unit?: string;
191
+ }) => Counter;
192
+ readonly createHistogram: (name: string, options?: {
193
+ readonly description?: string;
194
+ readonly unit?: string;
195
+ }) => Histogram;
196
+ };
197
+ declare const Meter_base: import("@btravstack/di").PortClass<"Meter">;
198
+ /** The application's meter, as a port. See {@link Tracer} for why it does not name a vendor. */
199
+ declare class Meter extends Meter_base<MeterService> {}
200
+ //#endregion
201
+ //#region src/observation.d.ts
202
+ /**
203
+ * One operation worth observing, named before it runs.
204
+ *
205
+ * `component` is the starter's own word for itself — `"http"`, `"cache"`,
206
+ * `"amqp"` — and it is what lets an observer mint per-component instrument and
207
+ * span names without knowing anything about the component. `name` is the
208
+ * operation within it (`"get"`, `"unit"`), and `attributes` are the dimensions
209
+ * known before the call.
210
+ *
211
+ * The dimensions are the contributor's business and their **cardinality is its
212
+ * responsibility**: a request path or a workflow id here mints a time series
213
+ * per value, which is how a metrics bill becomes the incident.
214
+ */
215
+ type Operation = {
216
+ readonly component: string;
217
+ readonly name: string;
218
+ /**
219
+ * The DIMENSIONS: bounded, and what a metrics observer may put on an
220
+ * instrument. A cache operation, an HTTP method, an activity type.
221
+ *
222
+ * Their cardinality is the contributor's responsibility — a request path or a
223
+ * cache key here mints a time series per value, which is how a metrics bill
224
+ * becomes the incident. That is what {@link Operation.details} is for.
225
+ */
226
+ readonly attributes: Attributes;
227
+ /**
228
+ * Unbounded context: a cache key, a mail subject, a URL. It rides spans and
229
+ * log lines, where one more field costs one more field — and **never a
230
+ * metric**, where it would cost a time series per value.
231
+ *
232
+ * The split is the whole reason an observer can be shared: without it every
233
+ * contributor would have to choose between a useful span and a safe metric.
234
+ */
235
+ readonly details?: Attributes;
236
+ /**
237
+ * Whether a tracing observer should open a span (default `true`).
238
+ *
239
+ * `false` is for a component whose spans already come from somewhere better:
240
+ * `@btravstack/prisma` says so because `@prisma/instrumentation` traces at the
241
+ * ENGINE level — the real SQL, the connection acquisition — all of it below
242
+ * what a client-level wrapper can see, so a second span would cost one more
243
+ * per query for strictly less information. Counting and timing still happen.
244
+ *
245
+ * Whether a span is worth opening is the CONTRIBUTOR's knowledge, which is
246
+ * why it rides the operation rather than being configured on the observer.
247
+ */
248
+ readonly traced?: boolean;
249
+ };
250
+ /** How an operation came out, plus whatever was only knowable at the end. */
251
+ type Settled = {
252
+ readonly outcome: "ok" | "error";
253
+ /** Dimensions known only at the end, merged over the operation's own — an HTTP status, a cache hit or miss. */
254
+ readonly attributes?: Attributes;
255
+ /** The failure itself, for an observer that writes a line about it. */
256
+ readonly cause?: unknown;
257
+ };
258
+ /** What an observer answers: a finisher, called exactly once when the operation settles. */
259
+ type Settle = (settled: Settled) => void;
260
+ declare const Observers_base: import("@btravstack/di").ManyPortClass<"Observers">;
261
+ /**
262
+ * The set port a starter contributes its observability to, and reads whole.
263
+ *
264
+ * **Called at the START and answering a finisher**, rather than told about a
265
+ * finished operation. That is what lets an observer open a span before the work
266
+ * and end it after — a span reconstructed afterwards from a duration is not the
267
+ * parent of anything that ran inside it.
268
+ *
269
+ * A set port rather than a `Meter` in every starter's `needs`: an observer
270
+ * package an application never composed contributes nothing, and a starter
271
+ * costs a graph no ports to have observability available. That is the
272
+ * difference between "instrumented by default" and "instrumented if you asked
273
+ * for observability", and only the second is free.
274
+ *
275
+ * **A module that reads this port contributes a no-op member of its own**, the
276
+ * way `otel()` does for `Instrumentations`: a collector depending on a set port
277
+ * NOTHING provides is an unmet dependency, at plan time and in `Needs` alike.
278
+ * Several no-ops in one graph cost a call each and nothing else.
279
+ */
280
+ declare class Observers extends Observers_base<(operation: Operation) => Settle> {}
281
+ /** The no-op member every reader of {@link Observers} contributes, so the set is never empty. */
282
+ declare const noObserver: () => Settle;
283
+ /**
284
+ * Run every observer around one operation, and hand back the one finisher that
285
+ * settles them all.
286
+ *
287
+ * Observers are started in order and settled in order, and the finisher is
288
+ * **guarded**: a second call is dropped. "Called exactly once" is therefore a
289
+ * property of this function rather than a rule each starter re-keeps — which
290
+ * matters because the settling call sites are error paths, where a `tap` and a
291
+ * `tapFailure` on the same chain, or a retry, is exactly the shape that fires
292
+ * twice and doubles a failure count.
293
+ */
294
+ declare const observe: (observers: readonly ((operation: Operation) => Settle)[], operation: Operation) => Settle;
295
+ //#endregion
86
296
  //#region src/clock.d.ts
87
297
  type Clock = {
88
298
  readonly now: () => number;
@@ -105,7 +315,6 @@ type UnitRecord = {
105
315
  readonly unitId: string;
106
316
  readonly traceId: string;
107
317
  readonly tenantId: string | undefined;
108
- readonly deadline: number | undefined;
109
318
  readonly signal: AbortSignal;
110
319
  };
111
320
  declare const currentUnit: () => UnitRecord | undefined;
@@ -129,7 +338,6 @@ type UnitMeta = {
129
338
  readonly id: string;
130
339
  readonly traceId?: string;
131
340
  readonly tenantId?: string;
132
- readonly deadline?: number;
133
341
  };
134
342
  type UnitWork<T, E> = (signal: AbortSignal) => AsyncResult<T, E> | Promise<Result<T, E>> | Result<T, E>;
135
343
  type UnitRegistry = {
@@ -305,121 +513,6 @@ type KernelEvent = {
305
513
  type EventSink = (event: KernelEvent) => void;
306
514
  declare const stderrSink: EventSink;
307
515
  //#endregion
308
- //#region src/observability.d.ts
309
- /**
310
- * The severity of one line, and the whole of the set: six levels, ordered, with
311
- * no caller-defined additions — which is what lets `LOG_LEVEL` be validated at
312
- * startup and `isEnabled` be a comparison rather than a lookup.
313
- */
314
- type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
315
- /** The levels in order, least severe first — what `isEnabled` compares through. */
316
- declare const LEVELS: readonly Level[];
317
- /**
318
- * What a line, a span or a measurement carries besides itself: a flat record of
319
- * scalars.
320
- *
321
- * Flat and scalar on purpose. A nested object is where a field's name stops
322
- * being stable, and an `unknown` value is where a logger starts stringifying
323
- * whatever it is handed — which is how a log call becomes the thing that
324
- * throws. A failure has a channel of its own, `cause`.
325
- */
326
- type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;
327
- /**
328
- * Every method takes the same three arguments in the same order:
329
- * `(message, attributes?, cause?)`.
330
- *
331
- * Uniform on purpose. A failure is not a property of severity — an `info` line
332
- * reporting a recovered fault carries one too — so every level can take a
333
- * `cause`, at the cost of `logger.error("boom", undefined, cause)` for a
334
- * failure with nothing else to say.
335
- */
336
- type LoggerService = {
337
- readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
338
- readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
339
- readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
340
- readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
341
- readonly warn: (message: string, attributes?: Attributes, cause?: unknown) => void;
342
- /** `cause` is the failure itself — an `Error`, an `unthrown` `Err`'s error, a rejected value. */
343
- readonly error: (message: string, attributes?: Attributes, cause?: unknown) => void;
344
- readonly fatal: (message: string, attributes?: Attributes, cause?: unknown) => void;
345
- /** A logger carrying `attributes` on every line it writes, on top of this one's. Never mutates this one. */
346
- readonly with: (attributes: Attributes) => LoggerService;
347
- /** Whether a line at `level` would be written — for a payload expensive enough to be worth not building. */
348
- readonly isEnabled: (level: Level) => boolean;
349
- };
350
- declare const Logger_base: import("@btravstack/di").PortClass<"Logger">;
351
- /**
352
- * The application's logger, as a port — declared here and implemented in
353
- * `@btravstack/observability`, because a contract every package may depend on
354
- * has to be reachable without installing an implementation, and because the
355
- * correlation an implementation stamps per line is the kernel's own ambient
356
- * record.
357
- *
358
- * An implementation must not throw: a logger that throws turns an observability
359
- * problem into an outage. Synchronous `void`, not an `AsyncResult` — thesis 6's
360
- * one deliberate exemption, since a lost line is not a modeled error.
361
- */
362
- declare class Logger extends Logger_base<LoggerService> {}
363
- /** A span's outcome: unset until something says otherwise, then `ok` or `error`. */
364
- declare const SPAN_STATUS: {
365
- readonly unset: 0;
366
- readonly ok: 1;
367
- readonly error: 2;
368
- };
369
- /** The three codes {@link SPAN_STATUS} names, as the type a {@link Span} takes. */
370
- type SpanStatusCode = (typeof SPAN_STATUS)[keyof typeof SPAN_STATUS];
371
- /**
372
- * One span, narrowed to what a framework package does with it: label it, say
373
- * how it ended, end it. The numbers are OpenTelemetry's own, so an OTel span
374
- * satisfies this structurally with no translation in between.
375
- */
376
- type Span = {
377
- readonly setAttributes: (attributes: Attributes) => unknown;
378
- readonly setStatus: (status: {
379
- readonly code: SpanStatusCode;
380
- readonly message?: string;
381
- }) => unknown;
382
- readonly end: () => void;
383
- };
384
- /** What a {@link Tracer} does: start a span, by name. */
385
- type TracerService = {
386
- readonly startSpan: (name: string) => Span;
387
- };
388
- declare const Tracer_base: import("@btravstack/di").PortClass<"Tracer">;
389
- /**
390
- * The application's tracer, as a port — declared without naming OpenTelemetry,
391
- * because a port typed as a vendor's type points the dependency arrow outwards
392
- * and makes the whole family install that vendor.
393
- */
394
- declare class Tracer extends Tracer_base<TracerService> {}
395
- /** A monotonic count. */
396
- type Counter = {
397
- readonly add: (value: number, attributes?: Attributes) => void;
398
- };
399
- /** A distribution of measurements. */
400
- type Histogram = {
401
- readonly record: (value: number, attributes?: Attributes) => void;
402
- };
403
- /**
404
- * What a {@link Meter} does: mint the two instruments a framework package needs
405
- * — it counts what happened and measures how long it took. A gauge is something
406
- * an application declares about its own domain, reaching the vendor's meter for
407
- * it as it would any other adapter.
408
- */
409
- type MeterService = {
410
- readonly createCounter: (name: string, options?: {
411
- readonly description?: string;
412
- readonly unit?: string;
413
- }) => Counter;
414
- readonly createHistogram: (name: string, options?: {
415
- readonly description?: string;
416
- readonly unit?: string;
417
- }) => Histogram;
418
- };
419
- declare const Meter_base: import("@btravstack/di").PortClass<"Meter">;
420
- /** The application's meter, as a port. See {@link Tracer} for why it does not name a vendor. */
421
- declare class Meter extends Meter_base<MeterService> {}
422
- //#endregion
423
516
  //#region src/phase.d.ts
424
517
  type Phase = "building" | "starting" | "serving" | "draining" | "stopping" | "exited";
425
518
  //#endregion
@@ -505,18 +598,38 @@ type RunningApp<E, Info = never> = {
505
598
  };
506
599
  /**
507
600
  * The phantom marker `start`, `runMain` and `Boot` all intersect onto their
508
- * `module` parameter: `unknown` — and invisible — when the module exports a
509
- * runtime, its exports cover what that runtime resolves and they cover the
510
- * unit module's needs; one of three sentences otherwise, printed at the call
511
- * site as the parameter type the argument did not match.
601
+ * `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.
512
606
  *
513
607
  * `unknown` is the satisfied arm because intersecting it leaves the module
514
608
  * type untouched. A runtime's `resolves` is checked against the module's
515
609
  * exports only, never the unit module's: `RuntimeHost.ctx` is the application
516
610
  * context, so a unit-only port would resolve to nothing there.
611
+ *
612
+ * **Unmet needs are checked FIRST, and answered in di's own words rather than
613
+ * a sentence of the kernel's.** A root that forgot `provides: [router]` used
614
+ * to be told `Type 'HttpRouterPort' is not assignable to type 'Scope'` — an
615
+ * internal phantom nobody has heard of — or, when the port was missing from
616
+ * `exports` too, `UNSATISFIED RUNTIME PORTS`, which is a correct diagnosis of
617
+ * the second mistake that reads as a wrong one of the first and steers the fix
618
+ * into the wrong list. It is the same defect `Module.build` reports, so it
619
+ * prints the same sentence and ends on the port that is missing.
620
+ */
621
+ /**
622
+ * A port's declared id, which is what a marker names it by: an id is the short
623
+ * string the application wrote in `Port("…")`, where the port TYPE of a
624
+ * starter's own generic port — `AmqpHandlers` over a contract — expands to
625
+ * hundreds of characters of the caller's own schema and truncates before the
626
+ * name is reached.
517
627
  */
518
- 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";
519
- 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>>;
628
+ 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" : {
630
+ readonly "UNSATISFIED DEPENDENCIES — nothing provides": PortIdOf<Exclude<N, Scope | Env>>;
631
+ };
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>>;
520
633
  //#endregion
521
634
  //#region src/run-main.d.ts
522
635
  /**
@@ -551,6 +664,6 @@ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X,
551
664
  * await runMain(OrderApi);
552
665
  * ```
553
666
  */
554
- 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>;
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>;
555
668
  //#endregion
556
- 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, 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, runHealthChecks, runMain, start, stderrSink, systemClock };
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 };
package/dist/index.mjs CHANGED
@@ -60,6 +60,50 @@ const runHealthChecks = (checks) => allAsync(checks.map((health) => OkAsync().fl
60
60
  components
61
61
  }));
62
62
  //#endregion
63
+ //#region src/observation.ts
64
+ /**
65
+ * The set port a starter contributes its observability to, and reads whole.
66
+ *
67
+ * **Called at the START and answering a finisher**, rather than told about a
68
+ * finished operation. That is what lets an observer open a span before the work
69
+ * and end it after — a span reconstructed afterwards from a duration is not the
70
+ * parent of anything that ran inside it.
71
+ *
72
+ * A set port rather than a `Meter` in every starter's `needs`: an observer
73
+ * package an application never composed contributes nothing, and a starter
74
+ * costs a graph no ports to have observability available. That is the
75
+ * difference between "instrumented by default" and "instrumented if you asked
76
+ * for observability", and only the second is free.
77
+ *
78
+ * **A module that reads this port contributes a no-op member of its own**, the
79
+ * way `otel()` does for `Instrumentations`: a collector depending on a set port
80
+ * NOTHING provides is an unmet dependency, at plan time and in `Needs` alike.
81
+ * Several no-ops in one graph cost a call each and nothing else.
82
+ */
83
+ var Observers = class extends Port.many("Observers") {};
84
+ /** The no-op member every reader of {@link Observers} contributes, so the set is never empty. */
85
+ const noObserver = () => () => {};
86
+ /**
87
+ * Run every observer around one operation, and hand back the one finisher that
88
+ * settles them all.
89
+ *
90
+ * Observers are started in order and settled in order, and the finisher is
91
+ * **guarded**: a second call is dropped. "Called exactly once" is therefore a
92
+ * property of this function rather than a rule each starter re-keeps — which
93
+ * matters because the settling call sites are error paths, where a `tap` and a
94
+ * `tapFailure` on the same chain, or a retry, is exactly the shape that fires
95
+ * twice and doubles a failure count.
96
+ */
97
+ const observe = (observers, operation) => {
98
+ const settlers = observers.map((observer) => observer(operation));
99
+ let done = false;
100
+ return (settled) => {
101
+ if (done) return;
102
+ done = true;
103
+ for (const settle of settlers) settle(settled);
104
+ };
105
+ };
106
+ //#endregion
63
107
  //#region src/clock.ts
64
108
  const systemClock = {
65
109
  now: () => Date.now(),
@@ -341,7 +385,6 @@ const createUnitRegistry = () => {
341
385
  unitId: nextId(),
342
386
  traceId: meta.traceId ?? meta.id,
343
387
  tenantId: meta.tenantId,
344
- deadline: meta.deadline,
345
388
  signal: controller.signal
346
389
  };
347
390
  return fromSafePromise(runWithUnit(record, async () => {
@@ -645,4 +688,4 @@ const runMain = async (module, options = {}, exit = (code) => {
645
688
  await awaitExit(start(module, options), exit);
646
689
  };
647
690
  //#endregion
648
- export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
691
+ export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, Observers, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, noObserver, observe, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/core",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",
@@ -53,13 +53,13 @@
53
53
  "typescript": "7.0.2",
54
54
  "unthrown": "5.7.0",
55
55
  "vitest": "4.1.11",
56
- "@btravstack/config": "0.9.0",
57
- "@btravstack/di": "0.9.0"
56
+ "@btravstack/di": "0.11.0",
57
+ "@btravstack/config": "0.11.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "unthrown": "^5.0.0",
61
- "@btravstack/config": "^0.9.0",
62
- "@btravstack/di": "^0.9.0"
61
+ "@btravstack/config": "^0.11.0",
62
+ "@btravstack/di": "^0.11.0"
63
63
  },
64
64
  "engines": {
65
65
  "node": ">=22"