@btravstack/core 0.14.0 → 0.16.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 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`. A second signal skips the drain. Both timings have to agree with
139
- the pod's `terminationGracePeriodSeconds`, which is why the deployment can
140
- set them and the option only pins them.
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`), up before the graph is built.
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) => (0, unthrown.OkAsync)().flatMap(() => health.check()).map(() => ({
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, "127.0.0.1", () => {
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 probePort = options.probes === void 0 ? void 0 : options.probes === false ? KERNEL_DEFAULTS.probePort : options.probes.port;
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: () => skipDrain.abort()
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 : { port: probePort });
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
- disposeSignals();
597
- disposeUncaught();
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
- return {
610
- exited: probesStarted.flatMap(() => _btravstack_di.Module.scoped(root, (ctx) => {
611
- tracker.advanceTo("starting");
612
- const runtime = ctx.get(RuntimePort);
613
- runtimeName = runtime.name;
614
- healthChecks = ctx.get(HealthChecks);
615
- const runtimeCtx = ctx;
616
- const run = (meta, work) => registry.run(meta, (signal) => {
617
- const settled = Promise.withResolvers();
618
- let closing;
619
- let hasSettled = false;
620
- const fork = (module, seed) => {
621
- if (hasSettled) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
622
- if (closing !== void 0) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
623
- const ready = Promise.withResolvers();
624
- closing = _btravstack_di.Module.forkScope(ctx, module, (forked) => {
625
- ready.resolve(forked);
626
- return (0, unthrown.fromSafePromise)(settled.promise);
627
- }, {
628
- seed,
629
- onTeardownError: (port, cause) => emit({
630
- type: "teardownError",
631
- port,
632
- cause
633
- })
634
- }).recoverDefect((cause) => {
635
- ready.reject(cause);
636
- return (0, unthrown.Ok)();
637
- }).get();
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
- return runtime.start(host).flatMap((serving) => {
659
- servingInfo = serving.info;
660
- tracker.advanceTo("serving");
661
- runtimePublished.resolve(serving.info);
662
- return (0, unthrown.fromSafePromise)(shutdown.promise).flatMap((reason) => finish(serving, reason));
663
- });
664
- }, { onTeardownError: (port, cause) => {
665
- teardownErrors.push({
666
- port,
667
- cause
668
- });
669
- emit({
670
- type: "teardownError",
671
- port,
672
- cause
673
- });
674
- } }).tapFailure((failure) => {
675
- if (tracker.current() !== "stopping") emit({
676
- type: "startFailed",
677
- cause: failure.tag === "Err" ? failure.error : failure.cause
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
- runtimePublished.resolve(void 0);
680
- tracker.advanceTo("stopping");
681
- disposeSignals();
682
- disposeUncaught();
683
- tracker.advanceTo("exited");
684
- disposeProbes();
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;