@btravstack/core 0.16.0 → 0.18.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 +137 -35
- package/dist/index.d.cts +139 -24
- package/dist/index.d.mts +139 -24
- package/dist/index.mjs +132 -36
- package/package.json +12 -11
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
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");
|
|
8
|
+
let node_crypto = require("node:crypto");
|
|
7
9
|
//#region src/instrumentation.ts
|
|
8
10
|
/**
|
|
9
11
|
* The set port a package contributes its instrumentation to.
|
|
@@ -69,10 +71,11 @@ const answeringWithin = (health, ms) => {
|
|
|
69
71
|
* rather than a recovery.** Neither recovery above can see one: there is no
|
|
70
72
|
* error and no defect, only silence, so `/healthz` held a socket open per hit
|
|
71
73
|
* while kubelet timed out against a report that named nothing. Each check now
|
|
72
|
-
* races {@link
|
|
74
|
+
* races its own `timeoutMs` (default {@link DEFAULT_HEALTH_TIMEOUT_MS}), and
|
|
75
|
+
* the component that hung is named
|
|
73
76
|
* in the report like any other unhealthy one.
|
|
74
77
|
*/
|
|
75
|
-
const runHealthChecks = (checks
|
|
78
|
+
const runHealthChecks = (checks) => (0, unthrown.allAsync)(checks.map((health) => answeringWithin(health, health.timeoutMs ?? 800).map(() => ({
|
|
76
79
|
name: health.name,
|
|
77
80
|
status: "healthy"
|
|
78
81
|
})).recoverErrCases((matcher) => matcher.with(unthrown.P.tag("HealthCheckFailed"), (error) => ({
|
|
@@ -153,22 +156,10 @@ const observed = (observers, operation, call, settled = {}) => {
|
|
|
153
156
|
//#region src/clock.ts
|
|
154
157
|
const systemClock = {
|
|
155
158
|
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
|
-
}))
|
|
159
|
+
sleep: (ms, signal) => (0, unthrown.fromSafePromise)((0, node_timers_promises.setTimeout)(ms, void 0, {
|
|
160
|
+
signal,
|
|
161
|
+
ref: false
|
|
162
|
+
}).catch(() => {}))
|
|
172
163
|
};
|
|
173
164
|
//#endregion
|
|
174
165
|
//#region src/events.ts
|
|
@@ -442,10 +433,22 @@ const installUncaughtHandlers = (onUncaught) => {
|
|
|
442
433
|
const storage = new node_async_hooks.AsyncLocalStorage();
|
|
443
434
|
const runWithUnit = (record, fn) => storage.run(record, fn);
|
|
444
435
|
const currentUnit = () => storage.getStore();
|
|
445
|
-
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
436
|
+
const outcomes = /* @__PURE__ */ new WeakMap();
|
|
437
|
+
const settleUnit = (record, outcome) => {
|
|
438
|
+
outcomes.set(record, outcome);
|
|
439
|
+
};
|
|
440
|
+
/**
|
|
441
|
+
* How the current unit's work settled, for the code that runs AFTER it: a
|
|
442
|
+
* unit module's `onStop`, which tears down inside the record but is handed
|
|
443
|
+
* only its own service. `undefined` outside a unit and while the work is still
|
|
444
|
+
* running. Whether the unit was aborted is the record's own `signal`.
|
|
445
|
+
*
|
|
446
|
+
* It is the kernel's outcome, not the transport's: an HTTP unit that answered
|
|
447
|
+
* a `500` still settled `ok`, since the kernel never maps a status.
|
|
448
|
+
*/
|
|
449
|
+
const unitOutcome = () => {
|
|
450
|
+
const unit = currentUnit();
|
|
451
|
+
return unit === void 0 ? void 0 : outcomes.get(unit);
|
|
449
452
|
};
|
|
450
453
|
const createUnitRegistry = () => {
|
|
451
454
|
const open = /* @__PURE__ */ new Set();
|
|
@@ -461,7 +464,7 @@ const createUnitRegistry = () => {
|
|
|
461
464
|
const controller = new AbortController();
|
|
462
465
|
open.add(controller);
|
|
463
466
|
const record = {
|
|
464
|
-
unitId:
|
|
467
|
+
unitId: (0, node_crypto.randomUUID)(),
|
|
465
468
|
traceId: meta.traceId ?? meta.id,
|
|
466
469
|
tenantId: meta.tenantId,
|
|
467
470
|
signal: controller.signal
|
|
@@ -621,6 +624,11 @@ const start = (module, options = {}) => {
|
|
|
621
624
|
const health = () => runHealthChecks(healthChecks);
|
|
622
625
|
const probeBound = Promise.withResolvers();
|
|
623
626
|
const runtimePublished = Promise.withResolvers();
|
|
627
|
+
const leaveStartup = () => {
|
|
628
|
+
runtimePublished.resolve(void 0);
|
|
629
|
+
tracker.advanceTo("stopping");
|
|
630
|
+
disposeAll();
|
|
631
|
+
};
|
|
624
632
|
const stopping = Promise.withResolvers();
|
|
625
633
|
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
634
|
const abandonStop = (reason, drain) => {
|
|
@@ -640,9 +648,7 @@ const start = (module, options = {}) => {
|
|
|
640
648
|
afterMs: void 0
|
|
641
649
|
});
|
|
642
650
|
registry.abortAll();
|
|
643
|
-
|
|
644
|
-
tracker.advanceTo("stopping");
|
|
645
|
-
disposeAll();
|
|
651
|
+
leaveStartup();
|
|
646
652
|
return reportOf(reason, void 0, "build");
|
|
647
653
|
});
|
|
648
654
|
const abandonBuild = (reason) => {
|
|
@@ -673,11 +679,7 @@ const start = (module, options = {}) => {
|
|
|
673
679
|
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
674
680
|
});
|
|
675
681
|
probeBound.resolve(void 0);
|
|
676
|
-
|
|
677
|
-
tracker.advanceTo("stopping");
|
|
678
|
-
disposeSignals();
|
|
679
|
-
disposeUncaught();
|
|
680
|
-
tracker.advanceTo("exited");
|
|
682
|
+
leaveStartup();
|
|
681
683
|
});
|
|
682
684
|
const root = (0, _btravstack_di.Module)("Kernel")({
|
|
683
685
|
imports: providesEnv(module) ? [module] : [module, (0, _btravstack_di.Module)("Environment")({
|
|
@@ -752,13 +754,18 @@ const start = (module, options = {}) => {
|
|
|
752
754
|
}).get();
|
|
753
755
|
return (0, unthrown.fromSafePromise)(ready.promise);
|
|
754
756
|
};
|
|
757
|
+
const record = currentUnit();
|
|
755
758
|
const outcome = (async () => {
|
|
759
|
+
let settledAs = "error";
|
|
756
760
|
try {
|
|
757
|
-
|
|
761
|
+
const result = await work({
|
|
758
762
|
ctx: runtimeCtx,
|
|
759
763
|
fork
|
|
760
764
|
}, signal);
|
|
765
|
+
if (result.isOk()) settledAs = "ok";
|
|
766
|
+
return result;
|
|
761
767
|
} finally {
|
|
768
|
+
if (record !== void 0) settleUnit(record, settledAs);
|
|
762
769
|
hasSettled = true;
|
|
763
770
|
settled.resolve();
|
|
764
771
|
if (closing !== void 0) await closing;
|
|
@@ -794,9 +801,7 @@ const start = (module, options = {}) => {
|
|
|
794
801
|
type: "startFailed",
|
|
795
802
|
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
796
803
|
});
|
|
797
|
-
|
|
798
|
-
tracker.advanceTo("stopping");
|
|
799
|
-
disposeAll();
|
|
804
|
+
leaveStartup();
|
|
800
805
|
}).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
|
|
801
806
|
return {
|
|
802
807
|
exited: probesStarted.flatMap(() => (0, unthrown.fromSafePromise)(Promise.race([
|
|
@@ -872,6 +877,97 @@ const runMain = async (module, options = {}, exit = (code) => {
|
|
|
872
877
|
await awaitExit(start(module, options), exit);
|
|
873
878
|
};
|
|
874
879
|
//#endregion
|
|
880
|
+
//#region src/unit-record.ts
|
|
881
|
+
/**
|
|
882
|
+
* Where {@link dispatchUnit} leaves the forked context for {@link withUnitRecord}
|
|
883
|
+
* to read. A symbol rather than a name: the library merges it into the context
|
|
884
|
+
* every piece sees, and a string key would appear on the record a piece written
|
|
885
|
+
* against the `{ inject, sync }` arm destructures.
|
|
886
|
+
*/
|
|
887
|
+
const UNIT_SCOPE = Symbol("@btravstack/core/unit-scope");
|
|
888
|
+
/**
|
|
889
|
+
* The declared record, as a getter per name resolved on read out of the fork.
|
|
890
|
+
* Neither writable nor configurable — a piece reads what the fork holds, and
|
|
891
|
+
* cannot reshape the record under the next unit. With no unit module bound
|
|
892
|
+
* there is nothing to resolve from, so the record is empty whatever was
|
|
893
|
+
* declared.
|
|
894
|
+
*/
|
|
895
|
+
const unitRecordOf = (forked, record) => {
|
|
896
|
+
const unit = {};
|
|
897
|
+
if (forked === void 0) return unit;
|
|
898
|
+
for (const [name, port] of Object.entries(record)) Object.defineProperty(unit, name, {
|
|
899
|
+
enumerable: true,
|
|
900
|
+
get: () => forked.get(port)
|
|
901
|
+
});
|
|
902
|
+
return unit;
|
|
903
|
+
};
|
|
904
|
+
/**
|
|
905
|
+
* One `(helpers, input)` implementation with its piece's declared record put on
|
|
906
|
+
* `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
|
|
907
|
+
* Applied once per piece as di constructs it, so a unit costs one record and
|
|
908
|
+
* one context object. Each worker maps it over its own entry shape.
|
|
909
|
+
*/
|
|
910
|
+
const withUnitRecord = (record, implementation) => (helpers, input) => {
|
|
911
|
+
const { [UNIT_SCOPE]: forked, ...rest } = helpers.context ?? {};
|
|
912
|
+
return implementation({
|
|
913
|
+
...helpers,
|
|
914
|
+
context: {
|
|
915
|
+
...rest,
|
|
916
|
+
unit: unitRecordOf(forked, record)
|
|
917
|
+
}
|
|
918
|
+
}, input);
|
|
919
|
+
};
|
|
920
|
+
/**
|
|
921
|
+
* A worker middleware's body: observe `operation`, open one kernel unit, fork
|
|
922
|
+
* `unit` — when one is bound — seeded with `seed`, and call `next` with the
|
|
923
|
+
* forked context where {@link withUnitRecord} finds it. With no `unit` bound,
|
|
924
|
+
* `next()` runs unchanged.
|
|
925
|
+
*
|
|
926
|
+
* Observed from whichever channel the unit settles on, a defect included: a
|
|
927
|
+
* defect is a failed unit too, and an errors count that omitted it would be the
|
|
928
|
+
* reassuring half.
|
|
929
|
+
*/
|
|
930
|
+
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 } }))));
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region src/pieces.ts
|
|
933
|
+
/**
|
|
934
|
+
* The runtime half of a starter's piece factory — `AmqpHandler(contract, key)`,
|
|
935
|
+
* `TemporalWorkflowActivities(contract, key)`: a provider on `port` whose
|
|
936
|
+
* service is what `sync` answers, wrapped once by `withUnit` so it reads the
|
|
937
|
+
* record the piece declared. The starter keeps the types; this keeps the body.
|
|
938
|
+
*/
|
|
939
|
+
const mintPiece = (port, withUnit) => (options) => (0, _btravstack_di.Provider)(port)({
|
|
940
|
+
inject: options.inject,
|
|
941
|
+
sync: (services) => withUnit(options.unit ?? {}, options.sync(services))
|
|
942
|
+
});
|
|
943
|
+
/**
|
|
944
|
+
* The runtime half of a starter's composing provider — `AmqpHandlers(contract)`,
|
|
945
|
+
* `TemporalActivities(contract)` — and its two arms. An array of pieces is
|
|
946
|
+
* composed into one record keyed by each piece's port id less `prefix`, so the
|
|
947
|
+
* services record IS the composed record; a `{ inject, unit?, sync }` record
|
|
948
|
+
* has `withUnit` applied to every entry `sync` answers.
|
|
949
|
+
*/
|
|
950
|
+
const composeByPrefix = (port, prefix, withUnit) => {
|
|
951
|
+
const whole = mintPiece(port, (record, entries) => Object.fromEntries(Object.entries(entries).map(([key, entry]) => [key, withUnit(record, entry)])));
|
|
952
|
+
return (first) => Array.isArray(first) ? (0, _btravstack_di.Provider)(port)({
|
|
953
|
+
inject: Object.fromEntries(first.map((piece) => [piece.port.portId.slice(prefix.length), piece.port])),
|
|
954
|
+
sync: (services) => services
|
|
955
|
+
}) : whole(first);
|
|
956
|
+
};
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/starter.ts
|
|
959
|
+
/**
|
|
960
|
+
* The no-op member every module reading {@link Observers} provides, so the set
|
|
961
|
+
* it reads is never the empty dependency di refuses: a graph composing no
|
|
962
|
+
* observability still starts. One provider, shared — di de-duplicates a graph's
|
|
963
|
+
* providers by reference, so however many modules contribute it, an operation
|
|
964
|
+
* costs one inert call.
|
|
965
|
+
*/
|
|
966
|
+
const noObserverMember = _btravstack_di.Provider.member(Observers)({
|
|
967
|
+
inject: {},
|
|
968
|
+
value: noObserver
|
|
969
|
+
});
|
|
970
|
+
//#endregion
|
|
875
971
|
exports.HealthCheckFailed = HealthCheckFailed;
|
|
876
972
|
exports.HealthChecks = HealthChecks;
|
|
877
973
|
exports.Instrumentations = Instrumentations;
|
|
@@ -883,8 +979,12 @@ exports.RuntimePort = RuntimePort;
|
|
|
883
979
|
exports.RuntimeStartFailed = RuntimeStartFailed;
|
|
884
980
|
exports.SPAN_STATUS = SPAN_STATUS;
|
|
885
981
|
exports.Tracer = Tracer;
|
|
982
|
+
exports.composeByPrefix = composeByPrefix;
|
|
886
983
|
exports.currentUnit = currentUnit;
|
|
984
|
+
exports.dispatchUnit = dispatchUnit;
|
|
985
|
+
exports.mintPiece = mintPiece;
|
|
887
986
|
exports.noObserver = noObserver;
|
|
987
|
+
exports.noObserverMember = noObserverMember;
|
|
888
988
|
exports.observe = observe;
|
|
889
989
|
exports.observed = observed;
|
|
890
990
|
exports.releasedBy = releasedBy;
|
|
@@ -894,3 +994,5 @@ exports.start = start;
|
|
|
894
994
|
exports.stderrSink = stderrSink;
|
|
895
995
|
exports.systemClock = systemClock;
|
|
896
996
|
exports.traceIdOfTraceparent = traceIdOfTraceparent;
|
|
997
|
+
exports.unitOutcome = unitOutcome;
|
|
998
|
+
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.
|
|
@@ -358,6 +352,18 @@ type UnitRecord = {
|
|
|
358
352
|
readonly signal: AbortSignal;
|
|
359
353
|
};
|
|
360
354
|
export declare const currentUnit: () => UnitRecord | undefined;
|
|
355
|
+
/** How a unit's work settled: `ok`, or `error` for an `Err`, a `Defect` and a throw alike. */
|
|
356
|
+
type UnitOutcome = "ok" | "error";
|
|
357
|
+
/**
|
|
358
|
+
* How the current unit's work settled, for the code that runs AFTER it: a
|
|
359
|
+
* unit module's `onStop`, which tears down inside the record but is handed
|
|
360
|
+
* only its own service. `undefined` outside a unit and while the work is still
|
|
361
|
+
* running. Whether the unit was aborted is the record's own `signal`.
|
|
362
|
+
*
|
|
363
|
+
* It is the kernel's outcome, not the transport's: an HTTP unit that answered
|
|
364
|
+
* a `500` still settled `ok`, since the kernel never maps a status.
|
|
365
|
+
*/
|
|
366
|
+
export declare const unitOutcome: () => UnitOutcome | undefined;
|
|
361
367
|
/**
|
|
362
368
|
* What a runtime says about one piece of work as it submits it. `kind` is the
|
|
363
369
|
* category (`"http"`, `"tick"`, `"job"`); `id` identifies **this** unit.
|
|
@@ -369,7 +375,8 @@ export declare const currentUnit: () => UnitRecord | undefined;
|
|
|
369
375
|
* silently defeats the ambient record. A route template is a `kind`.
|
|
370
376
|
*
|
|
371
377
|
* The kernel cannot check this, so uniqueness is the runtime's to guarantee.
|
|
372
|
-
* What it does guarantee is {@link UnitRecord}'s `unitId`, minted per
|
|
378
|
+
* What it does guarantee is {@link UnitRecord}'s `unitId`, a UUID minted per
|
|
379
|
+
* unit, so no two units share one across replicas either;
|
|
373
380
|
* `traceId` is the CORRELATION id, which is why it is the one a runtime may
|
|
374
381
|
* supply — it carries an id from outside the process.
|
|
375
382
|
*/
|
|
@@ -840,4 +847,112 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
|
|
|
840
847
|
*/
|
|
841
848
|
export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
|
|
842
849
|
//#endregion
|
|
843
|
-
|
|
850
|
+
//#region src/unit-record.d.ts
|
|
851
|
+
/**
|
|
852
|
+
* A module a runtime's `unit` option may bind, as the upper bound a starter
|
|
853
|
+
* constrains its own `Unit` type parameter to. `Module`'s `_exports` channel is
|
|
854
|
+
* contravariant, so `Exports = never` — never `unknown` — is what makes a REAL
|
|
855
|
+
* module's own (necessarily narrower) export type assignable to this bound:
|
|
856
|
+
* `(x: Concrete) => void` is assignable to `(x: never) => void`, not to
|
|
857
|
+
* `(x: unknown) => void`.
|
|
858
|
+
*/
|
|
859
|
+
type AnyUnitModule = Module<never, never, unknown>;
|
|
860
|
+
/**
|
|
861
|
+
* The needs a bound unit module still owes, or `never` when none is bound.
|
|
862
|
+
* `Scope` is excluded, since nothing can ever provide it — the same exemption
|
|
863
|
+
* `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
|
|
864
|
+
* fork seed discharges.
|
|
865
|
+
*/
|
|
866
|
+
type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
|
|
867
|
+
/** What a bound unit module exports — the port instances a piece of that kind may read. */
|
|
868
|
+
type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
|
|
869
|
+
/** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
|
|
870
|
+
type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
|
|
871
|
+
/**
|
|
872
|
+
* A piece injecting a port the bound unit module does not export. `unknown`
|
|
873
|
+
* when every declared port is covered — including the empty case, so a worker
|
|
874
|
+
* whose pieces declare no `unit:` is gated on nothing.
|
|
875
|
+
*/
|
|
876
|
+
type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
|
|
877
|
+
readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
|
|
878
|
+
};
|
|
879
|
+
/**
|
|
880
|
+
* One `(helpers, input)` implementation with its piece's declared record put on
|
|
881
|
+
* `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
|
|
882
|
+
* Applied once per piece as di constructs it, so a unit costs one record and
|
|
883
|
+
* one context object. Each worker maps it over its own entry shape.
|
|
884
|
+
*/
|
|
885
|
+
export declare const withUnitRecord: (record: Readonly<Record<string, AnyPort>>, implementation: (helpers: never, input: never) => unknown) => (helpers: {
|
|
886
|
+
readonly context?: Readonly<Record<string | symbol, unknown>>;
|
|
887
|
+
}, input: never) => unknown;
|
|
888
|
+
/**
|
|
889
|
+
* A worker middleware's body: observe `operation`, open one kernel unit, fork
|
|
890
|
+
* `unit` — when one is bound — seeded with `seed`, and call `next` with the
|
|
891
|
+
* forked context where {@link withUnitRecord} finds it. With no `unit` bound,
|
|
892
|
+
* `next()` runs unchanged.
|
|
893
|
+
*
|
|
894
|
+
* Observed from whichever channel the unit settles on, a defect included: a
|
|
895
|
+
* defect is a failed unit too, and an errors count that omitted it would be the
|
|
896
|
+
* reassuring half.
|
|
897
|
+
*/
|
|
898
|
+
export declare const dispatchUnit: <T, E>(dispatch: {
|
|
899
|
+
readonly host: RuntimeHost<never>;
|
|
900
|
+
readonly observers: readonly ((operation: Operation) => Settle)[];
|
|
901
|
+
readonly operation: Operation;
|
|
902
|
+
readonly meta: UnitMeta;
|
|
903
|
+
readonly unit: AnyUnitModule | undefined;
|
|
904
|
+
readonly seed: readonly [AnyPort, unknown];
|
|
905
|
+
readonly next: (overrides?: never) => AsyncResult<T, E>;
|
|
906
|
+
}) => AsyncResult<T, E>;
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region src/pieces.d.ts
|
|
909
|
+
/**
|
|
910
|
+
* A refused array: as long as the array the caller wrote, its head the caller's
|
|
911
|
+
* own elements — which match — and its LAST element the marker paired with what
|
|
912
|
+
* is wrong.
|
|
913
|
+
*
|
|
914
|
+
* TypeScript compares two equal-length tuples element by element, so the extra
|
|
915
|
+
* diagnostic it reports lands on the trailing element and carries both the
|
|
916
|
+
* sentence and the missing key. A fixed two-element tuple named the key only
|
|
917
|
+
* when the array happened to be two elements long; every other arity was a
|
|
918
|
+
* length mismatch, and the developer diffed the contract against the array by
|
|
919
|
+
* hand.
|
|
920
|
+
*/
|
|
921
|
+
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]];
|
|
922
|
+
/** How a starter puts a piece's declared `unit:` record on one of its entries. */
|
|
923
|
+
type EntryWrapper = (record: Readonly<Record<string, AnyPort>>, entry: unknown) => unknown;
|
|
924
|
+
/** A piece's `{ inject, unit?, sync }` options, with the types each starter states erased. */
|
|
925
|
+
type PieceOptions = {
|
|
926
|
+
readonly inject: Readonly<Record<string, AnyPort>>;
|
|
927
|
+
readonly unit?: Readonly<Record<string, AnyPort>>;
|
|
928
|
+
readonly sync: (services: never) => unknown;
|
|
929
|
+
};
|
|
930
|
+
/**
|
|
931
|
+
* The runtime half of a starter's piece factory — `AmqpHandler(contract, key)`,
|
|
932
|
+
* `TemporalWorkflowActivities(contract, key)`: a provider on `port` whose
|
|
933
|
+
* service is what `sync` answers, wrapped once by `withUnit` so it reads the
|
|
934
|
+
* record the piece declared. The starter keeps the types; this keeps the body.
|
|
935
|
+
*/
|
|
936
|
+
export declare const mintPiece: (port: AnyPort, withUnit: EntryWrapper) => (options: PieceOptions) => unknown;
|
|
937
|
+
/**
|
|
938
|
+
* The runtime half of a starter's composing provider — `AmqpHandlers(contract)`,
|
|
939
|
+
* `TemporalActivities(contract)` — and its two arms. An array of pieces is
|
|
940
|
+
* composed into one record keyed by each piece's port id less `prefix`, so the
|
|
941
|
+
* services record IS the composed record; a `{ inject, unit?, sync }` record
|
|
942
|
+
* has `withUnit` applied to every entry `sync` answers.
|
|
943
|
+
*/
|
|
944
|
+
export declare const composeByPrefix: (port: AnyPort, prefix: string, withUnit: EntryWrapper) => ((first: unknown) => unknown);
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/starter.d.ts
|
|
947
|
+
/**
|
|
948
|
+
* The no-op member every module reading {@link Observers} provides, so the set
|
|
949
|
+
* it reads is never the empty dependency di refuses: a graph composing no
|
|
950
|
+
* observability still starts. One provider, shared — di de-duplicates a graph's
|
|
951
|
+
* providers by reference, so however many modules contribute it, an operation
|
|
952
|
+
* costs one inert call.
|
|
953
|
+
*/
|
|
954
|
+
export declare const noObserverMember: Provider<Observers, never, never> & {
|
|
955
|
+
readonly port: typeof Observers;
|
|
956
|
+
};
|
|
957
|
+
//#endregion
|
|
958
|
+
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, UnitOutcome, 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.
|
|
@@ -358,6 +352,18 @@ type UnitRecord = {
|
|
|
358
352
|
readonly signal: AbortSignal;
|
|
359
353
|
};
|
|
360
354
|
export declare const currentUnit: () => UnitRecord | undefined;
|
|
355
|
+
/** How a unit's work settled: `ok`, or `error` for an `Err`, a `Defect` and a throw alike. */
|
|
356
|
+
type UnitOutcome = "ok" | "error";
|
|
357
|
+
/**
|
|
358
|
+
* How the current unit's work settled, for the code that runs AFTER it: a
|
|
359
|
+
* unit module's `onStop`, which tears down inside the record but is handed
|
|
360
|
+
* only its own service. `undefined` outside a unit and while the work is still
|
|
361
|
+
* running. Whether the unit was aborted is the record's own `signal`.
|
|
362
|
+
*
|
|
363
|
+
* It is the kernel's outcome, not the transport's: an HTTP unit that answered
|
|
364
|
+
* a `500` still settled `ok`, since the kernel never maps a status.
|
|
365
|
+
*/
|
|
366
|
+
export declare const unitOutcome: () => UnitOutcome | undefined;
|
|
361
367
|
/**
|
|
362
368
|
* What a runtime says about one piece of work as it submits it. `kind` is the
|
|
363
369
|
* category (`"http"`, `"tick"`, `"job"`); `id` identifies **this** unit.
|
|
@@ -369,7 +375,8 @@ export declare const currentUnit: () => UnitRecord | undefined;
|
|
|
369
375
|
* silently defeats the ambient record. A route template is a `kind`.
|
|
370
376
|
*
|
|
371
377
|
* The kernel cannot check this, so uniqueness is the runtime's to guarantee.
|
|
372
|
-
* What it does guarantee is {@link UnitRecord}'s `unitId`, minted per
|
|
378
|
+
* What it does guarantee is {@link UnitRecord}'s `unitId`, a UUID minted per
|
|
379
|
+
* unit, so no two units share one across replicas either;
|
|
373
380
|
* `traceId` is the CORRELATION id, which is why it is the one a runtime may
|
|
374
381
|
* supply — it carries an id from outside the process.
|
|
375
382
|
*/
|
|
@@ -840,4 +847,112 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
|
|
|
840
847
|
*/
|
|
841
848
|
export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
|
|
842
849
|
//#endregion
|
|
843
|
-
|
|
850
|
+
//#region src/unit-record.d.ts
|
|
851
|
+
/**
|
|
852
|
+
* A module a runtime's `unit` option may bind, as the upper bound a starter
|
|
853
|
+
* constrains its own `Unit` type parameter to. `Module`'s `_exports` channel is
|
|
854
|
+
* contravariant, so `Exports = never` — never `unknown` — is what makes a REAL
|
|
855
|
+
* module's own (necessarily narrower) export type assignable to this bound:
|
|
856
|
+
* `(x: Concrete) => void` is assignable to `(x: never) => void`, not to
|
|
857
|
+
* `(x: unknown) => void`.
|
|
858
|
+
*/
|
|
859
|
+
type AnyUnitModule = Module<never, never, unknown>;
|
|
860
|
+
/**
|
|
861
|
+
* The needs a bound unit module still owes, or `never` when none is bound.
|
|
862
|
+
* `Scope` is excluded, since nothing can ever provide it — the same exemption
|
|
863
|
+
* `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
|
|
864
|
+
* fork seed discharges.
|
|
865
|
+
*/
|
|
866
|
+
type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
|
|
867
|
+
/** What a bound unit module exports — the port instances a piece of that kind may read. */
|
|
868
|
+
type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
|
|
869
|
+
/** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
|
|
870
|
+
type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
|
|
871
|
+
/**
|
|
872
|
+
* A piece injecting a port the bound unit module does not export. `unknown`
|
|
873
|
+
* when every declared port is covered — including the empty case, so a worker
|
|
874
|
+
* whose pieces declare no `unit:` is gated on nothing.
|
|
875
|
+
*/
|
|
876
|
+
type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
|
|
877
|
+
readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
|
|
878
|
+
};
|
|
879
|
+
/**
|
|
880
|
+
* One `(helpers, input)` implementation with its piece's declared record put on
|
|
881
|
+
* `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
|
|
882
|
+
* Applied once per piece as di constructs it, so a unit costs one record and
|
|
883
|
+
* one context object. Each worker maps it over its own entry shape.
|
|
884
|
+
*/
|
|
885
|
+
export declare const withUnitRecord: (record: Readonly<Record<string, AnyPort>>, implementation: (helpers: never, input: never) => unknown) => (helpers: {
|
|
886
|
+
readonly context?: Readonly<Record<string | symbol, unknown>>;
|
|
887
|
+
}, input: never) => unknown;
|
|
888
|
+
/**
|
|
889
|
+
* A worker middleware's body: observe `operation`, open one kernel unit, fork
|
|
890
|
+
* `unit` — when one is bound — seeded with `seed`, and call `next` with the
|
|
891
|
+
* forked context where {@link withUnitRecord} finds it. With no `unit` bound,
|
|
892
|
+
* `next()` runs unchanged.
|
|
893
|
+
*
|
|
894
|
+
* Observed from whichever channel the unit settles on, a defect included: a
|
|
895
|
+
* defect is a failed unit too, and an errors count that omitted it would be the
|
|
896
|
+
* reassuring half.
|
|
897
|
+
*/
|
|
898
|
+
export declare const dispatchUnit: <T, E>(dispatch: {
|
|
899
|
+
readonly host: RuntimeHost<never>;
|
|
900
|
+
readonly observers: readonly ((operation: Operation) => Settle)[];
|
|
901
|
+
readonly operation: Operation;
|
|
902
|
+
readonly meta: UnitMeta;
|
|
903
|
+
readonly unit: AnyUnitModule | undefined;
|
|
904
|
+
readonly seed: readonly [AnyPort, unknown];
|
|
905
|
+
readonly next: (overrides?: never) => AsyncResult<T, E>;
|
|
906
|
+
}) => AsyncResult<T, E>;
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region src/pieces.d.ts
|
|
909
|
+
/**
|
|
910
|
+
* A refused array: as long as the array the caller wrote, its head the caller's
|
|
911
|
+
* own elements — which match — and its LAST element the marker paired with what
|
|
912
|
+
* is wrong.
|
|
913
|
+
*
|
|
914
|
+
* TypeScript compares two equal-length tuples element by element, so the extra
|
|
915
|
+
* diagnostic it reports lands on the trailing element and carries both the
|
|
916
|
+
* sentence and the missing key. A fixed two-element tuple named the key only
|
|
917
|
+
* when the array happened to be two elements long; every other arity was a
|
|
918
|
+
* length mismatch, and the developer diffed the contract against the array by
|
|
919
|
+
* hand.
|
|
920
|
+
*/
|
|
921
|
+
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]];
|
|
922
|
+
/** How a starter puts a piece's declared `unit:` record on one of its entries. */
|
|
923
|
+
type EntryWrapper = (record: Readonly<Record<string, AnyPort>>, entry: unknown) => unknown;
|
|
924
|
+
/** A piece's `{ inject, unit?, sync }` options, with the types each starter states erased. */
|
|
925
|
+
type PieceOptions = {
|
|
926
|
+
readonly inject: Readonly<Record<string, AnyPort>>;
|
|
927
|
+
readonly unit?: Readonly<Record<string, AnyPort>>;
|
|
928
|
+
readonly sync: (services: never) => unknown;
|
|
929
|
+
};
|
|
930
|
+
/**
|
|
931
|
+
* The runtime half of a starter's piece factory — `AmqpHandler(contract, key)`,
|
|
932
|
+
* `TemporalWorkflowActivities(contract, key)`: a provider on `port` whose
|
|
933
|
+
* service is what `sync` answers, wrapped once by `withUnit` so it reads the
|
|
934
|
+
* record the piece declared. The starter keeps the types; this keeps the body.
|
|
935
|
+
*/
|
|
936
|
+
export declare const mintPiece: (port: AnyPort, withUnit: EntryWrapper) => (options: PieceOptions) => unknown;
|
|
937
|
+
/**
|
|
938
|
+
* The runtime half of a starter's composing provider — `AmqpHandlers(contract)`,
|
|
939
|
+
* `TemporalActivities(contract)` — and its two arms. An array of pieces is
|
|
940
|
+
* composed into one record keyed by each piece's port id less `prefix`, so the
|
|
941
|
+
* services record IS the composed record; a `{ inject, unit?, sync }` record
|
|
942
|
+
* has `withUnit` applied to every entry `sync` answers.
|
|
943
|
+
*/
|
|
944
|
+
export declare const composeByPrefix: (port: AnyPort, prefix: string, withUnit: EntryWrapper) => ((first: unknown) => unknown);
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/starter.d.ts
|
|
947
|
+
/**
|
|
948
|
+
* The no-op member every module reading {@link Observers} provides, so the set
|
|
949
|
+
* it reads is never the empty dependency di refuses: a graph composing no
|
|
950
|
+
* observability still starts. One provider, shared — di de-duplicates a graph's
|
|
951
|
+
* providers by reference, so however many modules contribute it, an operation
|
|
952
|
+
* costs one inert call.
|
|
953
|
+
*/
|
|
954
|
+
export declare const noObserverMember: Provider<Observers, never, never> & {
|
|
955
|
+
readonly port: typeof Observers;
|
|
956
|
+
};
|
|
957
|
+
//#endregion
|
|
958
|
+
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, UnitOutcome, UnitRecord, UnitRecordOf, UnitRegistry, UnitWork };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
6
8
|
//#region src/instrumentation.ts
|
|
7
9
|
/**
|
|
8
10
|
* The set port a package contributes its instrumentation to.
|
|
@@ -68,10 +70,11 @@ const answeringWithin = (health, ms) => {
|
|
|
68
70
|
* rather than a recovery.** Neither recovery above can see one: there is no
|
|
69
71
|
* error and no defect, only silence, so `/healthz` held a socket open per hit
|
|
70
72
|
* while kubelet timed out against a report that named nothing. Each check now
|
|
71
|
-
* races {@link
|
|
73
|
+
* races its own `timeoutMs` (default {@link DEFAULT_HEALTH_TIMEOUT_MS}), and
|
|
74
|
+
* the component that hung is named
|
|
72
75
|
* in the report like any other unhealthy one.
|
|
73
76
|
*/
|
|
74
|
-
const runHealthChecks = (checks
|
|
77
|
+
const runHealthChecks = (checks) => allAsync(checks.map((health) => answeringWithin(health, health.timeoutMs ?? 800).map(() => ({
|
|
75
78
|
name: health.name,
|
|
76
79
|
status: "healthy"
|
|
77
80
|
})).recoverErrCases((matcher) => matcher.with(P.tag("HealthCheckFailed"), (error) => ({
|
|
@@ -152,22 +155,10 @@ const observed = (observers, operation, call, settled = {}) => {
|
|
|
152
155
|
//#region src/clock.ts
|
|
153
156
|
const systemClock = {
|
|
154
157
|
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
|
-
}))
|
|
158
|
+
sleep: (ms, signal) => fromSafePromise(setTimeout$1(ms, void 0, {
|
|
159
|
+
signal,
|
|
160
|
+
ref: false
|
|
161
|
+
}).catch(() => {}))
|
|
171
162
|
};
|
|
172
163
|
//#endregion
|
|
173
164
|
//#region src/events.ts
|
|
@@ -441,10 +432,22 @@ const installUncaughtHandlers = (onUncaught) => {
|
|
|
441
432
|
const storage = new AsyncLocalStorage();
|
|
442
433
|
const runWithUnit = (record, fn) => storage.run(record, fn);
|
|
443
434
|
const currentUnit = () => storage.getStore();
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
435
|
+
const outcomes = /* @__PURE__ */ new WeakMap();
|
|
436
|
+
const settleUnit = (record, outcome) => {
|
|
437
|
+
outcomes.set(record, outcome);
|
|
438
|
+
};
|
|
439
|
+
/**
|
|
440
|
+
* How the current unit's work settled, for the code that runs AFTER it: a
|
|
441
|
+
* unit module's `onStop`, which tears down inside the record but is handed
|
|
442
|
+
* only its own service. `undefined` outside a unit and while the work is still
|
|
443
|
+
* running. Whether the unit was aborted is the record's own `signal`.
|
|
444
|
+
*
|
|
445
|
+
* It is the kernel's outcome, not the transport's: an HTTP unit that answered
|
|
446
|
+
* a `500` still settled `ok`, since the kernel never maps a status.
|
|
447
|
+
*/
|
|
448
|
+
const unitOutcome = () => {
|
|
449
|
+
const unit = currentUnit();
|
|
450
|
+
return unit === void 0 ? void 0 : outcomes.get(unit);
|
|
448
451
|
};
|
|
449
452
|
const createUnitRegistry = () => {
|
|
450
453
|
const open = /* @__PURE__ */ new Set();
|
|
@@ -460,7 +463,7 @@ const createUnitRegistry = () => {
|
|
|
460
463
|
const controller = new AbortController();
|
|
461
464
|
open.add(controller);
|
|
462
465
|
const record = {
|
|
463
|
-
unitId:
|
|
466
|
+
unitId: randomUUID(),
|
|
464
467
|
traceId: meta.traceId ?? meta.id,
|
|
465
468
|
tenantId: meta.tenantId,
|
|
466
469
|
signal: controller.signal
|
|
@@ -620,6 +623,11 @@ const start = (module, options = {}) => {
|
|
|
620
623
|
const health = () => runHealthChecks(healthChecks);
|
|
621
624
|
const probeBound = Promise.withResolvers();
|
|
622
625
|
const runtimePublished = Promise.withResolvers();
|
|
626
|
+
const leaveStartup = () => {
|
|
627
|
+
runtimePublished.resolve(void 0);
|
|
628
|
+
tracker.advanceTo("stopping");
|
|
629
|
+
disposeAll();
|
|
630
|
+
};
|
|
623
631
|
const stopping = Promise.withResolvers();
|
|
624
632
|
const stopAbandoned = () => fromSafePromise(stopping.promise).flatMap(({ reason, drain }) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : OkAsync(abandonStop(reason, drain))));
|
|
625
633
|
const abandonStop = (reason, drain) => {
|
|
@@ -639,9 +647,7 @@ const start = (module, options = {}) => {
|
|
|
639
647
|
afterMs: void 0
|
|
640
648
|
});
|
|
641
649
|
registry.abortAll();
|
|
642
|
-
|
|
643
|
-
tracker.advanceTo("stopping");
|
|
644
|
-
disposeAll();
|
|
650
|
+
leaveStartup();
|
|
645
651
|
return reportOf(reason, void 0, "build");
|
|
646
652
|
});
|
|
647
653
|
const abandonBuild = (reason) => {
|
|
@@ -672,11 +678,7 @@ const start = (module, options = {}) => {
|
|
|
672
678
|
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
673
679
|
});
|
|
674
680
|
probeBound.resolve(void 0);
|
|
675
|
-
|
|
676
|
-
tracker.advanceTo("stopping");
|
|
677
|
-
disposeSignals();
|
|
678
|
-
disposeUncaught();
|
|
679
|
-
tracker.advanceTo("exited");
|
|
681
|
+
leaveStartup();
|
|
680
682
|
});
|
|
681
683
|
const root = Module("Kernel")({
|
|
682
684
|
imports: providesEnv(module) ? [module] : [module, Module("Environment")({
|
|
@@ -751,13 +753,18 @@ const start = (module, options = {}) => {
|
|
|
751
753
|
}).get();
|
|
752
754
|
return fromSafePromise(ready.promise);
|
|
753
755
|
};
|
|
756
|
+
const record = currentUnit();
|
|
754
757
|
const outcome = (async () => {
|
|
758
|
+
let settledAs = "error";
|
|
755
759
|
try {
|
|
756
|
-
|
|
760
|
+
const result = await work({
|
|
757
761
|
ctx: runtimeCtx,
|
|
758
762
|
fork
|
|
759
763
|
}, signal);
|
|
764
|
+
if (result.isOk()) settledAs = "ok";
|
|
765
|
+
return result;
|
|
760
766
|
} finally {
|
|
767
|
+
if (record !== void 0) settleUnit(record, settledAs);
|
|
761
768
|
hasSettled = true;
|
|
762
769
|
settled.resolve();
|
|
763
770
|
if (closing !== void 0) await closing;
|
|
@@ -793,9 +800,7 @@ const start = (module, options = {}) => {
|
|
|
793
800
|
type: "startFailed",
|
|
794
801
|
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
795
802
|
});
|
|
796
|
-
|
|
797
|
-
tracker.advanceTo("stopping");
|
|
798
|
-
disposeAll();
|
|
803
|
+
leaveStartup();
|
|
799
804
|
}).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
|
|
800
805
|
return {
|
|
801
806
|
exited: probesStarted.flatMap(() => fromSafePromise(Promise.race([
|
|
@@ -871,4 +876,95 @@ const runMain = async (module, options = {}, exit = (code) => {
|
|
|
871
876
|
await awaitExit(start(module, options), exit);
|
|
872
877
|
};
|
|
873
878
|
//#endregion
|
|
874
|
-
|
|
879
|
+
//#region src/unit-record.ts
|
|
880
|
+
/**
|
|
881
|
+
* Where {@link dispatchUnit} leaves the forked context for {@link withUnitRecord}
|
|
882
|
+
* to read. A symbol rather than a name: the library merges it into the context
|
|
883
|
+
* every piece sees, and a string key would appear on the record a piece written
|
|
884
|
+
* against the `{ inject, sync }` arm destructures.
|
|
885
|
+
*/
|
|
886
|
+
const UNIT_SCOPE = Symbol("@btravstack/core/unit-scope");
|
|
887
|
+
/**
|
|
888
|
+
* The declared record, as a getter per name resolved on read out of the fork.
|
|
889
|
+
* Neither writable nor configurable — a piece reads what the fork holds, and
|
|
890
|
+
* cannot reshape the record under the next unit. With no unit module bound
|
|
891
|
+
* there is nothing to resolve from, so the record is empty whatever was
|
|
892
|
+
* declared.
|
|
893
|
+
*/
|
|
894
|
+
const unitRecordOf = (forked, record) => {
|
|
895
|
+
const unit = {};
|
|
896
|
+
if (forked === void 0) return unit;
|
|
897
|
+
for (const [name, port] of Object.entries(record)) Object.defineProperty(unit, name, {
|
|
898
|
+
enumerable: true,
|
|
899
|
+
get: () => forked.get(port)
|
|
900
|
+
});
|
|
901
|
+
return unit;
|
|
902
|
+
};
|
|
903
|
+
/**
|
|
904
|
+
* One `(helpers, input)` implementation with its piece's declared record put on
|
|
905
|
+
* `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
|
|
906
|
+
* Applied once per piece as di constructs it, so a unit costs one record and
|
|
907
|
+
* one context object. Each worker maps it over its own entry shape.
|
|
908
|
+
*/
|
|
909
|
+
const withUnitRecord = (record, implementation) => (helpers, input) => {
|
|
910
|
+
const { [UNIT_SCOPE]: forked, ...rest } = helpers.context ?? {};
|
|
911
|
+
return implementation({
|
|
912
|
+
...helpers,
|
|
913
|
+
context: {
|
|
914
|
+
...rest,
|
|
915
|
+
unit: unitRecordOf(forked, record)
|
|
916
|
+
}
|
|
917
|
+
}, input);
|
|
918
|
+
};
|
|
919
|
+
/**
|
|
920
|
+
* A worker middleware's body: observe `operation`, open one kernel unit, fork
|
|
921
|
+
* `unit` — when one is bound — seeded with `seed`, and call `next` with the
|
|
922
|
+
* forked context where {@link withUnitRecord} finds it. With no `unit` bound,
|
|
923
|
+
* `next()` runs unchanged.
|
|
924
|
+
*
|
|
925
|
+
* Observed from whichever channel the unit settles on, a defect included: a
|
|
926
|
+
* defect is a failed unit too, and an errors count that omitted it would be the
|
|
927
|
+
* reassuring half.
|
|
928
|
+
*/
|
|
929
|
+
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 } }))));
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region src/pieces.ts
|
|
932
|
+
/**
|
|
933
|
+
* The runtime half of a starter's piece factory — `AmqpHandler(contract, key)`,
|
|
934
|
+
* `TemporalWorkflowActivities(contract, key)`: a provider on `port` whose
|
|
935
|
+
* service is what `sync` answers, wrapped once by `withUnit` so it reads the
|
|
936
|
+
* record the piece declared. The starter keeps the types; this keeps the body.
|
|
937
|
+
*/
|
|
938
|
+
const mintPiece = (port, withUnit) => (options) => Provider(port)({
|
|
939
|
+
inject: options.inject,
|
|
940
|
+
sync: (services) => withUnit(options.unit ?? {}, options.sync(services))
|
|
941
|
+
});
|
|
942
|
+
/**
|
|
943
|
+
* The runtime half of a starter's composing provider — `AmqpHandlers(contract)`,
|
|
944
|
+
* `TemporalActivities(contract)` — and its two arms. An array of pieces is
|
|
945
|
+
* composed into one record keyed by each piece's port id less `prefix`, so the
|
|
946
|
+
* services record IS the composed record; a `{ inject, unit?, sync }` record
|
|
947
|
+
* has `withUnit` applied to every entry `sync` answers.
|
|
948
|
+
*/
|
|
949
|
+
const composeByPrefix = (port, prefix, withUnit) => {
|
|
950
|
+
const whole = mintPiece(port, (record, entries) => Object.fromEntries(Object.entries(entries).map(([key, entry]) => [key, withUnit(record, entry)])));
|
|
951
|
+
return (first) => Array.isArray(first) ? Provider(port)({
|
|
952
|
+
inject: Object.fromEntries(first.map((piece) => [piece.port.portId.slice(prefix.length), piece.port])),
|
|
953
|
+
sync: (services) => services
|
|
954
|
+
}) : whole(first);
|
|
955
|
+
};
|
|
956
|
+
//#endregion
|
|
957
|
+
//#region src/starter.ts
|
|
958
|
+
/**
|
|
959
|
+
* The no-op member every module reading {@link Observers} provides, so the set
|
|
960
|
+
* it reads is never the empty dependency di refuses: a graph composing no
|
|
961
|
+
* observability still starts. One provider, shared — di de-duplicates a graph's
|
|
962
|
+
* providers by reference, so however many modules contribute it, an operation
|
|
963
|
+
* costs one inert call.
|
|
964
|
+
*/
|
|
965
|
+
const noObserverMember = Provider.member(Observers)({
|
|
966
|
+
inject: {},
|
|
967
|
+
value: noObserver
|
|
968
|
+
});
|
|
969
|
+
//#endregion
|
|
970
|
+
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, unitOutcome, withUnitRecord };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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,21 @@
|
|
|
45
45
|
"./package.json": "./package.json"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@btravstack/config": "0.
|
|
49
|
-
"@btravstack/di": "0.
|
|
48
|
+
"@btravstack/config": "0.18.0",
|
|
49
|
+
"@btravstack/di": "0.18.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
|
-
"
|
|
57
|
-
"
|
|
56
|
+
"typescript-consumer": "npm:typescript@5.9.3",
|
|
57
|
+
"unthrown": "5.12.0",
|
|
58
|
+
"vitest": "5.0.3"
|
|
58
59
|
},
|
|
59
60
|
"peerDependencies": {
|
|
60
|
-
"@btravstack/config": "^0.
|
|
61
|
-
"@btravstack/di": "^0.
|
|
61
|
+
"@btravstack/config": "^0.18.0",
|
|
62
|
+
"@btravstack/di": "^0.18.0",
|
|
62
63
|
"unthrown": "^5.0.0"
|
|
63
64
|
},
|
|
64
65
|
"engines": {
|
|
@@ -69,6 +70,6 @@
|
|
|
69
70
|
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch --tsconfig tsconfig.build.json",
|
|
70
71
|
"generate": "node ../../docs/scripts/extract-doc-samples.ts --group core --out src/generated/doc-samples",
|
|
71
72
|
"test": "vitest run --coverage",
|
|
72
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json"
|
|
73
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json && node ./node_modules/typescript-consumer/bin/tsc --noEmit -p tsconfig.test-d.json"
|
|
73
74
|
}
|
|
74
75
|
}
|