@btravstack/core 0.17.0 → 0.19.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 +131 -39
- package/dist/index.d.cts +93 -57
- package/dist/index.d.mts +93 -57
- package/dist/index.mjs +130 -40
- package/package.json +10 -6
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,7 @@ let node_timers_promises = require("node:timers/promises");
|
|
|
5
5
|
let _btravstack_config = require("@btravstack/config");
|
|
6
6
|
let node_http = require("node:http");
|
|
7
7
|
let node_async_hooks = require("node:async_hooks");
|
|
8
|
+
let node_crypto = require("node:crypto");
|
|
8
9
|
//#region src/instrumentation.ts
|
|
9
10
|
/**
|
|
10
11
|
* The set port a package contributes its instrumentation to.
|
|
@@ -428,14 +429,71 @@ const installUncaughtHandlers = (onUncaught) => {
|
|
|
428
429
|
};
|
|
429
430
|
};
|
|
430
431
|
//#endregion
|
|
432
|
+
//#region src/unit-overrides.ts
|
|
433
|
+
/**
|
|
434
|
+
* Providers substituted inside a runtime's unit modules, keyed by the kind
|
|
435
|
+
* the runtime declares on `Runtime.units` — test-facing: `@btravstack/testing`'s
|
|
436
|
+
* `overridden(root, providers, { unit })` is the one contributor, and a
|
|
437
|
+
* production root never names it.
|
|
438
|
+
*
|
|
439
|
+
* Each provider must already carry di's override brand. The kernel applies
|
|
440
|
+
* them at boot, before the runtime starts: every kind named must be one the
|
|
441
|
+
* runtime binds, and every port must be one that kind's module provides, or
|
|
442
|
+
* the boot is a defect — the same drift gate a root-level override has.
|
|
443
|
+
*/
|
|
444
|
+
var UnitOverrides = class extends _btravstack_di.Port.many("UnitOverrides") {};
|
|
445
|
+
/** Whether `module`'s tree carries a provider for `portId`. */
|
|
446
|
+
const provides = (module, portId) => module.provides.some((provider) => provider.port.portId === portId) || module.imports.some((imported) => provides(imported, portId));
|
|
447
|
+
/**
|
|
448
|
+
* Each overridden kind's module, mapped to the module the kernel forks in its
|
|
449
|
+
* place. Keyed by module IDENTITY, because `UnitHost.fork` is handed the
|
|
450
|
+
* module and never the kind — which is why a module bound under two kinds is
|
|
451
|
+
* refused rather than overridden under both.
|
|
452
|
+
*/
|
|
453
|
+
const unitSubstitutes = (runtime, units, overrides) => {
|
|
454
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const member of overrides) for (const [kind, providers] of Object.entries(member)) byKind.set(kind, [...byKind.get(kind) ?? [], ...providers]);
|
|
456
|
+
const substitutes = /* @__PURE__ */ new Map();
|
|
457
|
+
for (const [kind, providers] of byKind) {
|
|
458
|
+
const module = units[kind];
|
|
459
|
+
if (module === void 0) throw new Error(`[core] unit override for kind "${kind}", which runtime "${runtime}" binds no module for`);
|
|
460
|
+
const shared = Object.keys(units).find((other) => other !== kind && units[other] === module);
|
|
461
|
+
if (shared !== void 0) throw new Error(`[core] unit override for kind "${kind}", whose module kind "${shared}" binds too — an override cannot reach one without the other`);
|
|
462
|
+
const seen = /* @__PURE__ */ new Set();
|
|
463
|
+
for (const provider of providers) {
|
|
464
|
+
if (seen.has(provider.port.portId)) throw new Error(`[core] two unit overrides registered for port ${JSON.stringify(provider.port.portId)} in kind "${kind}"`);
|
|
465
|
+
seen.add(provider.port.portId);
|
|
466
|
+
if (!provides(module, provider.port.portId)) throw new Error(`[core] unit override for port ${JSON.stringify(provider.port.portId)} in kind "${kind}" with nothing to override — its module no longer provides it`);
|
|
467
|
+
}
|
|
468
|
+
substitutes.set(module, (0, _btravstack_di.Module)("UnitOverridden")({
|
|
469
|
+
imports: [module],
|
|
470
|
+
provides: providers,
|
|
471
|
+
exports: [module]
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
return substitutes;
|
|
475
|
+
};
|
|
476
|
+
//#endregion
|
|
431
477
|
//#region src/units.ts
|
|
432
478
|
const storage = new node_async_hooks.AsyncLocalStorage();
|
|
433
479
|
const runWithUnit = (record, fn) => storage.run(record, fn);
|
|
434
480
|
const currentUnit = () => storage.getStore();
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
481
|
+
const outcomes = /* @__PURE__ */ new WeakMap();
|
|
482
|
+
const settleUnit = (record, outcome) => {
|
|
483
|
+
outcomes.set(record, outcome);
|
|
484
|
+
};
|
|
485
|
+
/**
|
|
486
|
+
* How the current unit's work settled, for the code that runs AFTER it: a
|
|
487
|
+
* unit module's `onStop`, which tears down inside the record but is handed
|
|
488
|
+
* only its own service. `undefined` outside a unit and while the work is still
|
|
489
|
+
* running. Whether the unit was aborted is the record's own `signal`.
|
|
490
|
+
*
|
|
491
|
+
* It is the kernel's outcome, not the transport's: an HTTP unit that answered
|
|
492
|
+
* a `500` still settled `ok`, since the kernel never maps a status.
|
|
493
|
+
*/
|
|
494
|
+
const unitOutcome = () => {
|
|
495
|
+
const unit = currentUnit();
|
|
496
|
+
return unit === void 0 ? void 0 : outcomes.get(unit);
|
|
439
497
|
};
|
|
440
498
|
const createUnitRegistry = () => {
|
|
441
499
|
const open = /* @__PURE__ */ new Set();
|
|
@@ -451,7 +509,7 @@ const createUnitRegistry = () => {
|
|
|
451
509
|
const controller = new AbortController();
|
|
452
510
|
open.add(controller);
|
|
453
511
|
const record = {
|
|
454
|
-
unitId:
|
|
512
|
+
unitId: (0, node_crypto.randomUUID)(),
|
|
455
513
|
traceId: meta.traceId ?? meta.id,
|
|
456
514
|
tenantId: meta.tenantId,
|
|
457
515
|
signal: controller.signal
|
|
@@ -485,7 +543,7 @@ const createUnitRegistry = () => {
|
|
|
485
543
|
* and whose side effects then have to be guarded one by one.
|
|
486
544
|
*/
|
|
487
545
|
const withdrawn = () => (0, unthrown.fromSafePromise)(new Promise(() => {}));
|
|
488
|
-
const providesEnv = (module) =>
|
|
546
|
+
const providesEnv = (module) => provides(module, _btravstack_config.Env.portId);
|
|
489
547
|
/**
|
|
490
548
|
* What the kernel reads from the environment for itself. Each field is pinned
|
|
491
549
|
* by the matching `StartOptions` field — explicit beats environment beats
|
|
@@ -532,7 +590,13 @@ const readKernelConfig = (options, env) => {
|
|
|
532
590
|
const start = (module, options = {}) => {
|
|
533
591
|
const clock = options.clock ?? systemClock;
|
|
534
592
|
const env = options.env ?? process.env;
|
|
535
|
-
const
|
|
593
|
+
const sink = safeSink(options.onEvent ?? stderrSink);
|
|
594
|
+
let exitedEmitted = false;
|
|
595
|
+
const emit = (event) => {
|
|
596
|
+
if (exitedEmitted) return;
|
|
597
|
+
sink(event);
|
|
598
|
+
if (event.type === "exited") exitedEmitted = true;
|
|
599
|
+
};
|
|
536
600
|
let runtimeName = "";
|
|
537
601
|
let servingInfo = void 0;
|
|
538
602
|
let probeBoundPort = void 0;
|
|
@@ -584,6 +648,7 @@ const start = (module, options = {}) => {
|
|
|
584
648
|
}
|
|
585
649
|
});
|
|
586
650
|
const disposeUncaught = options.signals === false ? () => {} : installUncaughtHandlers((cause) => {
|
|
651
|
+
crashed = true;
|
|
587
652
|
emit({
|
|
588
653
|
type: "uncaught",
|
|
589
654
|
cause
|
|
@@ -594,6 +659,8 @@ const start = (module, options = {}) => {
|
|
|
594
659
|
abandonBuild("uncaught");
|
|
595
660
|
});
|
|
596
661
|
let disposeProbes = () => {};
|
|
662
|
+
let crashed = false;
|
|
663
|
+
let startupFailing = false;
|
|
597
664
|
const disposeAll = () => {
|
|
598
665
|
disposeSignals();
|
|
599
666
|
disposeUncaught();
|
|
@@ -601,7 +668,7 @@ const start = (module, options = {}) => {
|
|
|
601
668
|
disposeProbes();
|
|
602
669
|
};
|
|
603
670
|
const reportOf = (reason, drain, abandonedAt) => ({
|
|
604
|
-
reason,
|
|
671
|
+
reason: crashed ? "uncaught" : reason,
|
|
605
672
|
drain,
|
|
606
673
|
teardownErrors,
|
|
607
674
|
uptimeMs: clock.now() - startedAt,
|
|
@@ -616,16 +683,24 @@ const start = (module, options = {}) => {
|
|
|
616
683
|
tracker.advanceTo("stopping");
|
|
617
684
|
disposeAll();
|
|
618
685
|
};
|
|
686
|
+
const leaveFailed = (failure) => {
|
|
687
|
+
if (tracker.current() !== "stopping") emit({
|
|
688
|
+
type: "startFailed",
|
|
689
|
+
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
690
|
+
});
|
|
691
|
+
runtimePublished.resolve(void 0);
|
|
692
|
+
tracker.advanceTo("stopping");
|
|
693
|
+
};
|
|
619
694
|
const stopping = Promise.withResolvers();
|
|
620
|
-
const stopAbandoned = () => (0, unthrown.fromSafePromise)(stopping.promise).flatMap((
|
|
621
|
-
const abandonStop = (
|
|
695
|
+
const stopAbandoned = () => (0, unthrown.fromSafePromise)(stopping.promise).flatMap((abandoned) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : abandonStop(abandoned)));
|
|
696
|
+
const abandonStop = (abandoned) => {
|
|
622
697
|
emit({
|
|
623
698
|
type: "stoppedWaiting",
|
|
624
699
|
phase: "stop",
|
|
625
700
|
afterMs: stopSettled.signal.aborted ? void 0 : stopTimeoutMs
|
|
626
701
|
});
|
|
627
702
|
disposeAll();
|
|
628
|
-
return reportOf(reason, drain, "stop");
|
|
703
|
+
return abandoned().map(({ reason, drain }) => reportOf(reason, drain, "stop"));
|
|
629
704
|
};
|
|
630
705
|
const abandoningBuild = Promise.withResolvers();
|
|
631
706
|
const buildAbandoned = () => (0, unthrown.fromSafePromise)(abandoningBuild.promise).map((reason) => {
|
|
@@ -640,7 +715,7 @@ const start = (module, options = {}) => {
|
|
|
640
715
|
});
|
|
641
716
|
const abandonBuild = (reason) => {
|
|
642
717
|
const phase = tracker.current();
|
|
643
|
-
if (phase === "building" || phase === "starting") abandoningBuild.resolve(reason);
|
|
718
|
+
if (phase === "building" || phase === "starting" || reason === "uncaught" && startupFailing) abandoningBuild.resolve(reason);
|
|
644
719
|
};
|
|
645
720
|
emit({ type: "building" });
|
|
646
721
|
const probesOptions = kernelConfig.map(({ probePort, probeHost }) => options.probes === false ? false : {
|
|
@@ -701,22 +776,23 @@ const start = (module, options = {}) => {
|
|
|
701
776
|
if (reason !== "signal") registry.abortAll();
|
|
702
777
|
return (reason === "signal" ? runDrain(serving) : (0, unthrown.OkAsync)(void 0)).flatMap((report) => {
|
|
703
778
|
tracker.advanceTo("stopping");
|
|
704
|
-
stopping.resolve({
|
|
779
|
+
stopping.resolve(() => (0, unthrown.OkAsync)({
|
|
705
780
|
reason,
|
|
706
781
|
drain: report
|
|
707
|
-
});
|
|
708
|
-
return serving.stop().map(() => {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
});
|
|
782
|
+
}));
|
|
783
|
+
return serving.stop().map(() => ({
|
|
784
|
+
reason,
|
|
785
|
+
drain: report
|
|
786
|
+
}));
|
|
712
787
|
});
|
|
713
788
|
};
|
|
714
|
-
const
|
|
789
|
+
const serve = (ctx) => {
|
|
715
790
|
tracker.advanceTo("starting");
|
|
716
791
|
const runtime = ctx.get(RuntimePort);
|
|
717
792
|
runtimeName = runtime.name;
|
|
718
793
|
healthChecks = ctx.get(HealthChecks);
|
|
719
794
|
const runtimeCtx = ctx;
|
|
795
|
+
const substitutes = unitSubstitutes(runtime.name, runtime.units ?? {}, ctx.get(UnitOverrides));
|
|
720
796
|
const run = (meta, work) => registry.run(meta, (signal) => {
|
|
721
797
|
const settled = Promise.withResolvers();
|
|
722
798
|
let closing;
|
|
@@ -725,12 +801,12 @@ const start = (module, options = {}) => {
|
|
|
725
801
|
if (hasSettled) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
|
|
726
802
|
if (closing !== void 0) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
|
|
727
803
|
const ready = Promise.withResolvers();
|
|
728
|
-
closing = _btravstack_di.Module.forkScope(ctx, module, (forked) => {
|
|
804
|
+
closing = _btravstack_di.Module.forkScope(ctx, substitutes.get(module) ?? module, (forked) => {
|
|
729
805
|
ready.resolve(forked);
|
|
730
806
|
return (0, unthrown.fromSafePromise)(settled.promise);
|
|
731
807
|
}, {
|
|
732
808
|
seed,
|
|
733
|
-
onTeardownError: (port, cause) =>
|
|
809
|
+
onTeardownError: (port, cause) => sink({
|
|
734
810
|
type: "teardownError",
|
|
735
811
|
port,
|
|
736
812
|
cause
|
|
@@ -741,13 +817,18 @@ const start = (module, options = {}) => {
|
|
|
741
817
|
}).get();
|
|
742
818
|
return (0, unthrown.fromSafePromise)(ready.promise);
|
|
743
819
|
};
|
|
820
|
+
const record = currentUnit();
|
|
744
821
|
const outcome = (async () => {
|
|
822
|
+
let settledAs = "error";
|
|
745
823
|
try {
|
|
746
|
-
|
|
824
|
+
const result = await work({
|
|
747
825
|
ctx: runtimeCtx,
|
|
748
826
|
fork
|
|
749
827
|
}, signal);
|
|
828
|
+
if (result.isOk()) settledAs = "ok";
|
|
829
|
+
return result;
|
|
750
830
|
} finally {
|
|
831
|
+
if (record !== void 0) settleUnit(record, settledAs);
|
|
751
832
|
hasSettled = true;
|
|
752
833
|
settled.resolve();
|
|
753
834
|
if (closing !== void 0) await closing;
|
|
@@ -768,23 +849,32 @@ const start = (module, options = {}) => {
|
|
|
768
849
|
});
|
|
769
850
|
return (0, unthrown.fromSafePromise)(shutdown.promise).flatMap((reason) => finish(serving, reason));
|
|
770
851
|
});
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
852
|
+
};
|
|
853
|
+
const built = () => _btravstack_di.Module.scoped(root, (ctx) => (0, unthrown.OkAsync)(ctx).flatMap(serve).tapFailure((failure) => {
|
|
854
|
+
if (tracker.current() === "starting") startupFailing = true;
|
|
855
|
+
leaveFailed(failure);
|
|
856
|
+
stopping.resolve(() => failure.toAsync());
|
|
857
|
+
}), {
|
|
858
|
+
onConstructionFailure: (failure) => {
|
|
859
|
+
startupFailing = true;
|
|
860
|
+
leaveFailed(failure);
|
|
861
|
+
stopping.resolve(() => failure.toAsync());
|
|
862
|
+
},
|
|
863
|
+
onTeardownError: (port, cause) => {
|
|
864
|
+
teardownErrors.push({
|
|
865
|
+
port,
|
|
866
|
+
cause
|
|
867
|
+
});
|
|
868
|
+
emit({
|
|
869
|
+
type: "teardownError",
|
|
870
|
+
port,
|
|
871
|
+
cause
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}).tapFailure((failure) => {
|
|
875
|
+
leaveFailed(failure);
|
|
876
|
+
disposeAll();
|
|
877
|
+
}).map(({ reason, drain }) => reportOf(reason, drain)).tap(disposeAll).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
|
|
788
878
|
return {
|
|
789
879
|
exited: probesStarted.flatMap(() => (0, unthrown.fromSafePromise)(Promise.race([
|
|
790
880
|
built(),
|
|
@@ -961,6 +1051,7 @@ exports.RuntimePort = RuntimePort;
|
|
|
961
1051
|
exports.RuntimeStartFailed = RuntimeStartFailed;
|
|
962
1052
|
exports.SPAN_STATUS = SPAN_STATUS;
|
|
963
1053
|
exports.Tracer = Tracer;
|
|
1054
|
+
exports.UnitOverrides = UnitOverrides;
|
|
964
1055
|
exports.composeByPrefix = composeByPrefix;
|
|
965
1056
|
exports.currentUnit = currentUnit;
|
|
966
1057
|
exports.dispatchUnit = dispatchUnit;
|
|
@@ -976,4 +1067,5 @@ exports.start = start;
|
|
|
976
1067
|
exports.stderrSink = stderrSink;
|
|
977
1068
|
exports.systemClock = systemClock;
|
|
978
1069
|
exports.traceIdOfTraceparent = traceIdOfTraceparent;
|
|
1070
|
+
exports.unitOutcome = unitOutcome;
|
|
979
1071
|
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, Provider, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
|
|
2
|
+
import { AnyPort, AnyProvider, 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
|
/**
|
|
@@ -352,6 +352,18 @@ type UnitRecord = {
|
|
|
352
352
|
readonly signal: AbortSignal;
|
|
353
353
|
};
|
|
354
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;
|
|
355
367
|
/**
|
|
356
368
|
* What a runtime says about one piece of work as it submits it. `kind` is the
|
|
357
369
|
* category (`"http"`, `"tick"`, `"job"`); `id` identifies **this** unit.
|
|
@@ -363,7 +375,8 @@ export declare const currentUnit: () => UnitRecord | undefined;
|
|
|
363
375
|
* silently defeats the ambient record. A route template is a `kind`.
|
|
364
376
|
*
|
|
365
377
|
* The kernel cannot check this, so uniqueness is the runtime's to guarantee.
|
|
366
|
-
* 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;
|
|
367
380
|
* `traceId` is the CORRELATION id, which is why it is the one a runtime may
|
|
368
381
|
* supply — it carries an id from outside the process.
|
|
369
382
|
*/
|
|
@@ -382,6 +395,64 @@ type UnitRegistry = {
|
|
|
382
395
|
readonly awaitIdle: () => AsyncResult<void, never>;
|
|
383
396
|
};
|
|
384
397
|
//#endregion
|
|
398
|
+
//#region src/unit-record.d.ts
|
|
399
|
+
/**
|
|
400
|
+
* A module a runtime's `unit` option may bind, as the upper bound a starter
|
|
401
|
+
* constrains its own `Unit` type parameter to. `Module`'s `_exports` channel is
|
|
402
|
+
* contravariant, so `Exports = never` — never `unknown` — is what makes a REAL
|
|
403
|
+
* module's own (necessarily narrower) export type assignable to this bound:
|
|
404
|
+
* `(x: Concrete) => void` is assignable to `(x: never) => void`, not to
|
|
405
|
+
* `(x: unknown) => void`.
|
|
406
|
+
*/
|
|
407
|
+
type AnyUnitModule = Module<never, never, unknown>;
|
|
408
|
+
/**
|
|
409
|
+
* The needs a bound unit module still owes, or `never` when none is bound.
|
|
410
|
+
* `Scope` is excluded, since nothing can ever provide it — the same exemption
|
|
411
|
+
* `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
|
|
412
|
+
* fork seed discharges.
|
|
413
|
+
*/
|
|
414
|
+
type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
|
|
415
|
+
/** What a bound unit module exports — the port instances a piece of that kind may read. */
|
|
416
|
+
type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
|
|
417
|
+
/** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
|
|
418
|
+
type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
|
|
419
|
+
/**
|
|
420
|
+
* A piece injecting a port the bound unit module does not export. `unknown`
|
|
421
|
+
* when every declared port is covered — including the empty case, so a worker
|
|
422
|
+
* whose pieces declare no `unit:` is gated on nothing.
|
|
423
|
+
*/
|
|
424
|
+
type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
|
|
425
|
+
readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
|
|
426
|
+
};
|
|
427
|
+
/**
|
|
428
|
+
* One `(helpers, input)` implementation with its piece's declared record put on
|
|
429
|
+
* `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
|
|
430
|
+
* Applied once per piece as di constructs it, so a unit costs one record and
|
|
431
|
+
* one context object. Each worker maps it over its own entry shape.
|
|
432
|
+
*/
|
|
433
|
+
export declare const withUnitRecord: (record: Readonly<Record<string, AnyPort>>, implementation: (helpers: never, input: never) => unknown) => (helpers: {
|
|
434
|
+
readonly context?: Readonly<Record<string | symbol, unknown>>;
|
|
435
|
+
}, input: never) => unknown;
|
|
436
|
+
/**
|
|
437
|
+
* A worker middleware's body: observe `operation`, open one kernel unit, fork
|
|
438
|
+
* `unit` — when one is bound — seeded with `seed`, and call `next` with the
|
|
439
|
+
* forked context where {@link withUnitRecord} finds it. With no `unit` bound,
|
|
440
|
+
* `next()` runs unchanged.
|
|
441
|
+
*
|
|
442
|
+
* Observed from whichever channel the unit settles on, a defect included: a
|
|
443
|
+
* defect is a failed unit too, and an errors count that omitted it would be the
|
|
444
|
+
* reassuring half.
|
|
445
|
+
*/
|
|
446
|
+
export declare const dispatchUnit: <T, E>(dispatch: {
|
|
447
|
+
readonly host: RuntimeHost<never>;
|
|
448
|
+
readonly observers: readonly ((operation: Operation) => Settle)[];
|
|
449
|
+
readonly operation: Operation;
|
|
450
|
+
readonly meta: UnitMeta;
|
|
451
|
+
readonly unit: AnyUnitModule | undefined;
|
|
452
|
+
readonly seed: readonly [AnyPort, unknown];
|
|
453
|
+
readonly next: (overrides?: never) => AsyncResult<T, E>;
|
|
454
|
+
}) => AsyncResult<T, E>;
|
|
455
|
+
//#endregion
|
|
385
456
|
//#region src/runtime.d.ts
|
|
386
457
|
declare const RuntimeStartFailed_base: import("unthrown").TaggedErrorConstructor<"RuntimeStartFailed">;
|
|
387
458
|
export declare class RuntimeStartFailed extends RuntimeStartFailed_base<{
|
|
@@ -491,6 +562,14 @@ type Runtime<Resolves extends AnyPort = never, Info = never> = {
|
|
|
491
562
|
readonly name: string;
|
|
492
563
|
readonly resolves: readonly Resolves[];
|
|
493
564
|
readonly start: (host: RuntimeHost<Resolves>) => AsyncResult<Serving<Info>, RuntimeStartFailed>;
|
|
565
|
+
/**
|
|
566
|
+
* The unit modules this runtime forks, by kind — `{ message: … }`, or one
|
|
567
|
+
* per HTTP scheme. Optional, and read for one thing only: a test's
|
|
568
|
+
* `UnitOverrides` name a kind, `fork` is handed a module, and this is
|
|
569
|
+
* what maps one onto the other. A runtime that omits it refuses every unit
|
|
570
|
+
* override at boot rather than ignoring it.
|
|
571
|
+
*/
|
|
572
|
+
readonly units?: Readonly<Record<string, AnyUnitModule | undefined>>;
|
|
494
573
|
};
|
|
495
574
|
/**
|
|
496
575
|
* The port the kernel resolves its runtime from. A runtime is a **service the
|
|
@@ -834,63 +913,20 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
|
|
|
834
913
|
*/
|
|
835
914
|
export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
|
|
836
915
|
//#endregion
|
|
837
|
-
//#region src/unit-
|
|
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
|
-
};
|
|
916
|
+
//#region src/unit-overrides.d.ts
|
|
917
|
+
declare const UnitOverrides_base: import("@btravstack/di").ManyPortClass<"UnitOverrides">;
|
|
866
918
|
/**
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
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.
|
|
919
|
+
* Providers substituted inside a runtime's unit modules, keyed by the kind
|
|
920
|
+
* the runtime declares on `Runtime.units` — test-facing: `@btravstack/testing`'s
|
|
921
|
+
* `overridden(root, providers, { unit })` is the one contributor, and a
|
|
922
|
+
* production root never names it.
|
|
880
923
|
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
924
|
+
* Each provider must already carry di's override brand. The kernel applies
|
|
925
|
+
* them at boot, before the runtime starts: every kind named must be one the
|
|
926
|
+
* runtime binds, and every port must be one that kind's module provides, or
|
|
927
|
+
* the boot is a defect — the same drift gate a root-level override has.
|
|
884
928
|
*/
|
|
885
|
-
export declare
|
|
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>;
|
|
929
|
+
export declare class UnitOverrides extends UnitOverrides_base<Readonly<Record<string, readonly AnyProvider[]>>> {}
|
|
894
930
|
//#endregion
|
|
895
931
|
//#region src/pieces.d.ts
|
|
896
932
|
/**
|
|
@@ -942,4 +978,4 @@ export declare const noObserverMember: Provider<Observers, never, never> & {
|
|
|
942
978
|
readonly port: typeof Observers;
|
|
943
979
|
};
|
|
944
980
|
//#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 };
|
|
981
|
+
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, Provider, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
|
|
1
|
+
import { AnyPort, AnyProvider, 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
|
|
@@ -352,6 +352,18 @@ type UnitRecord = {
|
|
|
352
352
|
readonly signal: AbortSignal;
|
|
353
353
|
};
|
|
354
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;
|
|
355
367
|
/**
|
|
356
368
|
* What a runtime says about one piece of work as it submits it. `kind` is the
|
|
357
369
|
* category (`"http"`, `"tick"`, `"job"`); `id` identifies **this** unit.
|
|
@@ -363,7 +375,8 @@ export declare const currentUnit: () => UnitRecord | undefined;
|
|
|
363
375
|
* silently defeats the ambient record. A route template is a `kind`.
|
|
364
376
|
*
|
|
365
377
|
* The kernel cannot check this, so uniqueness is the runtime's to guarantee.
|
|
366
|
-
* 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;
|
|
367
380
|
* `traceId` is the CORRELATION id, which is why it is the one a runtime may
|
|
368
381
|
* supply — it carries an id from outside the process.
|
|
369
382
|
*/
|
|
@@ -382,6 +395,64 @@ type UnitRegistry = {
|
|
|
382
395
|
readonly awaitIdle: () => AsyncResult<void, never>;
|
|
383
396
|
};
|
|
384
397
|
//#endregion
|
|
398
|
+
//#region src/unit-record.d.ts
|
|
399
|
+
/**
|
|
400
|
+
* A module a runtime's `unit` option may bind, as the upper bound a starter
|
|
401
|
+
* constrains its own `Unit` type parameter to. `Module`'s `_exports` channel is
|
|
402
|
+
* contravariant, so `Exports = never` — never `unknown` — is what makes a REAL
|
|
403
|
+
* module's own (necessarily narrower) export type assignable to this bound:
|
|
404
|
+
* `(x: Concrete) => void` is assignable to `(x: never) => void`, not to
|
|
405
|
+
* `(x: unknown) => void`.
|
|
406
|
+
*/
|
|
407
|
+
type AnyUnitModule = Module<never, never, unknown>;
|
|
408
|
+
/**
|
|
409
|
+
* The needs a bound unit module still owes, or `never` when none is bound.
|
|
410
|
+
* `Scope` is excluded, since nothing can ever provide it — the same exemption
|
|
411
|
+
* `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
|
|
412
|
+
* fork seed discharges.
|
|
413
|
+
*/
|
|
414
|
+
type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
|
|
415
|
+
/** What a bound unit module exports — the port instances a piece of that kind may read. */
|
|
416
|
+
type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
|
|
417
|
+
/** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
|
|
418
|
+
type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
|
|
419
|
+
/**
|
|
420
|
+
* A piece injecting a port the bound unit module does not export. `unknown`
|
|
421
|
+
* when every declared port is covered — including the empty case, so a worker
|
|
422
|
+
* whose pieces declare no `unit:` is gated on nothing.
|
|
423
|
+
*/
|
|
424
|
+
type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
|
|
425
|
+
readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
|
|
426
|
+
};
|
|
427
|
+
/**
|
|
428
|
+
* One `(helpers, input)` implementation with its piece's declared record put on
|
|
429
|
+
* `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
|
|
430
|
+
* Applied once per piece as di constructs it, so a unit costs one record and
|
|
431
|
+
* one context object. Each worker maps it over its own entry shape.
|
|
432
|
+
*/
|
|
433
|
+
export declare const withUnitRecord: (record: Readonly<Record<string, AnyPort>>, implementation: (helpers: never, input: never) => unknown) => (helpers: {
|
|
434
|
+
readonly context?: Readonly<Record<string | symbol, unknown>>;
|
|
435
|
+
}, input: never) => unknown;
|
|
436
|
+
/**
|
|
437
|
+
* A worker middleware's body: observe `operation`, open one kernel unit, fork
|
|
438
|
+
* `unit` — when one is bound — seeded with `seed`, and call `next` with the
|
|
439
|
+
* forked context where {@link withUnitRecord} finds it. With no `unit` bound,
|
|
440
|
+
* `next()` runs unchanged.
|
|
441
|
+
*
|
|
442
|
+
* Observed from whichever channel the unit settles on, a defect included: a
|
|
443
|
+
* defect is a failed unit too, and an errors count that omitted it would be the
|
|
444
|
+
* reassuring half.
|
|
445
|
+
*/
|
|
446
|
+
export declare const dispatchUnit: <T, E>(dispatch: {
|
|
447
|
+
readonly host: RuntimeHost<never>;
|
|
448
|
+
readonly observers: readonly ((operation: Operation) => Settle)[];
|
|
449
|
+
readonly operation: Operation;
|
|
450
|
+
readonly meta: UnitMeta;
|
|
451
|
+
readonly unit: AnyUnitModule | undefined;
|
|
452
|
+
readonly seed: readonly [AnyPort, unknown];
|
|
453
|
+
readonly next: (overrides?: never) => AsyncResult<T, E>;
|
|
454
|
+
}) => AsyncResult<T, E>;
|
|
455
|
+
//#endregion
|
|
385
456
|
//#region src/runtime.d.ts
|
|
386
457
|
declare const RuntimeStartFailed_base: import("unthrown").TaggedErrorConstructor<"RuntimeStartFailed">;
|
|
387
458
|
export declare class RuntimeStartFailed extends RuntimeStartFailed_base<{
|
|
@@ -491,6 +562,14 @@ type Runtime<Resolves extends AnyPort = never, Info = never> = {
|
|
|
491
562
|
readonly name: string;
|
|
492
563
|
readonly resolves: readonly Resolves[];
|
|
493
564
|
readonly start: (host: RuntimeHost<Resolves>) => AsyncResult<Serving<Info>, RuntimeStartFailed>;
|
|
565
|
+
/**
|
|
566
|
+
* The unit modules this runtime forks, by kind — `{ message: … }`, or one
|
|
567
|
+
* per HTTP scheme. Optional, and read for one thing only: a test's
|
|
568
|
+
* `UnitOverrides` name a kind, `fork` is handed a module, and this is
|
|
569
|
+
* what maps one onto the other. A runtime that omits it refuses every unit
|
|
570
|
+
* override at boot rather than ignoring it.
|
|
571
|
+
*/
|
|
572
|
+
readonly units?: Readonly<Record<string, AnyUnitModule | undefined>>;
|
|
494
573
|
};
|
|
495
574
|
/**
|
|
496
575
|
* The port the kernel resolves its runtime from. A runtime is a **service the
|
|
@@ -834,63 +913,20 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
|
|
|
834
913
|
*/
|
|
835
914
|
export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
|
|
836
915
|
//#endregion
|
|
837
|
-
//#region src/unit-
|
|
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
|
-
};
|
|
916
|
+
//#region src/unit-overrides.d.ts
|
|
917
|
+
declare const UnitOverrides_base: import("@btravstack/di").ManyPortClass<"UnitOverrides">;
|
|
866
918
|
/**
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
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.
|
|
919
|
+
* Providers substituted inside a runtime's unit modules, keyed by the kind
|
|
920
|
+
* the runtime declares on `Runtime.units` — test-facing: `@btravstack/testing`'s
|
|
921
|
+
* `overridden(root, providers, { unit })` is the one contributor, and a
|
|
922
|
+
* production root never names it.
|
|
880
923
|
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
924
|
+
* Each provider must already carry di's override brand. The kernel applies
|
|
925
|
+
* them at boot, before the runtime starts: every kind named must be one the
|
|
926
|
+
* runtime binds, and every port must be one that kind's module provides, or
|
|
927
|
+
* the boot is a defect — the same drift gate a root-level override has.
|
|
884
928
|
*/
|
|
885
|
-
export declare
|
|
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>;
|
|
929
|
+
export declare class UnitOverrides extends UnitOverrides_base<Readonly<Record<string, readonly AnyProvider[]>>> {}
|
|
894
930
|
//#endregion
|
|
895
931
|
//#region src/pieces.d.ts
|
|
896
932
|
/**
|
|
@@ -942,4 +978,4 @@ export declare const noObserverMember: Provider<Observers, never, never> & {
|
|
|
942
978
|
readonly port: typeof Observers;
|
|
943
979
|
};
|
|
944
980
|
//#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 };
|
|
981
|
+
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
|
@@ -4,6 +4,7 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
|
4
4
|
import { Config, ConfigInvalid, Env } from "@btravstack/config";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
7
8
|
//#region src/instrumentation.ts
|
|
8
9
|
/**
|
|
9
10
|
* The set port a package contributes its instrumentation to.
|
|
@@ -427,14 +428,71 @@ const installUncaughtHandlers = (onUncaught) => {
|
|
|
427
428
|
};
|
|
428
429
|
};
|
|
429
430
|
//#endregion
|
|
431
|
+
//#region src/unit-overrides.ts
|
|
432
|
+
/**
|
|
433
|
+
* Providers substituted inside a runtime's unit modules, keyed by the kind
|
|
434
|
+
* the runtime declares on `Runtime.units` — test-facing: `@btravstack/testing`'s
|
|
435
|
+
* `overridden(root, providers, { unit })` is the one contributor, and a
|
|
436
|
+
* production root never names it.
|
|
437
|
+
*
|
|
438
|
+
* Each provider must already carry di's override brand. The kernel applies
|
|
439
|
+
* them at boot, before the runtime starts: every kind named must be one the
|
|
440
|
+
* runtime binds, and every port must be one that kind's module provides, or
|
|
441
|
+
* the boot is a defect — the same drift gate a root-level override has.
|
|
442
|
+
*/
|
|
443
|
+
var UnitOverrides = class extends Port.many("UnitOverrides") {};
|
|
444
|
+
/** Whether `module`'s tree carries a provider for `portId`. */
|
|
445
|
+
const provides = (module, portId) => module.provides.some((provider) => provider.port.portId === portId) || module.imports.some((imported) => provides(imported, portId));
|
|
446
|
+
/**
|
|
447
|
+
* Each overridden kind's module, mapped to the module the kernel forks in its
|
|
448
|
+
* place. Keyed by module IDENTITY, because `UnitHost.fork` is handed the
|
|
449
|
+
* module and never the kind — which is why a module bound under two kinds is
|
|
450
|
+
* refused rather than overridden under both.
|
|
451
|
+
*/
|
|
452
|
+
const unitSubstitutes = (runtime, units, overrides) => {
|
|
453
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const member of overrides) for (const [kind, providers] of Object.entries(member)) byKind.set(kind, [...byKind.get(kind) ?? [], ...providers]);
|
|
455
|
+
const substitutes = /* @__PURE__ */ new Map();
|
|
456
|
+
for (const [kind, providers] of byKind) {
|
|
457
|
+
const module = units[kind];
|
|
458
|
+
if (module === void 0) throw new Error(`[core] unit override for kind "${kind}", which runtime "${runtime}" binds no module for`);
|
|
459
|
+
const shared = Object.keys(units).find((other) => other !== kind && units[other] === module);
|
|
460
|
+
if (shared !== void 0) throw new Error(`[core] unit override for kind "${kind}", whose module kind "${shared}" binds too — an override cannot reach one without the other`);
|
|
461
|
+
const seen = /* @__PURE__ */ new Set();
|
|
462
|
+
for (const provider of providers) {
|
|
463
|
+
if (seen.has(provider.port.portId)) throw new Error(`[core] two unit overrides registered for port ${JSON.stringify(provider.port.portId)} in kind "${kind}"`);
|
|
464
|
+
seen.add(provider.port.portId);
|
|
465
|
+
if (!provides(module, provider.port.portId)) throw new Error(`[core] unit override for port ${JSON.stringify(provider.port.portId)} in kind "${kind}" with nothing to override — its module no longer provides it`);
|
|
466
|
+
}
|
|
467
|
+
substitutes.set(module, Module("UnitOverridden")({
|
|
468
|
+
imports: [module],
|
|
469
|
+
provides: providers,
|
|
470
|
+
exports: [module]
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
return substitutes;
|
|
474
|
+
};
|
|
475
|
+
//#endregion
|
|
430
476
|
//#region src/units.ts
|
|
431
477
|
const storage = new AsyncLocalStorage();
|
|
432
478
|
const runWithUnit = (record, fn) => storage.run(record, fn);
|
|
433
479
|
const currentUnit = () => storage.getStore();
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
480
|
+
const outcomes = /* @__PURE__ */ new WeakMap();
|
|
481
|
+
const settleUnit = (record, outcome) => {
|
|
482
|
+
outcomes.set(record, outcome);
|
|
483
|
+
};
|
|
484
|
+
/**
|
|
485
|
+
* How the current unit's work settled, for the code that runs AFTER it: a
|
|
486
|
+
* unit module's `onStop`, which tears down inside the record but is handed
|
|
487
|
+
* only its own service. `undefined` outside a unit and while the work is still
|
|
488
|
+
* running. Whether the unit was aborted is the record's own `signal`.
|
|
489
|
+
*
|
|
490
|
+
* It is the kernel's outcome, not the transport's: an HTTP unit that answered
|
|
491
|
+
* a `500` still settled `ok`, since the kernel never maps a status.
|
|
492
|
+
*/
|
|
493
|
+
const unitOutcome = () => {
|
|
494
|
+
const unit = currentUnit();
|
|
495
|
+
return unit === void 0 ? void 0 : outcomes.get(unit);
|
|
438
496
|
};
|
|
439
497
|
const createUnitRegistry = () => {
|
|
440
498
|
const open = /* @__PURE__ */ new Set();
|
|
@@ -450,7 +508,7 @@ const createUnitRegistry = () => {
|
|
|
450
508
|
const controller = new AbortController();
|
|
451
509
|
open.add(controller);
|
|
452
510
|
const record = {
|
|
453
|
-
unitId:
|
|
511
|
+
unitId: randomUUID(),
|
|
454
512
|
traceId: meta.traceId ?? meta.id,
|
|
455
513
|
tenantId: meta.tenantId,
|
|
456
514
|
signal: controller.signal
|
|
@@ -484,7 +542,7 @@ const createUnitRegistry = () => {
|
|
|
484
542
|
* and whose side effects then have to be guarded one by one.
|
|
485
543
|
*/
|
|
486
544
|
const withdrawn = () => fromSafePromise(new Promise(() => {}));
|
|
487
|
-
const providesEnv = (module) =>
|
|
545
|
+
const providesEnv = (module) => provides(module, Env.portId);
|
|
488
546
|
/**
|
|
489
547
|
* What the kernel reads from the environment for itself. Each field is pinned
|
|
490
548
|
* by the matching `StartOptions` field — explicit beats environment beats
|
|
@@ -531,7 +589,13 @@ const readKernelConfig = (options, env) => {
|
|
|
531
589
|
const start = (module, options = {}) => {
|
|
532
590
|
const clock = options.clock ?? systemClock;
|
|
533
591
|
const env = options.env ?? process.env;
|
|
534
|
-
const
|
|
592
|
+
const sink = safeSink(options.onEvent ?? stderrSink);
|
|
593
|
+
let exitedEmitted = false;
|
|
594
|
+
const emit = (event) => {
|
|
595
|
+
if (exitedEmitted) return;
|
|
596
|
+
sink(event);
|
|
597
|
+
if (event.type === "exited") exitedEmitted = true;
|
|
598
|
+
};
|
|
535
599
|
let runtimeName = "";
|
|
536
600
|
let servingInfo = void 0;
|
|
537
601
|
let probeBoundPort = void 0;
|
|
@@ -583,6 +647,7 @@ const start = (module, options = {}) => {
|
|
|
583
647
|
}
|
|
584
648
|
});
|
|
585
649
|
const disposeUncaught = options.signals === false ? () => {} : installUncaughtHandlers((cause) => {
|
|
650
|
+
crashed = true;
|
|
586
651
|
emit({
|
|
587
652
|
type: "uncaught",
|
|
588
653
|
cause
|
|
@@ -593,6 +658,8 @@ const start = (module, options = {}) => {
|
|
|
593
658
|
abandonBuild("uncaught");
|
|
594
659
|
});
|
|
595
660
|
let disposeProbes = () => {};
|
|
661
|
+
let crashed = false;
|
|
662
|
+
let startupFailing = false;
|
|
596
663
|
const disposeAll = () => {
|
|
597
664
|
disposeSignals();
|
|
598
665
|
disposeUncaught();
|
|
@@ -600,7 +667,7 @@ const start = (module, options = {}) => {
|
|
|
600
667
|
disposeProbes();
|
|
601
668
|
};
|
|
602
669
|
const reportOf = (reason, drain, abandonedAt) => ({
|
|
603
|
-
reason,
|
|
670
|
+
reason: crashed ? "uncaught" : reason,
|
|
604
671
|
drain,
|
|
605
672
|
teardownErrors,
|
|
606
673
|
uptimeMs: clock.now() - startedAt,
|
|
@@ -615,16 +682,24 @@ const start = (module, options = {}) => {
|
|
|
615
682
|
tracker.advanceTo("stopping");
|
|
616
683
|
disposeAll();
|
|
617
684
|
};
|
|
685
|
+
const leaveFailed = (failure) => {
|
|
686
|
+
if (tracker.current() !== "stopping") emit({
|
|
687
|
+
type: "startFailed",
|
|
688
|
+
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
689
|
+
});
|
|
690
|
+
runtimePublished.resolve(void 0);
|
|
691
|
+
tracker.advanceTo("stopping");
|
|
692
|
+
};
|
|
618
693
|
const stopping = Promise.withResolvers();
|
|
619
|
-
const stopAbandoned = () => fromSafePromise(stopping.promise).flatMap((
|
|
620
|
-
const abandonStop = (
|
|
694
|
+
const stopAbandoned = () => fromSafePromise(stopping.promise).flatMap((abandoned) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : abandonStop(abandoned)));
|
|
695
|
+
const abandonStop = (abandoned) => {
|
|
621
696
|
emit({
|
|
622
697
|
type: "stoppedWaiting",
|
|
623
698
|
phase: "stop",
|
|
624
699
|
afterMs: stopSettled.signal.aborted ? void 0 : stopTimeoutMs
|
|
625
700
|
});
|
|
626
701
|
disposeAll();
|
|
627
|
-
return reportOf(reason, drain, "stop");
|
|
702
|
+
return abandoned().map(({ reason, drain }) => reportOf(reason, drain, "stop"));
|
|
628
703
|
};
|
|
629
704
|
const abandoningBuild = Promise.withResolvers();
|
|
630
705
|
const buildAbandoned = () => fromSafePromise(abandoningBuild.promise).map((reason) => {
|
|
@@ -639,7 +714,7 @@ const start = (module, options = {}) => {
|
|
|
639
714
|
});
|
|
640
715
|
const abandonBuild = (reason) => {
|
|
641
716
|
const phase = tracker.current();
|
|
642
|
-
if (phase === "building" || phase === "starting") abandoningBuild.resolve(reason);
|
|
717
|
+
if (phase === "building" || phase === "starting" || reason === "uncaught" && startupFailing) abandoningBuild.resolve(reason);
|
|
643
718
|
};
|
|
644
719
|
emit({ type: "building" });
|
|
645
720
|
const probesOptions = kernelConfig.map(({ probePort, probeHost }) => options.probes === false ? false : {
|
|
@@ -700,22 +775,23 @@ const start = (module, options = {}) => {
|
|
|
700
775
|
if (reason !== "signal") registry.abortAll();
|
|
701
776
|
return (reason === "signal" ? runDrain(serving) : OkAsync(void 0)).flatMap((report) => {
|
|
702
777
|
tracker.advanceTo("stopping");
|
|
703
|
-
stopping.resolve({
|
|
778
|
+
stopping.resolve(() => OkAsync({
|
|
704
779
|
reason,
|
|
705
780
|
drain: report
|
|
706
|
-
});
|
|
707
|
-
return serving.stop().map(() => {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
});
|
|
781
|
+
}));
|
|
782
|
+
return serving.stop().map(() => ({
|
|
783
|
+
reason,
|
|
784
|
+
drain: report
|
|
785
|
+
}));
|
|
711
786
|
});
|
|
712
787
|
};
|
|
713
|
-
const
|
|
788
|
+
const serve = (ctx) => {
|
|
714
789
|
tracker.advanceTo("starting");
|
|
715
790
|
const runtime = ctx.get(RuntimePort);
|
|
716
791
|
runtimeName = runtime.name;
|
|
717
792
|
healthChecks = ctx.get(HealthChecks);
|
|
718
793
|
const runtimeCtx = ctx;
|
|
794
|
+
const substitutes = unitSubstitutes(runtime.name, runtime.units ?? {}, ctx.get(UnitOverrides));
|
|
719
795
|
const run = (meta, work) => registry.run(meta, (signal) => {
|
|
720
796
|
const settled = Promise.withResolvers();
|
|
721
797
|
let closing;
|
|
@@ -724,12 +800,12 @@ const start = (module, options = {}) => {
|
|
|
724
800
|
if (hasSettled) return fromSafePromise(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
|
|
725
801
|
if (closing !== void 0) return fromSafePromise(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
|
|
726
802
|
const ready = Promise.withResolvers();
|
|
727
|
-
closing = Module.forkScope(ctx, module, (forked) => {
|
|
803
|
+
closing = Module.forkScope(ctx, substitutes.get(module) ?? module, (forked) => {
|
|
728
804
|
ready.resolve(forked);
|
|
729
805
|
return fromSafePromise(settled.promise);
|
|
730
806
|
}, {
|
|
731
807
|
seed,
|
|
732
|
-
onTeardownError: (port, cause) =>
|
|
808
|
+
onTeardownError: (port, cause) => sink({
|
|
733
809
|
type: "teardownError",
|
|
734
810
|
port,
|
|
735
811
|
cause
|
|
@@ -740,13 +816,18 @@ const start = (module, options = {}) => {
|
|
|
740
816
|
}).get();
|
|
741
817
|
return fromSafePromise(ready.promise);
|
|
742
818
|
};
|
|
819
|
+
const record = currentUnit();
|
|
743
820
|
const outcome = (async () => {
|
|
821
|
+
let settledAs = "error";
|
|
744
822
|
try {
|
|
745
|
-
|
|
823
|
+
const result = await work({
|
|
746
824
|
ctx: runtimeCtx,
|
|
747
825
|
fork
|
|
748
826
|
}, signal);
|
|
827
|
+
if (result.isOk()) settledAs = "ok";
|
|
828
|
+
return result;
|
|
749
829
|
} finally {
|
|
830
|
+
if (record !== void 0) settleUnit(record, settledAs);
|
|
750
831
|
hasSettled = true;
|
|
751
832
|
settled.resolve();
|
|
752
833
|
if (closing !== void 0) await closing;
|
|
@@ -767,23 +848,32 @@ const start = (module, options = {}) => {
|
|
|
767
848
|
});
|
|
768
849
|
return fromSafePromise(shutdown.promise).flatMap((reason) => finish(serving, reason));
|
|
769
850
|
});
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
851
|
+
};
|
|
852
|
+
const built = () => Module.scoped(root, (ctx) => OkAsync(ctx).flatMap(serve).tapFailure((failure) => {
|
|
853
|
+
if (tracker.current() === "starting") startupFailing = true;
|
|
854
|
+
leaveFailed(failure);
|
|
855
|
+
stopping.resolve(() => failure.toAsync());
|
|
856
|
+
}), {
|
|
857
|
+
onConstructionFailure: (failure) => {
|
|
858
|
+
startupFailing = true;
|
|
859
|
+
leaveFailed(failure);
|
|
860
|
+
stopping.resolve(() => failure.toAsync());
|
|
861
|
+
},
|
|
862
|
+
onTeardownError: (port, cause) => {
|
|
863
|
+
teardownErrors.push({
|
|
864
|
+
port,
|
|
865
|
+
cause
|
|
866
|
+
});
|
|
867
|
+
emit({
|
|
868
|
+
type: "teardownError",
|
|
869
|
+
port,
|
|
870
|
+
cause
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}).tapFailure((failure) => {
|
|
874
|
+
leaveFailed(failure);
|
|
875
|
+
disposeAll();
|
|
876
|
+
}).map(({ reason, drain }) => reportOf(reason, drain)).tap(disposeAll).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
|
|
787
877
|
return {
|
|
788
878
|
exited: probesStarted.flatMap(() => fromSafePromise(Promise.race([
|
|
789
879
|
built(),
|
|
@@ -949,4 +1039,4 @@ const noObserverMember = Provider.member(Observers)({
|
|
|
949
1039
|
value: noObserver
|
|
950
1040
|
});
|
|
951
1041
|
//#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 };
|
|
1042
|
+
export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, Observers, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, UnitOverrides, 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.19.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,23 @@
|
|
|
45
45
|
"./package.json": "./package.json"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@btravstack/config": "0.
|
|
49
|
-
"@btravstack/di": "0.
|
|
48
|
+
"@btravstack/config": "0.19.0",
|
|
49
|
+
"@btravstack/di": "0.19.0",
|
|
50
50
|
"@btravstack/tsconfig": "0.4.0",
|
|
51
|
+
"@stryker-mutator/core": "10.0.0",
|
|
52
|
+
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
51
53
|
"@types/node": "26.6.4",
|
|
52
54
|
"@unthrown/vitest": "5.12.0",
|
|
53
55
|
"@vitest/coverage-v8": "5.0.3",
|
|
54
56
|
"tsdown": "0.23.0",
|
|
55
57
|
"typescript": "7.0.2",
|
|
58
|
+
"typescript-consumer": "npm:typescript@5.9.3",
|
|
56
59
|
"unthrown": "5.12.0",
|
|
57
60
|
"vitest": "5.0.3"
|
|
58
61
|
},
|
|
59
62
|
"peerDependencies": {
|
|
60
|
-
"@btravstack/config": "^0.
|
|
61
|
-
"@btravstack/di": "^0.
|
|
63
|
+
"@btravstack/config": "^0.19.0",
|
|
64
|
+
"@btravstack/di": "^0.19.0",
|
|
62
65
|
"unthrown": "^5.0.0"
|
|
63
66
|
},
|
|
64
67
|
"engines": {
|
|
@@ -68,7 +71,8 @@
|
|
|
68
71
|
"build": "tsdown src/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.build.json",
|
|
69
72
|
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch --tsconfig tsconfig.build.json",
|
|
70
73
|
"generate": "node ../../docs/scripts/extract-doc-samples.ts --group core --out src/generated/doc-samples",
|
|
74
|
+
"mutate": "stryker run",
|
|
71
75
|
"test": "vitest run --coverage",
|
|
72
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json"
|
|
76
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json && node ./node_modules/typescript-consumer/bin/tsc --noEmit -p tsconfig.test-d.json"
|
|
73
77
|
}
|
|
74
78
|
}
|