@btravstack/core 0.16.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 +112 -29
- package/dist/index.d.cts +125 -23
- package/dist/index.d.mts +125 -23
- package/dist/index.mjs +108 -30
- package/package.json +10 -10
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
|
|
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
|
|
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)(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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
|
|
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[]
|
|
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,15 +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
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* Prisma 7's engine-level `@prisma/instrumentation` was the better source;
|
|
267
|
-
* Prisma 8 has no engine and ships no tracer, so its middleware is the only
|
|
268
|
-
* span there is and it opens one.
|
|
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.
|
|
269
263
|
*
|
|
270
264
|
* Whether a span is worth opening is the CONTRIBUTOR's knowledge, which is
|
|
271
265
|
* why it rides the operation rather than being configured on the observer.
|
|
@@ -840,4 +834,112 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
|
|
|
840
834
|
*/
|
|
841
835
|
export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
|
|
842
836
|
//#endregion
|
|
843
|
-
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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
|
|
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[]
|
|
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,15 +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
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* Prisma 7's engine-level `@prisma/instrumentation` was the better source;
|
|
267
|
-
* Prisma 8 has no engine and ships no tracer, so its middleware is the only
|
|
268
|
-
* span there is and it opens one.
|
|
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.
|
|
269
263
|
*
|
|
270
264
|
* Whether a span is worth opening is the CONTRIBUTOR's knowledge, which is
|
|
271
265
|
* why it rides the operation rather than being configured on the observer.
|
|
@@ -840,4 +834,112 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
|
|
|
840
834
|
*/
|
|
841
835
|
export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
|
|
842
836
|
//#endregion
|
|
843
|
-
|
|
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
|
|
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
|
|
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(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
49
|
-
"@btravstack/di": "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
|
|
52
|
-
"@unthrown/vitest": "5.
|
|
53
|
-
"@vitest/coverage-v8": "
|
|
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.
|
|
57
|
-
"vitest": "
|
|
56
|
+
"unthrown": "5.12.0",
|
|
57
|
+
"vitest": "5.0.3"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@btravstack/config": "^0.
|
|
61
|
-
"@btravstack/di": "^0.
|
|
60
|
+
"@btravstack/config": "^0.17.0",
|
|
61
|
+
"@btravstack/di": "^0.17.0",
|
|
62
62
|
"unthrown": "^5.0.0"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|