@btravstack/core 0.14.0 → 0.15.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/README.md +30 -4
- package/dist/index.cjs +216 -93
- package/dist/index.d.cts +138 -7
- package/dist/index.d.mts +138 -7
- package/dist/index.mjs +216 -94
- package/package.json +7 -8
package/README.md
CHANGED
|
@@ -114,6 +114,20 @@ is a module that exports no runtime port at all.
|
|
|
114
114
|
|
|
115
115
|
## For a runtime author
|
|
116
116
|
|
|
117
|
+
**`Serving` carries an optional third member, `stopped`** — the channel a
|
|
118
|
+
runtime reports its own death on, `() => AsyncResult<void, never>`. Without it
|
|
119
|
+
the lifecycle only ever moves on a signal or a `stop()` call, so a worker whose
|
|
120
|
+
poll loop died stays alive with `/readyz` answering `200`: a pod in a Service's
|
|
121
|
+
endpoints, consuming nothing. A runtime that can stop on its own account should
|
|
122
|
+
implement it; the ticker above does not, because a `setInterval` has no third
|
|
123
|
+
state to report, and that is why the field is optional rather than required.
|
|
124
|
+
|
|
125
|
+
The obligation is the half a signature cannot express: **withdraw after a stop
|
|
126
|
+
the kernel asked for.** An arm that settles on the ordinary path races every
|
|
127
|
+
clean shutdown. `@btravstack/temporal-worker` is the worked implementation, and
|
|
128
|
+
[the runtime reference](https://btravstack.github.io/btravstack/reference/core/runtime)
|
|
129
|
+
states the contract in full.
|
|
130
|
+
|
|
117
131
|
Two helpers sit beside the `Runtime` contract, because every transport needs
|
|
118
132
|
them and none should own a private copy:
|
|
119
133
|
|
|
@@ -135,11 +149,15 @@ them and none should own a private copy:
|
|
|
135
149
|
telling the runtime to stop accepting — endpoint removal is eventually
|
|
136
150
|
consistent — then in-flight work gets `drainTimeoutMs` (`DRAIN_TIMEOUT_MS`,
|
|
137
151
|
default `20_000`); whatever is still open is aborted and reported
|
|
138
|
-
`abandoned`.
|
|
139
|
-
|
|
140
|
-
|
|
152
|
+
`abandoned`. The teardown that follows gets `stopTimeoutMs`
|
|
153
|
+
(`STOP_TIMEOUT_MS`, default `5_000`), so a `release` that never settles is
|
|
154
|
+
reported as `abandonedAt: "stop"` rather than leaving the process in
|
|
155
|
+
`stopping` with no exit report at all. A second signal stops every wait at
|
|
156
|
+
once. The three sum to the pod's `terminationGracePeriodSeconds` default of
|
|
157
|
+
30 s, which is why the deployment sets them and the options only pin them.
|
|
141
158
|
- **Probes from the state machine.** `GET /livez` and `GET /readyz` on
|
|
142
|
-
`PROBE_PORT` (default `9000`)
|
|
159
|
+
`PROBE_PORT` (default `9000`) and `PROBE_HOST` (default `0.0.0.0`, so a
|
|
160
|
+
kubelet `httpGet` probe reaches them), up before the graph is built.
|
|
143
161
|
- **Configuration from the environment, typed.** The kernel provides
|
|
144
162
|
[`@btravstack/config`](../config)'s `Env` port to every graph it boots; a
|
|
145
163
|
bad value is a `ConfigInvalid` naming every fault at once, and exit `78`.
|
|
@@ -153,6 +171,14 @@ them and none should own a private copy:
|
|
|
153
171
|
there. `signal` is the very `AbortSignal` the unit's work callback is handed,
|
|
154
172
|
so a runtime whose work is a library's `next()` — a Temporal activity, an
|
|
155
173
|
AMQP delivery — can still honour the drain deadline.
|
|
174
|
+
- **Observation as a set port, not a logger in every starter.** A starter
|
|
175
|
+
hands each operation to `Observers` — `observe(observers, operation)` for a
|
|
176
|
+
span-shaped start-then-settle, `observed(observers, operation, call)` around
|
|
177
|
+
one `AsyncResult`-returning call — and holds no `Logger`, `Meter` or
|
|
178
|
+
`Tracer` of its own. Composing
|
|
179
|
+
[`@btravstack/observability`](../observability) is what turns those
|
|
180
|
+
operations into lines, spans and instruments; composing nothing costs an
|
|
181
|
+
inert call.
|
|
156
182
|
- **Nothing throws.** Every async surface is an
|
|
157
183
|
[`unthrown`](https://github.com/btravstack/unthrown) `AsyncResult`;
|
|
158
184
|
`runMain` sets `process.exitCode` — `0` clean, `1` a modeled startup error,
|
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,26 @@ var HealthCheckFailed = class extends (0, unthrown.TaggedError)("HealthCheckFail
|
|
|
33
33
|
*/
|
|
34
34
|
var HealthChecks = class extends _btravstack_di.Port.many("HealthChecks") {};
|
|
35
35
|
/**
|
|
36
|
+
* A check that never settles, bounded. `unref`'d, so a pending probe is never
|
|
37
|
+
* the reason a process stays alive, and cleared on the winning branch so a
|
|
38
|
+
* slow-but-answering check leaves no timer behind.
|
|
39
|
+
*
|
|
40
|
+
* It bounds the WAIT, not the check: nothing here can cancel whatever the check
|
|
41
|
+
* is blocked on. That is the same bargain the drain and the stop deadline make,
|
|
42
|
+
* and it is why this reports rather than recovers.
|
|
43
|
+
*/
|
|
44
|
+
const answeringWithin = (health, ms) => {
|
|
45
|
+
let timer;
|
|
46
|
+
const timeout = new Promise((resolve) => {
|
|
47
|
+
timer = setTimeout(() => resolve((0, unthrown.Err)(new HealthCheckFailed({ reason: `did not answer within ${String(ms)} ms` }))), ms);
|
|
48
|
+
timer.unref();
|
|
49
|
+
});
|
|
50
|
+
const answering = (0, unthrown.OkAsync)().flatMap(() => health.check());
|
|
51
|
+
return (0, unthrown.fromSafePromise)(Promise.race([answering, timeout]).finally(() => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
})).flatMap((settled) => settled);
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
36
56
|
* Runs every check and folds the results into one report — the whole app is
|
|
37
57
|
* unhealthy if any single component is.
|
|
38
58
|
*
|
|
@@ -44,8 +64,15 @@ var HealthChecks = class extends _btravstack_di.Port.many("HealthChecks") {};
|
|
|
44
64
|
* a defecting `AsyncResult` alike are recovered into an unhealthy line —
|
|
45
65
|
* escaped, the first would reach the kernel's own `uncaughtException` handler
|
|
46
66
|
* and the second would leave `/healthz` hanging with nothing written.
|
|
67
|
+
*
|
|
68
|
+
* **A check that never settles is the third case, and it needed a deadline
|
|
69
|
+
* rather than a recovery.** Neither recovery above can see one: there is no
|
|
70
|
+
* error and no defect, only silence, so `/healthz` held a socket open per hit
|
|
71
|
+
* while kubelet timed out against a report that named nothing. Each check now
|
|
72
|
+
* races {@link HealthOptions.timeoutMs}, and the component that hung is named
|
|
73
|
+
* in the report like any other unhealthy one.
|
|
47
74
|
*/
|
|
48
|
-
const runHealthChecks = (checks) => (0, unthrown.allAsync)(checks.map((health) => (
|
|
75
|
+
const runHealthChecks = (checks, options = {}) => (0, unthrown.allAsync)(checks.map((health) => answeringWithin(health, health.timeoutMs ?? options.timeoutMs ?? 800).map(() => ({
|
|
49
76
|
name: health.name,
|
|
50
77
|
status: "healthy"
|
|
51
78
|
})).recoverErrCases((matcher) => matcher.with(unthrown.P.tag("HealthCheckFailed"), (error) => ({
|
|
@@ -104,6 +131,24 @@ const observe = (observers, operation) => {
|
|
|
104
131
|
for (const settle of settlers) settle(settled);
|
|
105
132
|
};
|
|
106
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* {@link observe}, wrapped around one call: every observer is started before
|
|
136
|
+
* `call` runs and settled from whichever channel it comes back on — `ok` on
|
|
137
|
+
* success, `error` carrying the `Err` or the defect as `cause` otherwise. The
|
|
138
|
+
* wrapper is transparent to the `Result`: whatever `call` answers is what the
|
|
139
|
+
* caller receives.
|
|
140
|
+
*
|
|
141
|
+
* `settled` replaces either default where a starter has more to say: `ok`
|
|
142
|
+
* receives the value (a cache reports a hit or a miss), `failure` the
|
|
143
|
+
* `FailureView` (a store reports a missing object as an ordinary answer).
|
|
144
|
+
*/
|
|
145
|
+
const observed = (observers, operation, call, settled = {}) => {
|
|
146
|
+
const settle = observe(observers, operation);
|
|
147
|
+
return call().tap((value) => settle(settled.ok?.(value) ?? { outcome: "ok" })).tapFailure((failure) => settle(settled.failure?.(failure) ?? {
|
|
148
|
+
outcome: "error",
|
|
149
|
+
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
150
|
+
}));
|
|
151
|
+
};
|
|
107
152
|
//#endregion
|
|
108
153
|
//#region src/clock.ts
|
|
109
154
|
const systemClock = {
|
|
@@ -336,7 +381,7 @@ const startProbeServer = (args) => (0, unthrown.fromSafePromise)(new Promise((re
|
|
|
336
381
|
const ignoreServingError = () => {};
|
|
337
382
|
server.once("error", onBindError);
|
|
338
383
|
try {
|
|
339
|
-
server.listen(args.port,
|
|
384
|
+
server.listen(args.port, args.hostname, () => {
|
|
340
385
|
server.removeListener("error", onBindError);
|
|
341
386
|
server.on("error", ignoreServingError);
|
|
342
387
|
const address = server.address();
|
|
@@ -443,6 +488,13 @@ const createUnitRegistry = () => {
|
|
|
443
488
|
};
|
|
444
489
|
//#endregion
|
|
445
490
|
//#region src/start.ts
|
|
491
|
+
/**
|
|
492
|
+
* An arm of a race that has lost and has nothing to report: it never settles,
|
|
493
|
+
* so the winner decides. It holds no timer and no handle, which is what makes
|
|
494
|
+
* it safe — the alternative is an arm that answers a report no consumer reads,
|
|
495
|
+
* and whose side effects then have to be guarded one by one.
|
|
496
|
+
*/
|
|
497
|
+
const withdrawn = () => (0, unthrown.fromSafePromise)(new Promise(() => {}));
|
|
446
498
|
const providesEnv = (module) => module.provides.some((provider) => provider.port.portId === _btravstack_config.Env.portId) || module.imports.some(providesEnv);
|
|
447
499
|
/**
|
|
448
500
|
* What the kernel reads from the environment for itself. Each field is pinned
|
|
@@ -451,13 +503,21 @@ const providesEnv = (module) => module.provides.some((provider) => provider.port
|
|
|
451
503
|
*/
|
|
452
504
|
const KERNEL_DEFAULTS = {
|
|
453
505
|
probePort: 9e3,
|
|
506
|
+
probeHost: "0.0.0.0",
|
|
454
507
|
preDrainDelayMs: 5e3,
|
|
455
|
-
drainTimeoutMs: 2e4
|
|
508
|
+
drainTimeoutMs: 2e4,
|
|
509
|
+
stopTimeoutMs: 5e3
|
|
456
510
|
};
|
|
457
511
|
const readKernelConfig = (options, env) => {
|
|
458
|
-
const
|
|
512
|
+
const probes = options.probes === false ? {
|
|
513
|
+
port: KERNEL_DEFAULTS.probePort,
|
|
514
|
+
host: KERNEL_DEFAULTS.probeHost
|
|
515
|
+
} : options.probes;
|
|
516
|
+
const probePort = probes?.port;
|
|
517
|
+
const probeHost = probes?.host;
|
|
459
518
|
const result = _btravstack_config.Config.object({
|
|
460
519
|
probePort: _btravstack_config.Config.pinned(probePort, _btravstack_config.Config.port("PROBE_PORT", { default: KERNEL_DEFAULTS.probePort })),
|
|
520
|
+
probeHost: _btravstack_config.Config.pinned(probeHost, _btravstack_config.Config.string("PROBE_HOST", { default: KERNEL_DEFAULTS.probeHost })),
|
|
461
521
|
preDrainDelayMs: _btravstack_config.Config.pinned(options.preDrainDelayMs, _btravstack_config.Config.integer("PRE_DRAIN_DELAY_MS", {
|
|
462
522
|
default: KERNEL_DEFAULTS.preDrainDelayMs,
|
|
463
523
|
min: 0
|
|
@@ -465,6 +525,10 @@ const readKernelConfig = (options, env) => {
|
|
|
465
525
|
drainTimeoutMs: _btravstack_config.Config.pinned(options.drainTimeoutMs, _btravstack_config.Config.integer("DRAIN_TIMEOUT_MS", {
|
|
466
526
|
default: KERNEL_DEFAULTS.drainTimeoutMs,
|
|
467
527
|
min: 0
|
|
528
|
+
})),
|
|
529
|
+
stopTimeoutMs: _btravstack_config.Config.pinned(options.stopTimeoutMs, _btravstack_config.Config.integer("STOP_TIMEOUT_MS", {
|
|
530
|
+
default: KERNEL_DEFAULTS.stopTimeoutMs,
|
|
531
|
+
min: 0
|
|
468
532
|
}))
|
|
469
533
|
})["~standard"].validate(env);
|
|
470
534
|
return result.issues === void 0 ? (0, unthrown.Ok)(result.value) : (0, unthrown.Err)(new RuntimeStartFailed({
|
|
@@ -497,8 +561,14 @@ const start = (module, options = {}) => {
|
|
|
497
561
|
const teardownErrors = [];
|
|
498
562
|
const startedAt = clock.now();
|
|
499
563
|
const kernelConfig = readKernelConfig(options, env);
|
|
500
|
-
const { preDrainDelayMs, drainTimeoutMs } = kernelConfig.getOrElse(() => KERNEL_DEFAULTS);
|
|
564
|
+
const { preDrainDelayMs, drainTimeoutMs, stopTimeoutMs } = kernelConfig.getOrElse(() => KERNEL_DEFAULTS);
|
|
501
565
|
const skipDrain = new AbortController();
|
|
566
|
+
const stopSettled = new AbortController();
|
|
567
|
+
let lifecycleSettled = false;
|
|
568
|
+
const onLifecycleSettled = () => {
|
|
569
|
+
lifecycleSettled = true;
|
|
570
|
+
stopSettled.abort();
|
|
571
|
+
};
|
|
502
572
|
let shutdownRequestedAt = startedAt;
|
|
503
573
|
let shutdownRequested = false;
|
|
504
574
|
const sinceShutdownRequested = () => clock.now() - shutdownRequestedAt;
|
|
@@ -517,7 +587,11 @@ const start = (module, options = {}) => {
|
|
|
517
587
|
const ready = () => tracker.current() === "serving" && !forcedUnready;
|
|
518
588
|
const disposeSignals = options.signals === false ? () => {} : installSignalHandlers({
|
|
519
589
|
onFirst: () => requestShutdown("signal"),
|
|
520
|
-
onSecond: () =>
|
|
590
|
+
onSecond: () => {
|
|
591
|
+
skipDrain.abort();
|
|
592
|
+
stopSettled.abort();
|
|
593
|
+
abandonBuild("signal");
|
|
594
|
+
}
|
|
521
595
|
});
|
|
522
596
|
const disposeUncaught = options.signals === false ? () => {} : installUncaughtHandlers((cause) => {
|
|
523
597
|
emit({
|
|
@@ -527,17 +601,63 @@ const start = (module, options = {}) => {
|
|
|
527
601
|
onUnready();
|
|
528
602
|
skipDrain.abort();
|
|
529
603
|
requestShutdown("uncaught");
|
|
604
|
+
abandonBuild("uncaught");
|
|
530
605
|
});
|
|
531
606
|
let disposeProbes = () => {};
|
|
607
|
+
const disposeAll = () => {
|
|
608
|
+
disposeSignals();
|
|
609
|
+
disposeUncaught();
|
|
610
|
+
tracker.advanceTo("exited");
|
|
611
|
+
disposeProbes();
|
|
612
|
+
};
|
|
613
|
+
const reportOf = (reason, drain, abandonedAt) => ({
|
|
614
|
+
reason,
|
|
615
|
+
drain,
|
|
616
|
+
teardownErrors,
|
|
617
|
+
uptimeMs: clock.now() - startedAt,
|
|
618
|
+
...abandonedAt === void 0 ? {} : { abandonedAt }
|
|
619
|
+
});
|
|
532
620
|
let healthChecks = [];
|
|
533
621
|
const health = () => runHealthChecks(healthChecks);
|
|
534
622
|
const probeBound = Promise.withResolvers();
|
|
535
623
|
const runtimePublished = Promise.withResolvers();
|
|
624
|
+
const stopping = Promise.withResolvers();
|
|
625
|
+
const stopAbandoned = () => (0, unthrown.fromSafePromise)(stopping.promise).flatMap(({ reason, drain }) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : (0, unthrown.OkAsync)(abandonStop(reason, drain))));
|
|
626
|
+
const abandonStop = (reason, drain) => {
|
|
627
|
+
emit({
|
|
628
|
+
type: "stoppedWaiting",
|
|
629
|
+
phase: "stop",
|
|
630
|
+
afterMs: stopSettled.signal.aborted ? void 0 : stopTimeoutMs
|
|
631
|
+
});
|
|
632
|
+
disposeAll();
|
|
633
|
+
return reportOf(reason, drain, "stop");
|
|
634
|
+
};
|
|
635
|
+
const abandoningBuild = Promise.withResolvers();
|
|
636
|
+
const buildAbandoned = () => (0, unthrown.fromSafePromise)(abandoningBuild.promise).map((reason) => {
|
|
637
|
+
emit({
|
|
638
|
+
type: "stoppedWaiting",
|
|
639
|
+
phase: "build",
|
|
640
|
+
afterMs: void 0
|
|
641
|
+
});
|
|
642
|
+
registry.abortAll();
|
|
643
|
+
runtimePublished.resolve(void 0);
|
|
644
|
+
tracker.advanceTo("stopping");
|
|
645
|
+
disposeAll();
|
|
646
|
+
return reportOf(reason, void 0, "build");
|
|
647
|
+
});
|
|
648
|
+
const abandonBuild = (reason) => {
|
|
649
|
+
const phase = tracker.current();
|
|
650
|
+
if (phase === "building" || phase === "starting") abandoningBuild.resolve(reason);
|
|
651
|
+
};
|
|
536
652
|
emit({ type: "building" });
|
|
537
|
-
const probesOptions = kernelConfig.map(({ probePort }) => options.probes === false ? false : {
|
|
653
|
+
const probesOptions = kernelConfig.map(({ probePort, probeHost }) => options.probes === false ? false : {
|
|
654
|
+
port: probePort,
|
|
655
|
+
hostname: probeHost
|
|
656
|
+
});
|
|
538
657
|
if (options.probes === false) probeBound.resolve(void 0);
|
|
539
658
|
const probesStarted = probesOptions.toAsync().flatMap((probes) => probes === false ? (0, unthrown.OkAsync)() : startProbeServer({
|
|
540
659
|
port: probes.port,
|
|
660
|
+
hostname: probes.hostname,
|
|
541
661
|
live,
|
|
542
662
|
ready,
|
|
543
663
|
health
|
|
@@ -592,97 +712,98 @@ const start = (module, options = {}) => {
|
|
|
592
712
|
if (reason !== "signal") registry.abortAll();
|
|
593
713
|
return (reason === "signal" ? runDrain(serving) : (0, unthrown.OkAsync)(void 0)).flatMap((report) => {
|
|
594
714
|
tracker.advanceTo("stopping");
|
|
715
|
+
stopping.resolve({
|
|
716
|
+
reason,
|
|
717
|
+
drain: report
|
|
718
|
+
});
|
|
595
719
|
return serving.stop().map(() => {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
tracker.advanceTo("exited");
|
|
599
|
-
disposeProbes();
|
|
600
|
-
return {
|
|
601
|
-
reason,
|
|
602
|
-
drain: report,
|
|
603
|
-
teardownErrors,
|
|
604
|
-
uptimeMs: clock.now() - startedAt
|
|
605
|
-
};
|
|
720
|
+
disposeAll();
|
|
721
|
+
return reportOf(reason, report);
|
|
606
722
|
});
|
|
607
723
|
});
|
|
608
724
|
};
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
return (0, unthrown.fromSafePromise)(ready.promise);
|
|
639
|
-
};
|
|
640
|
-
const outcome = (async () => {
|
|
641
|
-
try {
|
|
642
|
-
return await work({
|
|
643
|
-
ctx: runtimeCtx,
|
|
644
|
-
fork
|
|
645
|
-
}, signal);
|
|
646
|
-
} finally {
|
|
647
|
-
hasSettled = true;
|
|
648
|
-
settled.resolve();
|
|
649
|
-
if (closing !== void 0) await closing;
|
|
650
|
-
}
|
|
651
|
-
})();
|
|
652
|
-
return (0, unthrown.fromSafePromise)(outcome).flatMap((result) => result);
|
|
653
|
-
});
|
|
654
|
-
const host = {
|
|
655
|
-
ctx: runtimeCtx,
|
|
656
|
-
run
|
|
725
|
+
const built = () => _btravstack_di.Module.scoped(root, (ctx) => {
|
|
726
|
+
tracker.advanceTo("starting");
|
|
727
|
+
const runtime = ctx.get(RuntimePort);
|
|
728
|
+
runtimeName = runtime.name;
|
|
729
|
+
healthChecks = ctx.get(HealthChecks);
|
|
730
|
+
const runtimeCtx = ctx;
|
|
731
|
+
const run = (meta, work) => registry.run(meta, (signal) => {
|
|
732
|
+
const settled = Promise.withResolvers();
|
|
733
|
+
let closing;
|
|
734
|
+
let hasSettled = false;
|
|
735
|
+
const fork = (module, seed) => {
|
|
736
|
+
if (hasSettled) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
|
|
737
|
+
if (closing !== void 0) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
|
|
738
|
+
const ready = Promise.withResolvers();
|
|
739
|
+
closing = _btravstack_di.Module.forkScope(ctx, module, (forked) => {
|
|
740
|
+
ready.resolve(forked);
|
|
741
|
+
return (0, unthrown.fromSafePromise)(settled.promise);
|
|
742
|
+
}, {
|
|
743
|
+
seed,
|
|
744
|
+
onTeardownError: (port, cause) => emit({
|
|
745
|
+
type: "teardownError",
|
|
746
|
+
port,
|
|
747
|
+
cause
|
|
748
|
+
})
|
|
749
|
+
}).recoverDefect((cause) => {
|
|
750
|
+
ready.reject(cause);
|
|
751
|
+
return (0, unthrown.Ok)();
|
|
752
|
+
}).get();
|
|
753
|
+
return (0, unthrown.fromSafePromise)(ready.promise);
|
|
657
754
|
};
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
755
|
+
const outcome = (async () => {
|
|
756
|
+
try {
|
|
757
|
+
return await work({
|
|
758
|
+
ctx: runtimeCtx,
|
|
759
|
+
fork
|
|
760
|
+
}, signal);
|
|
761
|
+
} finally {
|
|
762
|
+
hasSettled = true;
|
|
763
|
+
settled.resolve();
|
|
764
|
+
if (closing !== void 0) await closing;
|
|
765
|
+
}
|
|
766
|
+
})();
|
|
767
|
+
return (0, unthrown.fromSafePromise)(outcome).flatMap((result) => result);
|
|
768
|
+
});
|
|
769
|
+
const host = {
|
|
770
|
+
ctx: runtimeCtx,
|
|
771
|
+
run
|
|
772
|
+
};
|
|
773
|
+
return runtime.start(host).flatMap((serving) => {
|
|
774
|
+
servingInfo = serving.info;
|
|
775
|
+
tracker.advanceTo("serving");
|
|
776
|
+
runtimePublished.resolve(serving.info);
|
|
777
|
+
serving.stopped?.().map(() => {
|
|
778
|
+
requestShutdown("runtimeStopped");
|
|
678
779
|
});
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
})
|
|
780
|
+
return (0, unthrown.fromSafePromise)(shutdown.promise).flatMap((reason) => finish(serving, reason));
|
|
781
|
+
});
|
|
782
|
+
}, { onTeardownError: (port, cause) => {
|
|
783
|
+
teardownErrors.push({
|
|
784
|
+
port,
|
|
785
|
+
cause
|
|
786
|
+
});
|
|
787
|
+
emit({
|
|
788
|
+
type: "teardownError",
|
|
789
|
+
port,
|
|
790
|
+
cause
|
|
791
|
+
});
|
|
792
|
+
} }).tapFailure((failure) => {
|
|
793
|
+
if (tracker.current() !== "stopping") emit({
|
|
794
|
+
type: "startFailed",
|
|
795
|
+
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
796
|
+
});
|
|
797
|
+
runtimePublished.resolve(void 0);
|
|
798
|
+
tracker.advanceTo("stopping");
|
|
799
|
+
disposeAll();
|
|
800
|
+
}).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
|
|
801
|
+
return {
|
|
802
|
+
exited: probesStarted.flatMap(() => (0, unthrown.fromSafePromise)(Promise.race([
|
|
803
|
+
built(),
|
|
804
|
+
stopAbandoned(),
|
|
805
|
+
buildAbandoned()
|
|
806
|
+
])).flatMap((settled) => settled)),
|
|
686
807
|
stop: () => requestShutdown("runtimeStopped"),
|
|
687
808
|
requestDrain: () => requestShutdown("signal"),
|
|
688
809
|
phase: tracker.current,
|
|
@@ -698,7 +819,7 @@ const EX_CONFIG = 78;
|
|
|
698
819
|
const isConfig = (error) => error instanceof _btravstack_config.ConfigInvalid || error instanceof RuntimeStartFailed && error.cause instanceof _btravstack_config.ConfigInvalid;
|
|
699
820
|
const codeFor = (report) => {
|
|
700
821
|
if (report.reason === "uncaught") return EX_SOFTWARE;
|
|
701
|
-
return (report.drain?.abandoned ?? 0) > 0 || report.teardownErrors.length > 0 ? 2 : 0;
|
|
822
|
+
return (report.drain?.abandoned ?? 0) > 0 || report.teardownErrors.length > 0 || report.abandonedAt !== void 0 ? 2 : 0;
|
|
702
823
|
};
|
|
703
824
|
/**
|
|
704
825
|
* The exit-code half of `runMain`, on its own so the code table can be asserted
|
|
@@ -731,6 +852,7 @@ const awaitExit = async (app, exit) => {
|
|
|
731
852
|
* | a configuration port that could not be bound (`ConfigInvalid`, `PROBE_PORT` included) | `78` |
|
|
732
853
|
* | drained with work abandoned | `2` |
|
|
733
854
|
* | exited with teardown errors | `2` |
|
|
855
|
+
* | stopped waiting for the teardown or a build (`ExitReport.abandonedAt`) | `2` |
|
|
734
856
|
* | stopped by an uncaught exception or unhandled rejection | `70` |
|
|
735
857
|
* | a defect | `70` |
|
|
736
858
|
*
|
|
@@ -764,6 +886,7 @@ exports.Tracer = Tracer;
|
|
|
764
886
|
exports.currentUnit = currentUnit;
|
|
765
887
|
exports.noObserver = noObserver;
|
|
766
888
|
exports.observe = observe;
|
|
889
|
+
exports.observed = observed;
|
|
767
890
|
exports.releasedBy = releasedBy;
|
|
768
891
|
exports.runHealthChecks = runHealthChecks;
|
|
769
892
|
exports.runMain = runMain;
|