@btravstack/core 0.18.0 → 0.20.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 +107 -36
- package/dist/index.d.cts +78 -55
- package/dist/index.d.mts +78 -55
- package/dist/index.mjs +107 -37
- package/package.json +8 -5
package/dist/index.cjs
CHANGED
|
@@ -429,6 +429,51 @@ const installUncaughtHandlers = (onUncaught) => {
|
|
|
429
429
|
};
|
|
430
430
|
};
|
|
431
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
|
|
432
477
|
//#region src/units.ts
|
|
433
478
|
const storage = new node_async_hooks.AsyncLocalStorage();
|
|
434
479
|
const runWithUnit = (record, fn) => storage.run(record, fn);
|
|
@@ -498,7 +543,7 @@ const createUnitRegistry = () => {
|
|
|
498
543
|
* and whose side effects then have to be guarded one by one.
|
|
499
544
|
*/
|
|
500
545
|
const withdrawn = () => (0, unthrown.fromSafePromise)(new Promise(() => {}));
|
|
501
|
-
const providesEnv = (module) =>
|
|
546
|
+
const providesEnv = (module) => provides(module, _btravstack_config.Env.portId);
|
|
502
547
|
/**
|
|
503
548
|
* What the kernel reads from the environment for itself. Each field is pinned
|
|
504
549
|
* by the matching `StartOptions` field — explicit beats environment beats
|
|
@@ -545,7 +590,13 @@ const readKernelConfig = (options, env) => {
|
|
|
545
590
|
const start = (module, options = {}) => {
|
|
546
591
|
const clock = options.clock ?? systemClock;
|
|
547
592
|
const env = options.env ?? process.env;
|
|
548
|
-
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
|
+
};
|
|
549
600
|
let runtimeName = "";
|
|
550
601
|
let servingInfo = void 0;
|
|
551
602
|
let probeBoundPort = void 0;
|
|
@@ -597,6 +648,7 @@ const start = (module, options = {}) => {
|
|
|
597
648
|
}
|
|
598
649
|
});
|
|
599
650
|
const disposeUncaught = options.signals === false ? () => {} : installUncaughtHandlers((cause) => {
|
|
651
|
+
crashed = true;
|
|
600
652
|
emit({
|
|
601
653
|
type: "uncaught",
|
|
602
654
|
cause
|
|
@@ -607,6 +659,8 @@ const start = (module, options = {}) => {
|
|
|
607
659
|
abandonBuild("uncaught");
|
|
608
660
|
});
|
|
609
661
|
let disposeProbes = () => {};
|
|
662
|
+
let crashed = false;
|
|
663
|
+
let startupFailing = false;
|
|
610
664
|
const disposeAll = () => {
|
|
611
665
|
disposeSignals();
|
|
612
666
|
disposeUncaught();
|
|
@@ -614,7 +668,7 @@ const start = (module, options = {}) => {
|
|
|
614
668
|
disposeProbes();
|
|
615
669
|
};
|
|
616
670
|
const reportOf = (reason, drain, abandonedAt) => ({
|
|
617
|
-
reason,
|
|
671
|
+
reason: crashed ? "uncaught" : reason,
|
|
618
672
|
drain,
|
|
619
673
|
teardownErrors,
|
|
620
674
|
uptimeMs: clock.now() - startedAt,
|
|
@@ -629,16 +683,24 @@ const start = (module, options = {}) => {
|
|
|
629
683
|
tracker.advanceTo("stopping");
|
|
630
684
|
disposeAll();
|
|
631
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
|
+
};
|
|
632
694
|
const stopping = Promise.withResolvers();
|
|
633
|
-
const stopAbandoned = () => (0, unthrown.fromSafePromise)(stopping.promise).flatMap((
|
|
634
|
-
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) => {
|
|
635
697
|
emit({
|
|
636
698
|
type: "stoppedWaiting",
|
|
637
699
|
phase: "stop",
|
|
638
700
|
afterMs: stopSettled.signal.aborted ? void 0 : stopTimeoutMs
|
|
639
701
|
});
|
|
640
702
|
disposeAll();
|
|
641
|
-
return reportOf(reason, drain, "stop");
|
|
703
|
+
return abandoned().map(({ reason, drain }) => reportOf(reason, drain, "stop"));
|
|
642
704
|
};
|
|
643
705
|
const abandoningBuild = Promise.withResolvers();
|
|
644
706
|
const buildAbandoned = () => (0, unthrown.fromSafePromise)(abandoningBuild.promise).map((reason) => {
|
|
@@ -653,7 +715,7 @@ const start = (module, options = {}) => {
|
|
|
653
715
|
});
|
|
654
716
|
const abandonBuild = (reason) => {
|
|
655
717
|
const phase = tracker.current();
|
|
656
|
-
if (phase === "building" || phase === "starting") abandoningBuild.resolve(reason);
|
|
718
|
+
if (phase === "building" || phase === "starting" || reason === "uncaught" && startupFailing) abandoningBuild.resolve(reason);
|
|
657
719
|
};
|
|
658
720
|
emit({ type: "building" });
|
|
659
721
|
const probesOptions = kernelConfig.map(({ probePort, probeHost }) => options.probes === false ? false : {
|
|
@@ -714,22 +776,23 @@ const start = (module, options = {}) => {
|
|
|
714
776
|
if (reason !== "signal") registry.abortAll();
|
|
715
777
|
return (reason === "signal" ? runDrain(serving) : (0, unthrown.OkAsync)(void 0)).flatMap((report) => {
|
|
716
778
|
tracker.advanceTo("stopping");
|
|
717
|
-
stopping.resolve({
|
|
779
|
+
stopping.resolve(() => (0, unthrown.OkAsync)({
|
|
718
780
|
reason,
|
|
719
781
|
drain: report
|
|
720
|
-
});
|
|
721
|
-
return serving.stop().map(() => {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
});
|
|
782
|
+
}));
|
|
783
|
+
return serving.stop().map(() => ({
|
|
784
|
+
reason,
|
|
785
|
+
drain: report
|
|
786
|
+
}));
|
|
725
787
|
});
|
|
726
788
|
};
|
|
727
|
-
const
|
|
789
|
+
const serve = (ctx) => {
|
|
728
790
|
tracker.advanceTo("starting");
|
|
729
791
|
const runtime = ctx.get(RuntimePort);
|
|
730
792
|
runtimeName = runtime.name;
|
|
731
793
|
healthChecks = ctx.get(HealthChecks);
|
|
732
794
|
const runtimeCtx = ctx;
|
|
795
|
+
const substitutes = unitSubstitutes(runtime.name, runtime.units ?? {}, ctx.get(UnitOverrides));
|
|
733
796
|
const run = (meta, work) => registry.run(meta, (signal) => {
|
|
734
797
|
const settled = Promise.withResolvers();
|
|
735
798
|
let closing;
|
|
@@ -738,12 +801,12 @@ const start = (module, options = {}) => {
|
|
|
738
801
|
if (hasSettled) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
|
|
739
802
|
if (closing !== void 0) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
|
|
740
803
|
const ready = Promise.withResolvers();
|
|
741
|
-
closing = _btravstack_di.Module.forkScope(ctx, module, (forked) => {
|
|
804
|
+
closing = _btravstack_di.Module.forkScope(ctx, substitutes.get(module) ?? module, (forked) => {
|
|
742
805
|
ready.resolve(forked);
|
|
743
806
|
return (0, unthrown.fromSafePromise)(settled.promise);
|
|
744
807
|
}, {
|
|
745
808
|
seed,
|
|
746
|
-
onTeardownError: (port, cause) =>
|
|
809
|
+
onTeardownError: (port, cause) => sink({
|
|
747
810
|
type: "teardownError",
|
|
748
811
|
port,
|
|
749
812
|
cause
|
|
@@ -781,28 +844,35 @@ const start = (module, options = {}) => {
|
|
|
781
844
|
servingInfo = serving.info;
|
|
782
845
|
tracker.advanceTo("serving");
|
|
783
846
|
runtimePublished.resolve(serving.info);
|
|
784
|
-
serving.stopped?.().
|
|
785
|
-
requestShutdown("runtimeStopped");
|
|
786
|
-
});
|
|
847
|
+
serving.stopped?.().tap(() => requestShutdown("runtimeStopped")).tapFailure(() => requestShutdown("runtimeStopped"));
|
|
787
848
|
return (0, unthrown.fromSafePromise)(shutdown.promise).flatMap((reason) => finish(serving, reason));
|
|
788
849
|
});
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
850
|
+
};
|
|
851
|
+
const built = () => _btravstack_di.Module.scoped(root, (ctx) => (0, unthrown.OkAsync)(ctx).flatMap(serve).tapFailure((failure) => {
|
|
852
|
+
if (tracker.current() === "starting") startupFailing = true;
|
|
853
|
+
leaveFailed(failure);
|
|
854
|
+
stopping.resolve(() => failure.toAsync());
|
|
855
|
+
}), {
|
|
856
|
+
onConstructionFailure: (failure) => {
|
|
857
|
+
startupFailing = true;
|
|
858
|
+
leaveFailed(failure);
|
|
859
|
+
stopping.resolve(() => failure.toAsync());
|
|
860
|
+
},
|
|
861
|
+
onTeardownError: (port, cause) => {
|
|
862
|
+
teardownErrors.push({
|
|
863
|
+
port,
|
|
864
|
+
cause
|
|
865
|
+
});
|
|
866
|
+
emit({
|
|
867
|
+
type: "teardownError",
|
|
868
|
+
port,
|
|
869
|
+
cause
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}).tapFailure((failure) => {
|
|
873
|
+
leaveFailed(failure);
|
|
874
|
+
disposeAll();
|
|
875
|
+
}).map(({ reason, drain }) => reportOf(reason, drain)).tap(disposeAll).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
|
|
806
876
|
return {
|
|
807
877
|
exited: probesStarted.flatMap(() => (0, unthrown.fromSafePromise)(Promise.race([
|
|
808
878
|
built(),
|
|
@@ -979,6 +1049,7 @@ exports.RuntimePort = RuntimePort;
|
|
|
979
1049
|
exports.RuntimeStartFailed = RuntimeStartFailed;
|
|
980
1050
|
exports.SPAN_STATUS = SPAN_STATUS;
|
|
981
1051
|
exports.Tracer = Tracer;
|
|
1052
|
+
exports.UnitOverrides = UnitOverrides;
|
|
982
1053
|
exports.composeByPrefix = composeByPrefix;
|
|
983
1054
|
exports.currentUnit = currentUnit;
|
|
984
1055
|
exports.dispatchUnit = dispatchUnit;
|
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
|
/**
|
|
@@ -395,6 +395,64 @@ type UnitRegistry = {
|
|
|
395
395
|
readonly awaitIdle: () => AsyncResult<void, never>;
|
|
396
396
|
};
|
|
397
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
|
|
398
456
|
//#region src/runtime.d.ts
|
|
399
457
|
declare const RuntimeStartFailed_base: import("unthrown").TaggedErrorConstructor<"RuntimeStartFailed">;
|
|
400
458
|
export declare class RuntimeStartFailed extends RuntimeStartFailed_base<{
|
|
@@ -504,6 +562,14 @@ type Runtime<Resolves extends AnyPort = never, Info = never> = {
|
|
|
504
562
|
readonly name: string;
|
|
505
563
|
readonly resolves: readonly Resolves[];
|
|
506
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>>;
|
|
507
573
|
};
|
|
508
574
|
/**
|
|
509
575
|
* The port the kernel resolves its runtime from. A runtime is a **service the
|
|
@@ -847,63 +913,20 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
|
|
|
847
913
|
*/
|
|
848
914
|
export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
|
|
849
915
|
//#endregion
|
|
850
|
-
//#region src/unit-
|
|
916
|
+
//#region src/unit-overrides.d.ts
|
|
917
|
+
declare const UnitOverrides_base: import("@btravstack/di").ManyPortClass<"UnitOverrides">;
|
|
851
918
|
/**
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
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.
|
|
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.
|
|
893
923
|
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
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.
|
|
897
928
|
*/
|
|
898
|
-
export declare
|
|
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>;
|
|
929
|
+
export declare class UnitOverrides extends UnitOverrides_base<Readonly<Record<string, readonly AnyProvider[]>>> {}
|
|
907
930
|
//#endregion
|
|
908
931
|
//#region src/pieces.d.ts
|
|
909
932
|
/**
|
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
|
|
@@ -395,6 +395,64 @@ type UnitRegistry = {
|
|
|
395
395
|
readonly awaitIdle: () => AsyncResult<void, never>;
|
|
396
396
|
};
|
|
397
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
|
|
398
456
|
//#region src/runtime.d.ts
|
|
399
457
|
declare const RuntimeStartFailed_base: import("unthrown").TaggedErrorConstructor<"RuntimeStartFailed">;
|
|
400
458
|
export declare class RuntimeStartFailed extends RuntimeStartFailed_base<{
|
|
@@ -504,6 +562,14 @@ type Runtime<Resolves extends AnyPort = never, Info = never> = {
|
|
|
504
562
|
readonly name: string;
|
|
505
563
|
readonly resolves: readonly Resolves[];
|
|
506
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>>;
|
|
507
573
|
};
|
|
508
574
|
/**
|
|
509
575
|
* The port the kernel resolves its runtime from. A runtime is a **service the
|
|
@@ -847,63 +913,20 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
|
|
|
847
913
|
*/
|
|
848
914
|
export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
|
|
849
915
|
//#endregion
|
|
850
|
-
//#region src/unit-
|
|
916
|
+
//#region src/unit-overrides.d.ts
|
|
917
|
+
declare const UnitOverrides_base: import("@btravstack/di").ManyPortClass<"UnitOverrides">;
|
|
851
918
|
/**
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
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.
|
|
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.
|
|
893
923
|
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
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.
|
|
897
928
|
*/
|
|
898
|
-
export declare
|
|
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>;
|
|
929
|
+
export declare class UnitOverrides extends UnitOverrides_base<Readonly<Record<string, readonly AnyProvider[]>>> {}
|
|
907
930
|
//#endregion
|
|
908
931
|
//#region src/pieces.d.ts
|
|
909
932
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -428,6 +428,51 @@ const installUncaughtHandlers = (onUncaught) => {
|
|
|
428
428
|
};
|
|
429
429
|
};
|
|
430
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
|
|
431
476
|
//#region src/units.ts
|
|
432
477
|
const storage = new AsyncLocalStorage();
|
|
433
478
|
const runWithUnit = (record, fn) => storage.run(record, fn);
|
|
@@ -497,7 +542,7 @@ const createUnitRegistry = () => {
|
|
|
497
542
|
* and whose side effects then have to be guarded one by one.
|
|
498
543
|
*/
|
|
499
544
|
const withdrawn = () => fromSafePromise(new Promise(() => {}));
|
|
500
|
-
const providesEnv = (module) =>
|
|
545
|
+
const providesEnv = (module) => provides(module, Env.portId);
|
|
501
546
|
/**
|
|
502
547
|
* What the kernel reads from the environment for itself. Each field is pinned
|
|
503
548
|
* by the matching `StartOptions` field — explicit beats environment beats
|
|
@@ -544,7 +589,13 @@ const readKernelConfig = (options, env) => {
|
|
|
544
589
|
const start = (module, options = {}) => {
|
|
545
590
|
const clock = options.clock ?? systemClock;
|
|
546
591
|
const env = options.env ?? process.env;
|
|
547
|
-
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
|
+
};
|
|
548
599
|
let runtimeName = "";
|
|
549
600
|
let servingInfo = void 0;
|
|
550
601
|
let probeBoundPort = void 0;
|
|
@@ -596,6 +647,7 @@ const start = (module, options = {}) => {
|
|
|
596
647
|
}
|
|
597
648
|
});
|
|
598
649
|
const disposeUncaught = options.signals === false ? () => {} : installUncaughtHandlers((cause) => {
|
|
650
|
+
crashed = true;
|
|
599
651
|
emit({
|
|
600
652
|
type: "uncaught",
|
|
601
653
|
cause
|
|
@@ -606,6 +658,8 @@ const start = (module, options = {}) => {
|
|
|
606
658
|
abandonBuild("uncaught");
|
|
607
659
|
});
|
|
608
660
|
let disposeProbes = () => {};
|
|
661
|
+
let crashed = false;
|
|
662
|
+
let startupFailing = false;
|
|
609
663
|
const disposeAll = () => {
|
|
610
664
|
disposeSignals();
|
|
611
665
|
disposeUncaught();
|
|
@@ -613,7 +667,7 @@ const start = (module, options = {}) => {
|
|
|
613
667
|
disposeProbes();
|
|
614
668
|
};
|
|
615
669
|
const reportOf = (reason, drain, abandonedAt) => ({
|
|
616
|
-
reason,
|
|
670
|
+
reason: crashed ? "uncaught" : reason,
|
|
617
671
|
drain,
|
|
618
672
|
teardownErrors,
|
|
619
673
|
uptimeMs: clock.now() - startedAt,
|
|
@@ -628,16 +682,24 @@ const start = (module, options = {}) => {
|
|
|
628
682
|
tracker.advanceTo("stopping");
|
|
629
683
|
disposeAll();
|
|
630
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
|
+
};
|
|
631
693
|
const stopping = Promise.withResolvers();
|
|
632
|
-
const stopAbandoned = () => fromSafePromise(stopping.promise).flatMap((
|
|
633
|
-
const abandonStop = (
|
|
694
|
+
const stopAbandoned = () => fromSafePromise(stopping.promise).flatMap((abandoned) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : abandonStop(abandoned)));
|
|
695
|
+
const abandonStop = (abandoned) => {
|
|
634
696
|
emit({
|
|
635
697
|
type: "stoppedWaiting",
|
|
636
698
|
phase: "stop",
|
|
637
699
|
afterMs: stopSettled.signal.aborted ? void 0 : stopTimeoutMs
|
|
638
700
|
});
|
|
639
701
|
disposeAll();
|
|
640
|
-
return reportOf(reason, drain, "stop");
|
|
702
|
+
return abandoned().map(({ reason, drain }) => reportOf(reason, drain, "stop"));
|
|
641
703
|
};
|
|
642
704
|
const abandoningBuild = Promise.withResolvers();
|
|
643
705
|
const buildAbandoned = () => fromSafePromise(abandoningBuild.promise).map((reason) => {
|
|
@@ -652,7 +714,7 @@ const start = (module, options = {}) => {
|
|
|
652
714
|
});
|
|
653
715
|
const abandonBuild = (reason) => {
|
|
654
716
|
const phase = tracker.current();
|
|
655
|
-
if (phase === "building" || phase === "starting") abandoningBuild.resolve(reason);
|
|
717
|
+
if (phase === "building" || phase === "starting" || reason === "uncaught" && startupFailing) abandoningBuild.resolve(reason);
|
|
656
718
|
};
|
|
657
719
|
emit({ type: "building" });
|
|
658
720
|
const probesOptions = kernelConfig.map(({ probePort, probeHost }) => options.probes === false ? false : {
|
|
@@ -713,22 +775,23 @@ const start = (module, options = {}) => {
|
|
|
713
775
|
if (reason !== "signal") registry.abortAll();
|
|
714
776
|
return (reason === "signal" ? runDrain(serving) : OkAsync(void 0)).flatMap((report) => {
|
|
715
777
|
tracker.advanceTo("stopping");
|
|
716
|
-
stopping.resolve({
|
|
778
|
+
stopping.resolve(() => OkAsync({
|
|
717
779
|
reason,
|
|
718
780
|
drain: report
|
|
719
|
-
});
|
|
720
|
-
return serving.stop().map(() => {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
});
|
|
781
|
+
}));
|
|
782
|
+
return serving.stop().map(() => ({
|
|
783
|
+
reason,
|
|
784
|
+
drain: report
|
|
785
|
+
}));
|
|
724
786
|
});
|
|
725
787
|
};
|
|
726
|
-
const
|
|
788
|
+
const serve = (ctx) => {
|
|
727
789
|
tracker.advanceTo("starting");
|
|
728
790
|
const runtime = ctx.get(RuntimePort);
|
|
729
791
|
runtimeName = runtime.name;
|
|
730
792
|
healthChecks = ctx.get(HealthChecks);
|
|
731
793
|
const runtimeCtx = ctx;
|
|
794
|
+
const substitutes = unitSubstitutes(runtime.name, runtime.units ?? {}, ctx.get(UnitOverrides));
|
|
732
795
|
const run = (meta, work) => registry.run(meta, (signal) => {
|
|
733
796
|
const settled = Promise.withResolvers();
|
|
734
797
|
let closing;
|
|
@@ -737,12 +800,12 @@ const start = (module, options = {}) => {
|
|
|
737
800
|
if (hasSettled) return fromSafePromise(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
|
|
738
801
|
if (closing !== void 0) return fromSafePromise(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
|
|
739
802
|
const ready = Promise.withResolvers();
|
|
740
|
-
closing = Module.forkScope(ctx, module, (forked) => {
|
|
803
|
+
closing = Module.forkScope(ctx, substitutes.get(module) ?? module, (forked) => {
|
|
741
804
|
ready.resolve(forked);
|
|
742
805
|
return fromSafePromise(settled.promise);
|
|
743
806
|
}, {
|
|
744
807
|
seed,
|
|
745
|
-
onTeardownError: (port, cause) =>
|
|
808
|
+
onTeardownError: (port, cause) => sink({
|
|
746
809
|
type: "teardownError",
|
|
747
810
|
port,
|
|
748
811
|
cause
|
|
@@ -780,28 +843,35 @@ const start = (module, options = {}) => {
|
|
|
780
843
|
servingInfo = serving.info;
|
|
781
844
|
tracker.advanceTo("serving");
|
|
782
845
|
runtimePublished.resolve(serving.info);
|
|
783
|
-
serving.stopped?.().
|
|
784
|
-
requestShutdown("runtimeStopped");
|
|
785
|
-
});
|
|
846
|
+
serving.stopped?.().tap(() => requestShutdown("runtimeStopped")).tapFailure(() => requestShutdown("runtimeStopped"));
|
|
786
847
|
return fromSafePromise(shutdown.promise).flatMap((reason) => finish(serving, reason));
|
|
787
848
|
});
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
849
|
+
};
|
|
850
|
+
const built = () => Module.scoped(root, (ctx) => OkAsync(ctx).flatMap(serve).tapFailure((failure) => {
|
|
851
|
+
if (tracker.current() === "starting") startupFailing = true;
|
|
852
|
+
leaveFailed(failure);
|
|
853
|
+
stopping.resolve(() => failure.toAsync());
|
|
854
|
+
}), {
|
|
855
|
+
onConstructionFailure: (failure) => {
|
|
856
|
+
startupFailing = true;
|
|
857
|
+
leaveFailed(failure);
|
|
858
|
+
stopping.resolve(() => failure.toAsync());
|
|
859
|
+
},
|
|
860
|
+
onTeardownError: (port, cause) => {
|
|
861
|
+
teardownErrors.push({
|
|
862
|
+
port,
|
|
863
|
+
cause
|
|
864
|
+
});
|
|
865
|
+
emit({
|
|
866
|
+
type: "teardownError",
|
|
867
|
+
port,
|
|
868
|
+
cause
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}).tapFailure((failure) => {
|
|
872
|
+
leaveFailed(failure);
|
|
873
|
+
disposeAll();
|
|
874
|
+
}).map(({ reason, drain }) => reportOf(reason, drain)).tap(disposeAll).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
|
|
805
875
|
return {
|
|
806
876
|
exited: probesStarted.flatMap(() => fromSafePromise(Promise.race([
|
|
807
877
|
built(),
|
|
@@ -967,4 +1037,4 @@ const noObserverMember = Provider.member(Observers)({
|
|
|
967
1037
|
value: noObserver
|
|
968
1038
|
});
|
|
969
1039
|
//#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 };
|
|
1040
|
+
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.20.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,9 +45,11 @@
|
|
|
45
45
|
"./package.json": "./package.json"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@btravstack/config": "0.
|
|
49
|
-
"@btravstack/di": "0.
|
|
48
|
+
"@btravstack/config": "0.20.0",
|
|
49
|
+
"@btravstack/di": "0.20.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",
|
|
@@ -58,8 +60,8 @@
|
|
|
58
60
|
"vitest": "5.0.3"
|
|
59
61
|
},
|
|
60
62
|
"peerDependencies": {
|
|
61
|
-
"@btravstack/config": "^0.
|
|
62
|
-
"@btravstack/di": "^0.
|
|
63
|
+
"@btravstack/config": "^0.20.0",
|
|
64
|
+
"@btravstack/di": "^0.20.0",
|
|
63
65
|
"unthrown": "^5.0.0"
|
|
64
66
|
},
|
|
65
67
|
"engines": {
|
|
@@ -69,6 +71,7 @@
|
|
|
69
71
|
"build": "tsdown src/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.build.json",
|
|
70
72
|
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch --tsconfig tsconfig.build.json",
|
|
71
73
|
"generate": "node ../../docs/scripts/extract-doc-samples.ts --group core --out src/generated/doc-samples",
|
|
74
|
+
"mutate": "stryker run",
|
|
72
75
|
"test": "vitest run --coverage",
|
|
73
76
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json && node ./node_modules/typescript-consumer/bin/tsc --noEmit -p tsconfig.test-d.json"
|
|
74
77
|
}
|