@btravstack/core 0.15.0 → 0.17.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/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _btravstack_di = require("@btravstack/di");
3
3
  let unthrown = require("unthrown");
4
+ let node_timers_promises = require("node:timers/promises");
4
5
  let _btravstack_config = require("@btravstack/config");
5
6
  let node_http = require("node:http");
6
7
  let node_async_hooks = require("node:async_hooks");
@@ -69,10 +70,11 @@ const answeringWithin = (health, ms) => {
69
70
  * rather than a recovery.** Neither recovery above can see one: there is no
70
71
  * error and no defect, only silence, so `/healthz` held a socket open per hit
71
72
  * while kubelet timed out against a report that named nothing. Each check now
72
- * races {@link HealthOptions.timeoutMs}, and the component that hung is named
73
+ * races its own `timeoutMs` (default {@link DEFAULT_HEALTH_TIMEOUT_MS}), and
74
+ * the component that hung is named
73
75
  * in the report like any other unhealthy one.
74
76
  */
75
- const runHealthChecks = (checks, options = {}) => (0, unthrown.allAsync)(checks.map((health) => answeringWithin(health, health.timeoutMs ?? options.timeoutMs ?? 800).map(() => ({
77
+ const runHealthChecks = (checks) => (0, unthrown.allAsync)(checks.map((health) => answeringWithin(health, health.timeoutMs ?? 800).map(() => ({
76
78
  name: health.name,
77
79
  status: "healthy"
78
80
  })).recoverErrCases((matcher) => matcher.with(unthrown.P.tag("HealthCheckFailed"), (error) => ({
@@ -153,22 +155,10 @@ const observed = (observers, operation, call, settled = {}) => {
153
155
  //#region src/clock.ts
154
156
  const systemClock = {
155
157
  now: () => Date.now(),
156
- sleep: (ms, signal) => (0, unthrown.fromSafePromise)(new Promise((resolve) => {
157
- if (signal?.aborted === true) {
158
- resolve();
159
- return;
160
- }
161
- const timer = setTimeout(() => {
162
- signal?.removeEventListener("abort", onAbort);
163
- resolve();
164
- }, ms);
165
- timer.unref?.();
166
- function onAbort() {
167
- clearTimeout(timer);
168
- resolve();
169
- }
170
- signal?.addEventListener("abort", onAbort, { once: true });
171
- }))
158
+ sleep: (ms, signal) => (0, unthrown.fromSafePromise)((0, node_timers_promises.setTimeout)(ms, void 0, {
159
+ signal,
160
+ ref: false
161
+ }).catch(() => {}))
172
162
  };
173
163
  //#endregion
174
164
  //#region src/events.ts
@@ -621,6 +611,11 @@ const start = (module, options = {}) => {
621
611
  const health = () => runHealthChecks(healthChecks);
622
612
  const probeBound = Promise.withResolvers();
623
613
  const runtimePublished = Promise.withResolvers();
614
+ const leaveStartup = () => {
615
+ runtimePublished.resolve(void 0);
616
+ tracker.advanceTo("stopping");
617
+ disposeAll();
618
+ };
624
619
  const stopping = Promise.withResolvers();
625
620
  const stopAbandoned = () => (0, unthrown.fromSafePromise)(stopping.promise).flatMap(({ reason, drain }) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : (0, unthrown.OkAsync)(abandonStop(reason, drain))));
626
621
  const abandonStop = (reason, drain) => {
@@ -640,9 +635,7 @@ const start = (module, options = {}) => {
640
635
  afterMs: void 0
641
636
  });
642
637
  registry.abortAll();
643
- runtimePublished.resolve(void 0);
644
- tracker.advanceTo("stopping");
645
- disposeAll();
638
+ leaveStartup();
646
639
  return reportOf(reason, void 0, "build");
647
640
  });
648
641
  const abandonBuild = (reason) => {
@@ -673,11 +666,7 @@ const start = (module, options = {}) => {
673
666
  cause: failure.tag === "Err" ? failure.error : failure.cause
674
667
  });
675
668
  probeBound.resolve(void 0);
676
- runtimePublished.resolve(void 0);
677
- tracker.advanceTo("stopping");
678
- disposeSignals();
679
- disposeUncaught();
680
- tracker.advanceTo("exited");
669
+ leaveStartup();
681
670
  });
682
671
  const root = (0, _btravstack_di.Module)("Kernel")({
683
672
  imports: providesEnv(module) ? [module] : [module, (0, _btravstack_di.Module)("Environment")({
@@ -794,9 +783,7 @@ const start = (module, options = {}) => {
794
783
  type: "startFailed",
795
784
  cause: failure.tag === "Err" ? failure.error : failure.cause
796
785
  });
797
- runtimePublished.resolve(void 0);
798
- tracker.advanceTo("stopping");
799
- disposeAll();
786
+ leaveStartup();
800
787
  }).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
801
788
  return {
802
789
  exited: probesStarted.flatMap(() => (0, unthrown.fromSafePromise)(Promise.race([
@@ -872,6 +859,97 @@ const runMain = async (module, options = {}, exit = (code) => {
872
859
  await awaitExit(start(module, options), exit);
873
860
  };
874
861
  //#endregion
862
+ //#region src/unit-record.ts
863
+ /**
864
+ * Where {@link dispatchUnit} leaves the forked context for {@link withUnitRecord}
865
+ * to read. A symbol rather than a name: the library merges it into the context
866
+ * every piece sees, and a string key would appear on the record a piece written
867
+ * against the `{ inject, sync }` arm destructures.
868
+ */
869
+ const UNIT_SCOPE = Symbol("@btravstack/core/unit-scope");
870
+ /**
871
+ * The declared record, as a getter per name resolved on read out of the fork.
872
+ * Neither writable nor configurable — a piece reads what the fork holds, and
873
+ * cannot reshape the record under the next unit. With no unit module bound
874
+ * there is nothing to resolve from, so the record is empty whatever was
875
+ * declared.
876
+ */
877
+ const unitRecordOf = (forked, record) => {
878
+ const unit = {};
879
+ if (forked === void 0) return unit;
880
+ for (const [name, port] of Object.entries(record)) Object.defineProperty(unit, name, {
881
+ enumerable: true,
882
+ get: () => forked.get(port)
883
+ });
884
+ return unit;
885
+ };
886
+ /**
887
+ * One `(helpers, input)` implementation with its piece's declared record put on
888
+ * `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
889
+ * Applied once per piece as di constructs it, so a unit costs one record and
890
+ * one context object. Each worker maps it over its own entry shape.
891
+ */
892
+ const withUnitRecord = (record, implementation) => (helpers, input) => {
893
+ const { [UNIT_SCOPE]: forked, ...rest } = helpers.context ?? {};
894
+ return implementation({
895
+ ...helpers,
896
+ context: {
897
+ ...rest,
898
+ unit: unitRecordOf(forked, record)
899
+ }
900
+ }, input);
901
+ };
902
+ /**
903
+ * A worker middleware's body: observe `operation`, open one kernel unit, fork
904
+ * `unit` — when one is bound — seeded with `seed`, and call `next` with the
905
+ * forked context where {@link withUnitRecord} finds it. With no `unit` bound,
906
+ * `next()` runs unchanged.
907
+ *
908
+ * Observed from whichever channel the unit settles on, a defect included: a
909
+ * defect is a failed unit too, and an errors count that omitted it would be the
910
+ * reassuring half.
911
+ */
912
+ const dispatchUnit = (dispatch) => observed(dispatch.observers, dispatch.operation, () => dispatch.host.run(dispatch.meta, (scope) => dispatch.unit === void 0 ? dispatch.next() : scope.fork(dispatch.unit, [dispatch.seed]).flatMap((forked) => dispatch.next({ context: { [UNIT_SCOPE]: forked } }))));
913
+ //#endregion
914
+ //#region src/pieces.ts
915
+ /**
916
+ * The runtime half of a starter's piece factory — `AmqpHandler(contract, key)`,
917
+ * `TemporalWorkflowActivities(contract, key)`: a provider on `port` whose
918
+ * service is what `sync` answers, wrapped once by `withUnit` so it reads the
919
+ * record the piece declared. The starter keeps the types; this keeps the body.
920
+ */
921
+ const mintPiece = (port, withUnit) => (options) => (0, _btravstack_di.Provider)(port)({
922
+ inject: options.inject,
923
+ sync: (services) => withUnit(options.unit ?? {}, options.sync(services))
924
+ });
925
+ /**
926
+ * The runtime half of a starter's composing provider — `AmqpHandlers(contract)`,
927
+ * `TemporalActivities(contract)` — and its two arms. An array of pieces is
928
+ * composed into one record keyed by each piece's port id less `prefix`, so the
929
+ * services record IS the composed record; a `{ inject, unit?, sync }` record
930
+ * has `withUnit` applied to every entry `sync` answers.
931
+ */
932
+ const composeByPrefix = (port, prefix, withUnit) => {
933
+ const whole = mintPiece(port, (record, entries) => Object.fromEntries(Object.entries(entries).map(([key, entry]) => [key, withUnit(record, entry)])));
934
+ return (first) => Array.isArray(first) ? (0, _btravstack_di.Provider)(port)({
935
+ inject: Object.fromEntries(first.map((piece) => [piece.port.portId.slice(prefix.length), piece.port])),
936
+ sync: (services) => services
937
+ }) : whole(first);
938
+ };
939
+ //#endregion
940
+ //#region src/starter.ts
941
+ /**
942
+ * The no-op member every module reading {@link Observers} provides, so the set
943
+ * it reads is never the empty dependency di refuses: a graph composing no
944
+ * observability still starts. One provider, shared — di de-duplicates a graph's
945
+ * providers by reference, so however many modules contribute it, an operation
946
+ * costs one inert call.
947
+ */
948
+ const noObserverMember = _btravstack_di.Provider.member(Observers)({
949
+ inject: {},
950
+ value: noObserver
951
+ });
952
+ //#endregion
875
953
  exports.HealthCheckFailed = HealthCheckFailed;
876
954
  exports.HealthChecks = HealthChecks;
877
955
  exports.Instrumentations = Instrumentations;
@@ -883,8 +961,12 @@ exports.RuntimePort = RuntimePort;
883
961
  exports.RuntimeStartFailed = RuntimeStartFailed;
884
962
  exports.SPAN_STATUS = SPAN_STATUS;
885
963
  exports.Tracer = Tracer;
964
+ exports.composeByPrefix = composeByPrefix;
886
965
  exports.currentUnit = currentUnit;
966
+ exports.dispatchUnit = dispatchUnit;
967
+ exports.mintPiece = mintPiece;
887
968
  exports.noObserver = noObserver;
969
+ exports.noObserverMember = noObserverMember;
888
970
  exports.observe = observe;
889
971
  exports.observed = observed;
890
972
  exports.releasedBy = releasedBy;
@@ -894,3 +976,4 @@ exports.start = start;
894
976
  exports.stderrSink = stderrSink;
895
977
  exports.systemClock = systemClock;
896
978
  exports.traceIdOfTraceparent = traceIdOfTraceparent;
979
+ exports.withUnitRecord = withUnitRecord;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AsyncResult, FailureView, Result } from "unthrown";
2
- import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
2
+ import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Provider, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
3
3
  import { Env, Environment } from "@btravstack/config";
4
4
  //#region src/instrumentation.d.ts
5
5
  /**
@@ -8,10 +8,10 @@ import { Env, Environment } from "@btravstack/config";
8
8
  * **Async, and answers `undefined` rather than failing, because the package
9
9
  * supplying the instrumentation is an OPTIONAL peer.** A starter cannot know at
10
10
  * composition time whether the consumer installed it, so it contributes the
11
- * ATTEMPT and the collector drops what did not load — which is what lets
12
- * `@btravstack/prisma` declare engine tracing without making every consumer
13
- * install `@prisma/instrumentation`. A contributor that wants to say why its
14
- * load answered nothing logs that itself; it is the one that knows.
11
+ * ATTEMPT and the collector drops what did not load — which is what lets a
12
+ * starter declare an instrumentation without making every consumer install the
13
+ * package that supplies it. A contributor that wants to say why its load
14
+ * answered nothing logs that itself; it is the one that knows.
15
15
  *
16
16
  * The instrumentation is `unknown` on purpose. Naming OpenTelemetry's
17
17
  * `Instrumentation` would put the vendor in the package every other one peers
@@ -48,7 +48,8 @@ type HealthCheck = {
48
48
  readonly name: string;
49
49
  readonly check: () => AsyncResult<void, HealthCheckFailed>;
50
50
  /**
51
- * How long this component gets to answer, overriding the fold's own default.
51
+ * How long this component gets to answer, overriding
52
+ * {@link DEFAULT_HEALTH_TIMEOUT_MS}.
52
53
  *
53
54
  * It is on the CONTRIBUTION because only the contributor knows: a `SELECT 1`
54
55
  * over a warm pool and an SMTP relay's greeting are two orders of magnitude
@@ -78,10 +79,6 @@ type HealthReport = {
78
79
  readonly status: "healthy" | "unhealthy";
79
80
  readonly components: readonly ComponentHealth[];
80
81
  };
81
- type HealthOptions = {
82
- /** Per check, not for the fold: the checks run concurrently. Default {@link DEFAULT_HEALTH_TIMEOUT_MS}. */
83
- readonly timeoutMs?: number;
84
- };
85
82
  /**
86
83
  * Runs every check and folds the results into one report — the whole app is
87
84
  * unhealthy if any single component is.
@@ -99,10 +96,11 @@ type HealthOptions = {
99
96
  * rather than a recovery.** Neither recovery above can see one: there is no
100
97
  * error and no defect, only silence, so `/healthz` held a socket open per hit
101
98
  * while kubelet timed out against a report that named nothing. Each check now
102
- * races {@link HealthOptions.timeoutMs}, and the component that hung is named
99
+ * races its own `timeoutMs` (default {@link DEFAULT_HEALTH_TIMEOUT_MS}), and
100
+ * the component that hung is named
103
101
  * in the report like any other unhealthy one.
104
102
  */
105
- export declare const runHealthChecks: (checks: readonly HealthCheck[], options?: HealthOptions) => AsyncResult<HealthReport, never>;
103
+ export declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
106
104
  //#endregion
107
105
  //#region src/observability.d.ts
108
106
  /**
@@ -133,7 +131,6 @@ type Attributes = Readonly<Record<string, string | number | boolean | undefined>
133
131
  * failure with nothing else to say.
134
132
  */
135
133
  type LoggerService = {
136
- readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
137
134
  readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
138
135
  readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
139
136
  readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
@@ -257,11 +254,12 @@ type Operation = {
257
254
  /**
258
255
  * Whether a tracing observer should open a span (default `true`).
259
256
  *
260
- * `false` is for a component whose spans already come from somewhere better:
261
- * `@btravstack/prisma` says so because `@prisma/instrumentation` traces at the
262
- * ENGINE level — the real SQL, the connection acquisition — all of it below
263
- * what a client-level wrapper can see, so a second span would cost one more
264
- * per query for strictly less information. Counting and timing still happen.
257
+ * `false` is for an operation a span would say nothing about — one whose
258
+ * spans already come from somewhere better, or one with no duration at all.
259
+ * Counting and timing still happen. `@btravstack/amqp-worker`'s broker
260
+ * diagnostics pass it, being zero-length events; `@btravstack/prisma` no
261
+ * longer does, since Prisma 8 ships no engine tracer and its middleware is
262
+ * the only span there is.
265
263
  *
266
264
  * Whether a span is worth opening is the CONTRIBUTOR's knowledge, which is
267
265
  * why it rides the operation rather than being configured on the observer.
@@ -836,4 +834,112 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
836
834
  */
837
835
  export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
838
836
  //#endregion
839
- export type { Attributes, Clock, ComponentHealth, Counter, DrainReport, EventSink, ExitReport, HealthCheck, HealthReport, Histogram, InstrumentationLoader, KernelEvent, Level, LoggerService, MeterService, Operation, Phase, RunUnit, RunningApp, Runtime, RuntimeHost, RuntimeInfoOf, Serving, Settle, Settled, Span, SpanStatusCode, StartGate, StartOptions, TeardownError, TracerService, UnitHost, UnitMeta, UnitRecord, UnitRegistry, UnitWork };
837
+ //#region src/unit-record.d.ts
838
+ /**
839
+ * A module a runtime's `unit` option may bind, as the upper bound a starter
840
+ * constrains its own `Unit` type parameter to. `Module`'s `_exports` channel is
841
+ * contravariant, so `Exports = never` — never `unknown` — is what makes a REAL
842
+ * module's own (necessarily narrower) export type assignable to this bound:
843
+ * `(x: Concrete) => void` is assignable to `(x: never) => void`, not to
844
+ * `(x: unknown) => void`.
845
+ */
846
+ type AnyUnitModule = Module<never, never, unknown>;
847
+ /**
848
+ * The needs a bound unit module still owes, or `never` when none is bound.
849
+ * `Scope` is excluded, since nothing can ever provide it — the same exemption
850
+ * `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
851
+ * fork seed discharges.
852
+ */
853
+ type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
854
+ /** What a bound unit module exports — the port instances a piece of that kind may read. */
855
+ type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
856
+ /** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
857
+ type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
858
+ /**
859
+ * A piece injecting a port the bound unit module does not export. `unknown`
860
+ * when every declared port is covered — including the empty case, so a worker
861
+ * whose pieces declare no `unit:` is gated on nothing.
862
+ */
863
+ type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
864
+ readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
865
+ };
866
+ /**
867
+ * One `(helpers, input)` implementation with its piece's declared record put on
868
+ * `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
869
+ * Applied once per piece as di constructs it, so a unit costs one record and
870
+ * one context object. Each worker maps it over its own entry shape.
871
+ */
872
+ export declare const withUnitRecord: (record: Readonly<Record<string, AnyPort>>, implementation: (helpers: never, input: never) => unknown) => (helpers: {
873
+ readonly context?: Readonly<Record<string | symbol, unknown>>;
874
+ }, input: never) => unknown;
875
+ /**
876
+ * A worker middleware's body: observe `operation`, open one kernel unit, fork
877
+ * `unit` — when one is bound — seeded with `seed`, and call `next` with the
878
+ * forked context where {@link withUnitRecord} finds it. With no `unit` bound,
879
+ * `next()` runs unchanged.
880
+ *
881
+ * Observed from whichever channel the unit settles on, a defect included: a
882
+ * defect is a failed unit too, and an errors count that omitted it would be the
883
+ * reassuring half.
884
+ */
885
+ export declare const dispatchUnit: <T, E>(dispatch: {
886
+ readonly host: RuntimeHost<never>;
887
+ readonly observers: readonly ((operation: Operation) => Settle)[];
888
+ readonly operation: Operation;
889
+ readonly meta: UnitMeta;
890
+ readonly unit: AnyUnitModule | undefined;
891
+ readonly seed: readonly [AnyPort, unknown];
892
+ readonly next: (overrides?: never) => AsyncResult<T, E>;
893
+ }) => AsyncResult<T, E>;
894
+ //#endregion
895
+ //#region src/pieces.d.ts
896
+ /**
897
+ * A refused array: as long as the array the caller wrote, its head the caller's
898
+ * own elements — which match — and its LAST element the marker paired with what
899
+ * is wrong.
900
+ *
901
+ * TypeScript compares two equal-length tuples element by element, so the extra
902
+ * diagnostic it reports lands on the trailing element and carries both the
903
+ * sentence and the missing key. A fixed two-element tuple named the key only
904
+ * when the array happened to be two elements long; every other arity was a
905
+ * length mismatch, and the developer diffed the contract against the array by
906
+ * hand.
907
+ */
908
+ type Refuse<T extends readonly unknown[], Marker extends string, Detail> = T extends readonly [...infer Head, unknown] ? readonly [...Head, readonly [Marker, Detail]] : readonly [readonly [Marker, Detail]];
909
+ /** How a starter puts a piece's declared `unit:` record on one of its entries. */
910
+ type EntryWrapper = (record: Readonly<Record<string, AnyPort>>, entry: unknown) => unknown;
911
+ /** A piece's `{ inject, unit?, sync }` options, with the types each starter states erased. */
912
+ type PieceOptions = {
913
+ readonly inject: Readonly<Record<string, AnyPort>>;
914
+ readonly unit?: Readonly<Record<string, AnyPort>>;
915
+ readonly sync: (services: never) => unknown;
916
+ };
917
+ /**
918
+ * The runtime half of a starter's piece factory — `AmqpHandler(contract, key)`,
919
+ * `TemporalWorkflowActivities(contract, key)`: a provider on `port` whose
920
+ * service is what `sync` answers, wrapped once by `withUnit` so it reads the
921
+ * record the piece declared. The starter keeps the types; this keeps the body.
922
+ */
923
+ export declare const mintPiece: (port: AnyPort, withUnit: EntryWrapper) => (options: PieceOptions) => unknown;
924
+ /**
925
+ * The runtime half of a starter's composing provider — `AmqpHandlers(contract)`,
926
+ * `TemporalActivities(contract)` — and its two arms. An array of pieces is
927
+ * composed into one record keyed by each piece's port id less `prefix`, so the
928
+ * services record IS the composed record; a `{ inject, unit?, sync }` record
929
+ * has `withUnit` applied to every entry `sync` answers.
930
+ */
931
+ export declare const composeByPrefix: (port: AnyPort, prefix: string, withUnit: EntryWrapper) => ((first: unknown) => unknown);
932
+ //#endregion
933
+ //#region src/starter.d.ts
934
+ /**
935
+ * The no-op member every module reading {@link Observers} provides, so the set
936
+ * it reads is never the empty dependency di refuses: a graph composing no
937
+ * observability still starts. One provider, shared — di de-duplicates a graph's
938
+ * providers by reference, so however many modules contribute it, an operation
939
+ * costs one inert call.
940
+ */
941
+ export declare const noObserverMember: Provider<Observers, never, never> & {
942
+ readonly port: typeof Observers;
943
+ };
944
+ //#endregion
945
+ export type { AnyUnitModule, Attributes, Clock, ComponentHealth, Counter, DrainReport, EventSink, ExitReport, HealthCheck, HealthReport, Histogram, InstrumentationLoader, KernelEvent, Level, LoggerService, MeterService, Operation, Phase, Refuse, RunUnit, RunningApp, Runtime, RuntimeHost, RuntimeInfoOf, Serving, Settle, Settled, Span, SpanStatusCode, StartGate, StartOptions, TeardownError, TracerService, UnitExportsOf, UnitGate, UnitHost, UnitMeta, UnitNeedsOf, UnitRecord, UnitRecordOf, UnitRegistry, UnitWork };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
1
+ import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Provider, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
2
2
  import { AsyncResult, FailureView, Result } from "unthrown";
3
3
  import { Env, Environment } from "@btravstack/config";
4
4
  //#region src/instrumentation.d.ts
@@ -8,10 +8,10 @@ import { Env, Environment } from "@btravstack/config";
8
8
  * **Async, and answers `undefined` rather than failing, because the package
9
9
  * supplying the instrumentation is an OPTIONAL peer.** A starter cannot know at
10
10
  * composition time whether the consumer installed it, so it contributes the
11
- * ATTEMPT and the collector drops what did not load — which is what lets
12
- * `@btravstack/prisma` declare engine tracing without making every consumer
13
- * install `@prisma/instrumentation`. A contributor that wants to say why its
14
- * load answered nothing logs that itself; it is the one that knows.
11
+ * ATTEMPT and the collector drops what did not load — which is what lets a
12
+ * starter declare an instrumentation without making every consumer install the
13
+ * package that supplies it. A contributor that wants to say why its load
14
+ * answered nothing logs that itself; it is the one that knows.
15
15
  *
16
16
  * The instrumentation is `unknown` on purpose. Naming OpenTelemetry's
17
17
  * `Instrumentation` would put the vendor in the package every other one peers
@@ -48,7 +48,8 @@ type HealthCheck = {
48
48
  readonly name: string;
49
49
  readonly check: () => AsyncResult<void, HealthCheckFailed>;
50
50
  /**
51
- * How long this component gets to answer, overriding the fold's own default.
51
+ * How long this component gets to answer, overriding
52
+ * {@link DEFAULT_HEALTH_TIMEOUT_MS}.
52
53
  *
53
54
  * It is on the CONTRIBUTION because only the contributor knows: a `SELECT 1`
54
55
  * over a warm pool and an SMTP relay's greeting are two orders of magnitude
@@ -78,10 +79,6 @@ type HealthReport = {
78
79
  readonly status: "healthy" | "unhealthy";
79
80
  readonly components: readonly ComponentHealth[];
80
81
  };
81
- type HealthOptions = {
82
- /** Per check, not for the fold: the checks run concurrently. Default {@link DEFAULT_HEALTH_TIMEOUT_MS}. */
83
- readonly timeoutMs?: number;
84
- };
85
82
  /**
86
83
  * Runs every check and folds the results into one report — the whole app is
87
84
  * unhealthy if any single component is.
@@ -99,10 +96,11 @@ type HealthOptions = {
99
96
  * rather than a recovery.** Neither recovery above can see one: there is no
100
97
  * error and no defect, only silence, so `/healthz` held a socket open per hit
101
98
  * while kubelet timed out against a report that named nothing. Each check now
102
- * races {@link HealthOptions.timeoutMs}, and the component that hung is named
99
+ * races its own `timeoutMs` (default {@link DEFAULT_HEALTH_TIMEOUT_MS}), and
100
+ * the component that hung is named
103
101
  * in the report like any other unhealthy one.
104
102
  */
105
- export declare const runHealthChecks: (checks: readonly HealthCheck[], options?: HealthOptions) => AsyncResult<HealthReport, never>;
103
+ export declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
106
104
  //#endregion
107
105
  //#region src/observability.d.ts
108
106
  /**
@@ -133,7 +131,6 @@ type Attributes = Readonly<Record<string, string | number | boolean | undefined>
133
131
  * failure with nothing else to say.
134
132
  */
135
133
  type LoggerService = {
136
- readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
137
134
  readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
138
135
  readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
139
136
  readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
@@ -257,11 +254,12 @@ type Operation = {
257
254
  /**
258
255
  * Whether a tracing observer should open a span (default `true`).
259
256
  *
260
- * `false` is for a component whose spans already come from somewhere better:
261
- * `@btravstack/prisma` says so because `@prisma/instrumentation` traces at the
262
- * ENGINE level — the real SQL, the connection acquisition — all of it below
263
- * what a client-level wrapper can see, so a second span would cost one more
264
- * per query for strictly less information. Counting and timing still happen.
257
+ * `false` is for an operation a span would say nothing about — one whose
258
+ * spans already come from somewhere better, or one with no duration at all.
259
+ * Counting and timing still happen. `@btravstack/amqp-worker`'s broker
260
+ * diagnostics pass it, being zero-length events; `@btravstack/prisma` no
261
+ * longer does, since Prisma 8 ships no engine tracer and its middleware is
262
+ * the only span there is.
265
263
  *
266
264
  * Whether a span is worth opening is the CONTRIBUTOR's knowledge, which is
267
265
  * why it rides the operation rather than being configured on the observer.
@@ -836,4 +834,112 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
836
834
  */
837
835
  export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
838
836
  //#endregion
839
- export type { Attributes, Clock, ComponentHealth, Counter, DrainReport, EventSink, ExitReport, HealthCheck, HealthReport, Histogram, InstrumentationLoader, KernelEvent, Level, LoggerService, MeterService, Operation, Phase, RunUnit, RunningApp, Runtime, RuntimeHost, RuntimeInfoOf, Serving, Settle, Settled, Span, SpanStatusCode, StartGate, StartOptions, TeardownError, TracerService, UnitHost, UnitMeta, UnitRecord, UnitRegistry, UnitWork };
837
+ //#region src/unit-record.d.ts
838
+ /**
839
+ * A module a runtime's `unit` option may bind, as the upper bound a starter
840
+ * constrains its own `Unit` type parameter to. `Module`'s `_exports` channel is
841
+ * contravariant, so `Exports = never` — never `unknown` — is what makes a REAL
842
+ * module's own (necessarily narrower) export type assignable to this bound:
843
+ * `(x: Concrete) => void` is assignable to `(x: never) => void`, not to
844
+ * `(x: unknown) => void`.
845
+ */
846
+ type AnyUnitModule = Module<never, never, unknown>;
847
+ /**
848
+ * The needs a bound unit module still owes, or `never` when none is bound.
849
+ * `Scope` is excluded, since nothing can ever provide it — the same exemption
850
+ * `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
851
+ * fork seed discharges.
852
+ */
853
+ type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
854
+ /** What a bound unit module exports — the port instances a piece of that kind may read. */
855
+ type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
856
+ /** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
857
+ type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
858
+ /**
859
+ * A piece injecting a port the bound unit module does not export. `unknown`
860
+ * when every declared port is covered — including the empty case, so a worker
861
+ * whose pieces declare no `unit:` is gated on nothing.
862
+ */
863
+ type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
864
+ readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
865
+ };
866
+ /**
867
+ * One `(helpers, input)` implementation with its piece's declared record put on
868
+ * `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
869
+ * Applied once per piece as di constructs it, so a unit costs one record and
870
+ * one context object. Each worker maps it over its own entry shape.
871
+ */
872
+ export declare const withUnitRecord: (record: Readonly<Record<string, AnyPort>>, implementation: (helpers: never, input: never) => unknown) => (helpers: {
873
+ readonly context?: Readonly<Record<string | symbol, unknown>>;
874
+ }, input: never) => unknown;
875
+ /**
876
+ * A worker middleware's body: observe `operation`, open one kernel unit, fork
877
+ * `unit` — when one is bound — seeded with `seed`, and call `next` with the
878
+ * forked context where {@link withUnitRecord} finds it. With no `unit` bound,
879
+ * `next()` runs unchanged.
880
+ *
881
+ * Observed from whichever channel the unit settles on, a defect included: a
882
+ * defect is a failed unit too, and an errors count that omitted it would be the
883
+ * reassuring half.
884
+ */
885
+ export declare const dispatchUnit: <T, E>(dispatch: {
886
+ readonly host: RuntimeHost<never>;
887
+ readonly observers: readonly ((operation: Operation) => Settle)[];
888
+ readonly operation: Operation;
889
+ readonly meta: UnitMeta;
890
+ readonly unit: AnyUnitModule | undefined;
891
+ readonly seed: readonly [AnyPort, unknown];
892
+ readonly next: (overrides?: never) => AsyncResult<T, E>;
893
+ }) => AsyncResult<T, E>;
894
+ //#endregion
895
+ //#region src/pieces.d.ts
896
+ /**
897
+ * A refused array: as long as the array the caller wrote, its head the caller's
898
+ * own elements — which match — and its LAST element the marker paired with what
899
+ * is wrong.
900
+ *
901
+ * TypeScript compares two equal-length tuples element by element, so the extra
902
+ * diagnostic it reports lands on the trailing element and carries both the
903
+ * sentence and the missing key. A fixed two-element tuple named the key only
904
+ * when the array happened to be two elements long; every other arity was a
905
+ * length mismatch, and the developer diffed the contract against the array by
906
+ * hand.
907
+ */
908
+ type Refuse<T extends readonly unknown[], Marker extends string, Detail> = T extends readonly [...infer Head, unknown] ? readonly [...Head, readonly [Marker, Detail]] : readonly [readonly [Marker, Detail]];
909
+ /** How a starter puts a piece's declared `unit:` record on one of its entries. */
910
+ type EntryWrapper = (record: Readonly<Record<string, AnyPort>>, entry: unknown) => unknown;
911
+ /** A piece's `{ inject, unit?, sync }` options, with the types each starter states erased. */
912
+ type PieceOptions = {
913
+ readonly inject: Readonly<Record<string, AnyPort>>;
914
+ readonly unit?: Readonly<Record<string, AnyPort>>;
915
+ readonly sync: (services: never) => unknown;
916
+ };
917
+ /**
918
+ * The runtime half of a starter's piece factory — `AmqpHandler(contract, key)`,
919
+ * `TemporalWorkflowActivities(contract, key)`: a provider on `port` whose
920
+ * service is what `sync` answers, wrapped once by `withUnit` so it reads the
921
+ * record the piece declared. The starter keeps the types; this keeps the body.
922
+ */
923
+ export declare const mintPiece: (port: AnyPort, withUnit: EntryWrapper) => (options: PieceOptions) => unknown;
924
+ /**
925
+ * The runtime half of a starter's composing provider — `AmqpHandlers(contract)`,
926
+ * `TemporalActivities(contract)` — and its two arms. An array of pieces is
927
+ * composed into one record keyed by each piece's port id less `prefix`, so the
928
+ * services record IS the composed record; a `{ inject, unit?, sync }` record
929
+ * has `withUnit` applied to every entry `sync` answers.
930
+ */
931
+ export declare const composeByPrefix: (port: AnyPort, prefix: string, withUnit: EntryWrapper) => ((first: unknown) => unknown);
932
+ //#endregion
933
+ //#region src/starter.d.ts
934
+ /**
935
+ * The no-op member every module reading {@link Observers} provides, so the set
936
+ * it reads is never the empty dependency di refuses: a graph composing no
937
+ * observability still starts. One provider, shared — di de-duplicates a graph's
938
+ * providers by reference, so however many modules contribute it, an operation
939
+ * costs one inert call.
940
+ */
941
+ export declare const noObserverMember: Provider<Observers, never, never> & {
942
+ readonly port: typeof Observers;
943
+ };
944
+ //#endregion
945
+ export type { AnyUnitModule, Attributes, Clock, ComponentHealth, Counter, DrainReport, EventSink, ExitReport, HealthCheck, HealthReport, Histogram, InstrumentationLoader, KernelEvent, Level, LoggerService, MeterService, Operation, Phase, Refuse, RunUnit, RunningApp, Runtime, RuntimeHost, RuntimeInfoOf, Serving, Settle, Settled, Span, SpanStatusCode, StartGate, StartOptions, TeardownError, TracerService, UnitExportsOf, UnitGate, UnitHost, UnitMeta, UnitNeedsOf, UnitRecord, UnitRecordOf, UnitRegistry, UnitWork };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Module, Port, Provider } from "@btravstack/di";
2
2
  import { Err, Ok, OkAsync, P, TaggedError, allAsync, fromSafePromise } from "unthrown";
3
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
3
4
  import { Config, ConfigInvalid, Env } from "@btravstack/config";
4
5
  import { createServer } from "node:http";
5
6
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -68,10 +69,11 @@ const answeringWithin = (health, ms) => {
68
69
  * rather than a recovery.** Neither recovery above can see one: there is no
69
70
  * error and no defect, only silence, so `/healthz` held a socket open per hit
70
71
  * while kubelet timed out against a report that named nothing. Each check now
71
- * races {@link HealthOptions.timeoutMs}, and the component that hung is named
72
+ * races its own `timeoutMs` (default {@link DEFAULT_HEALTH_TIMEOUT_MS}), and
73
+ * the component that hung is named
72
74
  * in the report like any other unhealthy one.
73
75
  */
74
- const runHealthChecks = (checks, options = {}) => allAsync(checks.map((health) => answeringWithin(health, health.timeoutMs ?? options.timeoutMs ?? 800).map(() => ({
76
+ const runHealthChecks = (checks) => allAsync(checks.map((health) => answeringWithin(health, health.timeoutMs ?? 800).map(() => ({
75
77
  name: health.name,
76
78
  status: "healthy"
77
79
  })).recoverErrCases((matcher) => matcher.with(P.tag("HealthCheckFailed"), (error) => ({
@@ -152,22 +154,10 @@ const observed = (observers, operation, call, settled = {}) => {
152
154
  //#region src/clock.ts
153
155
  const systemClock = {
154
156
  now: () => Date.now(),
155
- sleep: (ms, signal) => fromSafePromise(new Promise((resolve) => {
156
- if (signal?.aborted === true) {
157
- resolve();
158
- return;
159
- }
160
- const timer = setTimeout(() => {
161
- signal?.removeEventListener("abort", onAbort);
162
- resolve();
163
- }, ms);
164
- timer.unref?.();
165
- function onAbort() {
166
- clearTimeout(timer);
167
- resolve();
168
- }
169
- signal?.addEventListener("abort", onAbort, { once: true });
170
- }))
157
+ sleep: (ms, signal) => fromSafePromise(setTimeout$1(ms, void 0, {
158
+ signal,
159
+ ref: false
160
+ }).catch(() => {}))
171
161
  };
172
162
  //#endregion
173
163
  //#region src/events.ts
@@ -620,6 +610,11 @@ const start = (module, options = {}) => {
620
610
  const health = () => runHealthChecks(healthChecks);
621
611
  const probeBound = Promise.withResolvers();
622
612
  const runtimePublished = Promise.withResolvers();
613
+ const leaveStartup = () => {
614
+ runtimePublished.resolve(void 0);
615
+ tracker.advanceTo("stopping");
616
+ disposeAll();
617
+ };
623
618
  const stopping = Promise.withResolvers();
624
619
  const stopAbandoned = () => fromSafePromise(stopping.promise).flatMap(({ reason, drain }) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : OkAsync(abandonStop(reason, drain))));
625
620
  const abandonStop = (reason, drain) => {
@@ -639,9 +634,7 @@ const start = (module, options = {}) => {
639
634
  afterMs: void 0
640
635
  });
641
636
  registry.abortAll();
642
- runtimePublished.resolve(void 0);
643
- tracker.advanceTo("stopping");
644
- disposeAll();
637
+ leaveStartup();
645
638
  return reportOf(reason, void 0, "build");
646
639
  });
647
640
  const abandonBuild = (reason) => {
@@ -672,11 +665,7 @@ const start = (module, options = {}) => {
672
665
  cause: failure.tag === "Err" ? failure.error : failure.cause
673
666
  });
674
667
  probeBound.resolve(void 0);
675
- runtimePublished.resolve(void 0);
676
- tracker.advanceTo("stopping");
677
- disposeSignals();
678
- disposeUncaught();
679
- tracker.advanceTo("exited");
668
+ leaveStartup();
680
669
  });
681
670
  const root = Module("Kernel")({
682
671
  imports: providesEnv(module) ? [module] : [module, Module("Environment")({
@@ -793,9 +782,7 @@ const start = (module, options = {}) => {
793
782
  type: "startFailed",
794
783
  cause: failure.tag === "Err" ? failure.error : failure.cause
795
784
  });
796
- runtimePublished.resolve(void 0);
797
- tracker.advanceTo("stopping");
798
- disposeAll();
785
+ leaveStartup();
799
786
  }).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
800
787
  return {
801
788
  exited: probesStarted.flatMap(() => fromSafePromise(Promise.race([
@@ -871,4 +858,95 @@ const runMain = async (module, options = {}, exit = (code) => {
871
858
  await awaitExit(start(module, options), exit);
872
859
  };
873
860
  //#endregion
874
- export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, Observers, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, noObserver, observe, observed, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock, traceIdOfTraceparent };
861
+ //#region src/unit-record.ts
862
+ /**
863
+ * Where {@link dispatchUnit} leaves the forked context for {@link withUnitRecord}
864
+ * to read. A symbol rather than a name: the library merges it into the context
865
+ * every piece sees, and a string key would appear on the record a piece written
866
+ * against the `{ inject, sync }` arm destructures.
867
+ */
868
+ const UNIT_SCOPE = Symbol("@btravstack/core/unit-scope");
869
+ /**
870
+ * The declared record, as a getter per name resolved on read out of the fork.
871
+ * Neither writable nor configurable — a piece reads what the fork holds, and
872
+ * cannot reshape the record under the next unit. With no unit module bound
873
+ * there is nothing to resolve from, so the record is empty whatever was
874
+ * declared.
875
+ */
876
+ const unitRecordOf = (forked, record) => {
877
+ const unit = {};
878
+ if (forked === void 0) return unit;
879
+ for (const [name, port] of Object.entries(record)) Object.defineProperty(unit, name, {
880
+ enumerable: true,
881
+ get: () => forked.get(port)
882
+ });
883
+ return unit;
884
+ };
885
+ /**
886
+ * One `(helpers, input)` implementation with its piece's declared record put on
887
+ * `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
888
+ * Applied once per piece as di constructs it, so a unit costs one record and
889
+ * one context object. Each worker maps it over its own entry shape.
890
+ */
891
+ const withUnitRecord = (record, implementation) => (helpers, input) => {
892
+ const { [UNIT_SCOPE]: forked, ...rest } = helpers.context ?? {};
893
+ return implementation({
894
+ ...helpers,
895
+ context: {
896
+ ...rest,
897
+ unit: unitRecordOf(forked, record)
898
+ }
899
+ }, input);
900
+ };
901
+ /**
902
+ * A worker middleware's body: observe `operation`, open one kernel unit, fork
903
+ * `unit` — when one is bound — seeded with `seed`, and call `next` with the
904
+ * forked context where {@link withUnitRecord} finds it. With no `unit` bound,
905
+ * `next()` runs unchanged.
906
+ *
907
+ * Observed from whichever channel the unit settles on, a defect included: a
908
+ * defect is a failed unit too, and an errors count that omitted it would be the
909
+ * reassuring half.
910
+ */
911
+ const dispatchUnit = (dispatch) => observed(dispatch.observers, dispatch.operation, () => dispatch.host.run(dispatch.meta, (scope) => dispatch.unit === void 0 ? dispatch.next() : scope.fork(dispatch.unit, [dispatch.seed]).flatMap((forked) => dispatch.next({ context: { [UNIT_SCOPE]: forked } }))));
912
+ //#endregion
913
+ //#region src/pieces.ts
914
+ /**
915
+ * The runtime half of a starter's piece factory — `AmqpHandler(contract, key)`,
916
+ * `TemporalWorkflowActivities(contract, key)`: a provider on `port` whose
917
+ * service is what `sync` answers, wrapped once by `withUnit` so it reads the
918
+ * record the piece declared. The starter keeps the types; this keeps the body.
919
+ */
920
+ const mintPiece = (port, withUnit) => (options) => Provider(port)({
921
+ inject: options.inject,
922
+ sync: (services) => withUnit(options.unit ?? {}, options.sync(services))
923
+ });
924
+ /**
925
+ * The runtime half of a starter's composing provider — `AmqpHandlers(contract)`,
926
+ * `TemporalActivities(contract)` — and its two arms. An array of pieces is
927
+ * composed into one record keyed by each piece's port id less `prefix`, so the
928
+ * services record IS the composed record; a `{ inject, unit?, sync }` record
929
+ * has `withUnit` applied to every entry `sync` answers.
930
+ */
931
+ const composeByPrefix = (port, prefix, withUnit) => {
932
+ const whole = mintPiece(port, (record, entries) => Object.fromEntries(Object.entries(entries).map(([key, entry]) => [key, withUnit(record, entry)])));
933
+ return (first) => Array.isArray(first) ? Provider(port)({
934
+ inject: Object.fromEntries(first.map((piece) => [piece.port.portId.slice(prefix.length), piece.port])),
935
+ sync: (services) => services
936
+ }) : whole(first);
937
+ };
938
+ //#endregion
939
+ //#region src/starter.ts
940
+ /**
941
+ * The no-op member every module reading {@link Observers} provides, so the set
942
+ * it reads is never the empty dependency di refuses: a graph composing no
943
+ * observability still starts. One provider, shared — di de-duplicates a graph's
944
+ * providers by reference, so however many modules contribute it, an operation
945
+ * costs one inert call.
946
+ */
947
+ const noObserverMember = Provider.member(Observers)({
948
+ inject: {},
949
+ value: noObserver
950
+ });
951
+ //#endregion
952
+ export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, Observers, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, composeByPrefix, currentUnit, dispatchUnit, mintPiece, noObserver, noObserverMember, observe, observed, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock, traceIdOfTraceparent, withUnitRecord };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/core",
3
- "version": "0.15.0",
3
+ "version": "0.17.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,20 +45,20 @@
45
45
  "./package.json": "./package.json"
46
46
  },
47
47
  "devDependencies": {
48
- "@btravstack/config": "0.15.0",
49
- "@btravstack/di": "0.15.0",
48
+ "@btravstack/config": "0.17.0",
49
+ "@btravstack/di": "0.17.0",
50
50
  "@btravstack/tsconfig": "0.4.0",
51
- "@types/node": "26.4.1",
52
- "@unthrown/vitest": "5.8.0",
53
- "@vitest/coverage-v8": "4.1.11",
51
+ "@types/node": "26.6.4",
52
+ "@unthrown/vitest": "5.12.0",
53
+ "@vitest/coverage-v8": "5.0.3",
54
54
  "tsdown": "0.23.0",
55
55
  "typescript": "7.0.2",
56
- "unthrown": "5.8.0",
57
- "vitest": "4.1.11"
56
+ "unthrown": "5.12.0",
57
+ "vitest": "5.0.3"
58
58
  },
59
59
  "peerDependencies": {
60
- "@btravstack/config": "^0.15.0",
61
- "@btravstack/di": "^0.15.0",
60
+ "@btravstack/config": "^0.17.0",
61
+ "@btravstack/di": "^0.17.0",
62
62
  "unthrown": "^5.0.0"
63
63
  },
64
64
  "engines": {