@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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AsyncResult, Result } from "unthrown";
1
+ import { AsyncResult, FailureView, Result } from "unthrown";
2
2
  import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
3
3
  import { Env, Environment } from "@btravstack/config";
4
4
  //#region src/instrumentation.d.ts
@@ -47,6 +47,16 @@ export declare class HealthCheckFailed extends HealthCheckFailed_base<{
47
47
  type HealthCheck = {
48
48
  readonly name: string;
49
49
  readonly check: () => AsyncResult<void, HealthCheckFailed>;
50
+ /**
51
+ * How long this component gets to answer, overriding the fold's own default.
52
+ *
53
+ * It is on the CONTRIBUTION because only the contributor knows: a `SELECT 1`
54
+ * over a warm pool and an SMTP relay's greeting are two orders of magnitude
55
+ * apart, and one number for both is either a false unhealthy or a probe that
56
+ * outlives the orchestrator's patience. A component with nothing to say about
57
+ * its own latency omits it.
58
+ */
59
+ readonly timeoutMs?: number;
50
60
  };
51
61
  declare const HealthChecks_base: import("@btravstack/di").ManyPortClass<"HealthChecks">;
52
62
  /**
@@ -68,6 +78,10 @@ type HealthReport = {
68
78
  readonly status: "healthy" | "unhealthy";
69
79
  readonly components: readonly ComponentHealth[];
70
80
  };
81
+ type HealthOptions = {
82
+ /** Per check, not for the fold: the checks run concurrently. Default {@link DEFAULT_HEALTH_TIMEOUT_MS}. */
83
+ readonly timeoutMs?: number;
84
+ };
71
85
  /**
72
86
  * Runs every check and folds the results into one report — the whole app is
73
87
  * unhealthy if any single component is.
@@ -80,8 +94,15 @@ type HealthReport = {
80
94
  * a defecting `AsyncResult` alike are recovered into an unhealthy line —
81
95
  * escaped, the first would reach the kernel's own `uncaughtException` handler
82
96
  * and the second would leave `/healthz` hanging with nothing written.
97
+ *
98
+ * **A check that never settles is the third case, and it needed a deadline
99
+ * rather than a recovery.** Neither recovery above can see one: there is no
100
+ * error and no defect, only silence, so `/healthz` held a socket open per hit
101
+ * while kubelet timed out against a report that named nothing. Each check now
102
+ * races {@link HealthOptions.timeoutMs}, and the component that hung is named
103
+ * in the report like any other unhealthy one.
83
104
  */
84
- export declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
105
+ export declare const runHealthChecks: (checks: readonly HealthCheck[], options?: HealthOptions) => AsyncResult<HealthReport, never>;
85
106
  //#endregion
86
107
  //#region src/observability.d.ts
87
108
  /**
@@ -292,6 +313,21 @@ export declare const noObserver: () => Settle;
292
313
  * twice and doubles a failure count.
293
314
  */
294
315
  export declare const observe: (observers: readonly ((operation: Operation) => Settle)[], operation: Operation) => Settle;
316
+ /**
317
+ * {@link observe}, wrapped around one call: every observer is started before
318
+ * `call` runs and settled from whichever channel it comes back on — `ok` on
319
+ * success, `error` carrying the `Err` or the defect as `cause` otherwise. The
320
+ * wrapper is transparent to the `Result`: whatever `call` answers is what the
321
+ * caller receives.
322
+ *
323
+ * `settled` replaces either default where a starter has more to say: `ok`
324
+ * receives the value (a cache reports a hit or a miss), `failure` the
325
+ * `FailureView` (a store reports a missing object as an ordinary answer).
326
+ */
327
+ export declare const observed: <T, E>(observers: readonly ((operation: Operation) => Settle)[], operation: Operation, call: () => AsyncResult<T, E>, settled?: {
328
+ readonly ok?: (value: T) => Settled;
329
+ readonly failure?: (failure: FailureView<E, T>) => Settled;
330
+ }) => AsyncResult<T, E>;
295
331
  //#endregion
296
332
  //#region src/clock.d.ts
297
333
  type Clock = {
@@ -425,6 +461,33 @@ type Serving<Info = never> = {
425
461
  readonly drain: (signal: AbortSignal) => AsyncResult<void, never>;
426
462
  readonly stop: () => AsyncResult<void, never>;
427
463
  readonly info?: Info;
464
+ /**
465
+ * Settles when the runtime has stopped serving **on its own account** —
466
+ * nobody asked it to, and it is not coming back.
467
+ *
468
+ * Without this a runtime has no way to say so, and the kernel's lifecycle
469
+ * only ever moves on a signal or a `stop()` call. So a Temporal worker whose
470
+ * `run()` rejected mid-flight, or an AMQP consumer the server cancelled and
471
+ * nothing re-subscribed, left the process alive and `/readyz` answering
472
+ * `200` — a pod in a Service's endpoints, consuming nothing. The orchestrator
473
+ * has no way to learn that on its own: liveness answers from `building`
474
+ * onward, and readiness is the lifecycle's, not the transport's.
475
+ *
476
+ * The kernel races it against its own shutdown deferred and reports
477
+ * `reason: "runtimeStopped"`, which `runMain` exits `1` for — a restart,
478
+ * which is the right answer to a transport that has given up.
479
+ *
480
+ * **Optional, and a runtime that cannot know omits it.** An HTTP server that
481
+ * is listening is serving; there is no third state for it to report, and a
482
+ * field it had to write `OkAsync()` into would be a promise it could not
483
+ * keep. A `never`-settling `AsyncResult` is the other honest spelling for a
484
+ * runtime that could report this and has nothing to say yet.
485
+ *
486
+ * It says nothing about WHY. The runtime that knows writes that line itself,
487
+ * through `Observers` or its own diagnostics; this channel exists so the
488
+ * process ends, not so the reason travels.
489
+ */
490
+ readonly stopped?: () => AsyncResult<void, never>;
428
491
  };
429
492
  type Runtime<Resolves extends AnyPort = never, Info = never> = {
430
493
  readonly name: string;
@@ -512,7 +575,28 @@ type DrainReport = {
512
575
  * reporting, where `inFlightAtStart - abandoned` can go negative.
513
576
  */
514
577
  readonly completed: number;
515
- /** Units still open at the deadline. The exit-code decision reads this. */
578
+ /**
579
+ * Units still open at the deadline. The exit-code decision reads this.
580
+ *
581
+ * **It means "no longer AWAITED", not "did not finish".** The kernel stops
582
+ * waiting and aborts each unit's signal; it cannot cancel work, and the
583
+ * transport underneath keeps running on its own clock. An abandoned AMQP
584
+ * delivery usually goes on to be acked, and an abandoned Temporal activity
585
+ * goes on to complete — after this report said they did not.
586
+ *
587
+ * **The consequence, stated because nothing else states it: the process may
588
+ * not end by itself.** `runMain` sets an exit code and deliberately never
589
+ * calls `process.exit()`, so the process ends when the event loop empties —
590
+ * and a transport still winding down is holding it open. Nothing here
591
+ * terminates it.
592
+ *
593
+ * Under a Kubernetes-initiated shutdown that ends at
594
+ * `terminationGracePeriodSeconds`, with SIGKILL, so exit `2` is what the
595
+ * report SAYS rather than what the orchestrator observes. Outside that
596
+ * lifecycle there is no SIGKILL: the process simply stays alive until the
597
+ * transport is done. `kubectl logs --previous` carries the report either
598
+ * way, which is what it is for.
599
+ */
516
600
  readonly abandoned: number;
517
601
  };
518
602
  //#endregion
@@ -547,6 +631,18 @@ type KernelEvent = {
547
631
  } | {
548
632
  readonly type: "drained";
549
633
  readonly report: DrainReport;
634
+ } | {
635
+ /**
636
+ * The kernel stopped WAITING for a phase that has no deadline of its own,
637
+ * and reported anyway. Without it a stop that ran long and a stop that
638
+ * finished are the same two lines on stderr (`stopping`, `exited`), which
639
+ * is the silence `ExitReport.abandonedAt` exists to end.
640
+ */
641
+ readonly type: "stoppedWaiting";
642
+ /** `"stop"` for `Serving.stop` and the finalisers, `"build"` for a graph that never served. */
643
+ readonly phase: "build" | "stop";
644
+ /** The deadline that expired, or `undefined` when a second signal cut the wait short. */
645
+ readonly afterMs: number | undefined;
550
646
  } | {
551
647
  readonly type: "stopping";
552
648
  } | {
@@ -575,22 +671,41 @@ type ExitReport = {
575
671
  readonly drain: DrainReport | undefined;
576
672
  readonly teardownErrors: readonly TeardownError[];
577
673
  readonly uptimeMs: number;
674
+ /**
675
+ * Set when the kernel stopped WAITING for a phase that has no deadline of its
676
+ * own — `"stop"` for a `Serving.stop` or a finaliser still running at
677
+ * `stopTimeoutMs`, `"build"` for a graph abandoned before it ever served.
678
+ *
679
+ * It is "stopped waiting", not "cancelled": nothing here can cancel a
680
+ * finaliser, so a wedged one can still hold the event loop past this report
681
+ * and end in SIGKILL. What the field buys is that the report EXISTS and says
682
+ * which phase ran long — where the alternative was a process stuck in
683
+ * `stopping` with no event, no code and no exit report at all.
684
+ */
685
+ readonly abandonedAt?: "build" | "stop";
578
686
  };
579
687
  type StartOptions = {
580
688
  /**
581
689
  * The environment the graph is configured from, provided to it as the `Env`
582
- * port and read for the kernel's own `PROBE_PORT`. Defaults to
583
- * `process.env`.
690
+ * port and read for the kernel's own variables. Defaults to `process.env`.
584
691
  */
585
692
  readonly env?: Environment;
586
693
  readonly clock?: Clock;
587
694
  readonly signals?: boolean;
588
695
  /**
589
- * The probe server's port. Unset, it is bound from `PROBE_PORT` in `env`
590
- * (default `9000`); `false` disables the probe server.
696
+ * The probe server's port and interface. Unset, they are bound from
697
+ * `PROBE_PORT` (default `9000`) and `PROBE_HOST` (default `0.0.0.0`) in
698
+ * `env`; `false` disables the probe server and reads neither.
699
+ *
700
+ * `0.0.0.0` matches `HOST`'s own default for `HOST`'s own reason: a kubelet
701
+ * `httpGet` probe connects over the POD IP, so the loopback-only bind this
702
+ * used to hardcode could not answer the probe shape every manifest reaches
703
+ * for first. Pin `host: "127.0.0.1"` where the port is shared with something
704
+ * else on the host, and probe it with `exec` from inside the container.
591
705
  */
592
706
  readonly probes?: {
593
707
  readonly port: number;
708
+ readonly host?: string;
594
709
  } | false;
595
710
  /**
596
711
  * How long readiness stays false before the runtime is told to stop
@@ -608,6 +723,21 @@ type StartOptions = {
608
723
  * together, in the same manifest.
609
724
  */
610
725
  readonly drainTimeoutMs?: number;
726
+ /**
727
+ * How long the kernel waits for `Serving.stop` and the application scope's
728
+ * finalisers together before it stops waiting and reports
729
+ * `ExitReport.abandonedAt: "stop"`. Unset, it is bound from
730
+ * `STOP_TIMEOUT_MS` in `env` (default `5_000`).
731
+ *
732
+ * It exists because beat 3's deadline covers in-flight WORK and nothing
733
+ * covered the teardown: a `release` that never settles — a socket to a host
734
+ * that stopped answering — left the phase at `stopping` with no exit report,
735
+ * which is the one artefact the lifecycle exists to produce. The three
736
+ * timings are cumulative against the pod's
737
+ * `terminationGracePeriodSeconds`, so the defaults sum to 30 s exactly
738
+ * (`5_000 + 20_000 + 5_000`): raise one and raise the grace period with it.
739
+ */
740
+ readonly stopTimeoutMs?: number;
611
741
  readonly onEvent?: EventSink;
612
742
  };
613
743
  type RunningApp<E, Info = never> = {
@@ -690,6 +820,7 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
690
820
  * | a configuration port that could not be bound (`ConfigInvalid`, `PROBE_PORT` included) | `78` |
691
821
  * | drained with work abandoned | `2` |
692
822
  * | exited with teardown errors | `2` |
823
+ * | stopped waiting for the teardown or a build (`ExitReport.abandonedAt`) | `2` |
693
824
  * | stopped by an uncaught exception or unhandled rejection | `70` |
694
825
  * | a defect | `70` |
695
826
  *
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
2
- import { AsyncResult, Result } from "unthrown";
2
+ import { AsyncResult, FailureView, Result } from "unthrown";
3
3
  import { Env, Environment } from "@btravstack/config";
4
4
  //#region src/instrumentation.d.ts
5
5
  /**
@@ -47,6 +47,16 @@ export declare class HealthCheckFailed extends HealthCheckFailed_base<{
47
47
  type HealthCheck = {
48
48
  readonly name: string;
49
49
  readonly check: () => AsyncResult<void, HealthCheckFailed>;
50
+ /**
51
+ * How long this component gets to answer, overriding the fold's own default.
52
+ *
53
+ * It is on the CONTRIBUTION because only the contributor knows: a `SELECT 1`
54
+ * over a warm pool and an SMTP relay's greeting are two orders of magnitude
55
+ * apart, and one number for both is either a false unhealthy or a probe that
56
+ * outlives the orchestrator's patience. A component with nothing to say about
57
+ * its own latency omits it.
58
+ */
59
+ readonly timeoutMs?: number;
50
60
  };
51
61
  declare const HealthChecks_base: import("@btravstack/di").ManyPortClass<"HealthChecks">;
52
62
  /**
@@ -68,6 +78,10 @@ type HealthReport = {
68
78
  readonly status: "healthy" | "unhealthy";
69
79
  readonly components: readonly ComponentHealth[];
70
80
  };
81
+ type HealthOptions = {
82
+ /** Per check, not for the fold: the checks run concurrently. Default {@link DEFAULT_HEALTH_TIMEOUT_MS}. */
83
+ readonly timeoutMs?: number;
84
+ };
71
85
  /**
72
86
  * Runs every check and folds the results into one report — the whole app is
73
87
  * unhealthy if any single component is.
@@ -80,8 +94,15 @@ type HealthReport = {
80
94
  * a defecting `AsyncResult` alike are recovered into an unhealthy line —
81
95
  * escaped, the first would reach the kernel's own `uncaughtException` handler
82
96
  * and the second would leave `/healthz` hanging with nothing written.
97
+ *
98
+ * **A check that never settles is the third case, and it needed a deadline
99
+ * rather than a recovery.** Neither recovery above can see one: there is no
100
+ * error and no defect, only silence, so `/healthz` held a socket open per hit
101
+ * while kubelet timed out against a report that named nothing. Each check now
102
+ * races {@link HealthOptions.timeoutMs}, and the component that hung is named
103
+ * in the report like any other unhealthy one.
83
104
  */
84
- export declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
105
+ export declare const runHealthChecks: (checks: readonly HealthCheck[], options?: HealthOptions) => AsyncResult<HealthReport, never>;
85
106
  //#endregion
86
107
  //#region src/observability.d.ts
87
108
  /**
@@ -292,6 +313,21 @@ export declare const noObserver: () => Settle;
292
313
  * twice and doubles a failure count.
293
314
  */
294
315
  export declare const observe: (observers: readonly ((operation: Operation) => Settle)[], operation: Operation) => Settle;
316
+ /**
317
+ * {@link observe}, wrapped around one call: every observer is started before
318
+ * `call` runs and settled from whichever channel it comes back on — `ok` on
319
+ * success, `error` carrying the `Err` or the defect as `cause` otherwise. The
320
+ * wrapper is transparent to the `Result`: whatever `call` answers is what the
321
+ * caller receives.
322
+ *
323
+ * `settled` replaces either default where a starter has more to say: `ok`
324
+ * receives the value (a cache reports a hit or a miss), `failure` the
325
+ * `FailureView` (a store reports a missing object as an ordinary answer).
326
+ */
327
+ export declare const observed: <T, E>(observers: readonly ((operation: Operation) => Settle)[], operation: Operation, call: () => AsyncResult<T, E>, settled?: {
328
+ readonly ok?: (value: T) => Settled;
329
+ readonly failure?: (failure: FailureView<E, T>) => Settled;
330
+ }) => AsyncResult<T, E>;
295
331
  //#endregion
296
332
  //#region src/clock.d.ts
297
333
  type Clock = {
@@ -425,6 +461,33 @@ type Serving<Info = never> = {
425
461
  readonly drain: (signal: AbortSignal) => AsyncResult<void, never>;
426
462
  readonly stop: () => AsyncResult<void, never>;
427
463
  readonly info?: Info;
464
+ /**
465
+ * Settles when the runtime has stopped serving **on its own account** —
466
+ * nobody asked it to, and it is not coming back.
467
+ *
468
+ * Without this a runtime has no way to say so, and the kernel's lifecycle
469
+ * only ever moves on a signal or a `stop()` call. So a Temporal worker whose
470
+ * `run()` rejected mid-flight, or an AMQP consumer the server cancelled and
471
+ * nothing re-subscribed, left the process alive and `/readyz` answering
472
+ * `200` — a pod in a Service's endpoints, consuming nothing. The orchestrator
473
+ * has no way to learn that on its own: liveness answers from `building`
474
+ * onward, and readiness is the lifecycle's, not the transport's.
475
+ *
476
+ * The kernel races it against its own shutdown deferred and reports
477
+ * `reason: "runtimeStopped"`, which `runMain` exits `1` for — a restart,
478
+ * which is the right answer to a transport that has given up.
479
+ *
480
+ * **Optional, and a runtime that cannot know omits it.** An HTTP server that
481
+ * is listening is serving; there is no third state for it to report, and a
482
+ * field it had to write `OkAsync()` into would be a promise it could not
483
+ * keep. A `never`-settling `AsyncResult` is the other honest spelling for a
484
+ * runtime that could report this and has nothing to say yet.
485
+ *
486
+ * It says nothing about WHY. The runtime that knows writes that line itself,
487
+ * through `Observers` or its own diagnostics; this channel exists so the
488
+ * process ends, not so the reason travels.
489
+ */
490
+ readonly stopped?: () => AsyncResult<void, never>;
428
491
  };
429
492
  type Runtime<Resolves extends AnyPort = never, Info = never> = {
430
493
  readonly name: string;
@@ -512,7 +575,28 @@ type DrainReport = {
512
575
  * reporting, where `inFlightAtStart - abandoned` can go negative.
513
576
  */
514
577
  readonly completed: number;
515
- /** Units still open at the deadline. The exit-code decision reads this. */
578
+ /**
579
+ * Units still open at the deadline. The exit-code decision reads this.
580
+ *
581
+ * **It means "no longer AWAITED", not "did not finish".** The kernel stops
582
+ * waiting and aborts each unit's signal; it cannot cancel work, and the
583
+ * transport underneath keeps running on its own clock. An abandoned AMQP
584
+ * delivery usually goes on to be acked, and an abandoned Temporal activity
585
+ * goes on to complete — after this report said they did not.
586
+ *
587
+ * **The consequence, stated because nothing else states it: the process may
588
+ * not end by itself.** `runMain` sets an exit code and deliberately never
589
+ * calls `process.exit()`, so the process ends when the event loop empties —
590
+ * and a transport still winding down is holding it open. Nothing here
591
+ * terminates it.
592
+ *
593
+ * Under a Kubernetes-initiated shutdown that ends at
594
+ * `terminationGracePeriodSeconds`, with SIGKILL, so exit `2` is what the
595
+ * report SAYS rather than what the orchestrator observes. Outside that
596
+ * lifecycle there is no SIGKILL: the process simply stays alive until the
597
+ * transport is done. `kubectl logs --previous` carries the report either
598
+ * way, which is what it is for.
599
+ */
516
600
  readonly abandoned: number;
517
601
  };
518
602
  //#endregion
@@ -547,6 +631,18 @@ type KernelEvent = {
547
631
  } | {
548
632
  readonly type: "drained";
549
633
  readonly report: DrainReport;
634
+ } | {
635
+ /**
636
+ * The kernel stopped WAITING for a phase that has no deadline of its own,
637
+ * and reported anyway. Without it a stop that ran long and a stop that
638
+ * finished are the same two lines on stderr (`stopping`, `exited`), which
639
+ * is the silence `ExitReport.abandonedAt` exists to end.
640
+ */
641
+ readonly type: "stoppedWaiting";
642
+ /** `"stop"` for `Serving.stop` and the finalisers, `"build"` for a graph that never served. */
643
+ readonly phase: "build" | "stop";
644
+ /** The deadline that expired, or `undefined` when a second signal cut the wait short. */
645
+ readonly afterMs: number | undefined;
550
646
  } | {
551
647
  readonly type: "stopping";
552
648
  } | {
@@ -575,22 +671,41 @@ type ExitReport = {
575
671
  readonly drain: DrainReport | undefined;
576
672
  readonly teardownErrors: readonly TeardownError[];
577
673
  readonly uptimeMs: number;
674
+ /**
675
+ * Set when the kernel stopped WAITING for a phase that has no deadline of its
676
+ * own — `"stop"` for a `Serving.stop` or a finaliser still running at
677
+ * `stopTimeoutMs`, `"build"` for a graph abandoned before it ever served.
678
+ *
679
+ * It is "stopped waiting", not "cancelled": nothing here can cancel a
680
+ * finaliser, so a wedged one can still hold the event loop past this report
681
+ * and end in SIGKILL. What the field buys is that the report EXISTS and says
682
+ * which phase ran long — where the alternative was a process stuck in
683
+ * `stopping` with no event, no code and no exit report at all.
684
+ */
685
+ readonly abandonedAt?: "build" | "stop";
578
686
  };
579
687
  type StartOptions = {
580
688
  /**
581
689
  * The environment the graph is configured from, provided to it as the `Env`
582
- * port and read for the kernel's own `PROBE_PORT`. Defaults to
583
- * `process.env`.
690
+ * port and read for the kernel's own variables. Defaults to `process.env`.
584
691
  */
585
692
  readonly env?: Environment;
586
693
  readonly clock?: Clock;
587
694
  readonly signals?: boolean;
588
695
  /**
589
- * The probe server's port. Unset, it is bound from `PROBE_PORT` in `env`
590
- * (default `9000`); `false` disables the probe server.
696
+ * The probe server's port and interface. Unset, they are bound from
697
+ * `PROBE_PORT` (default `9000`) and `PROBE_HOST` (default `0.0.0.0`) in
698
+ * `env`; `false` disables the probe server and reads neither.
699
+ *
700
+ * `0.0.0.0` matches `HOST`'s own default for `HOST`'s own reason: a kubelet
701
+ * `httpGet` probe connects over the POD IP, so the loopback-only bind this
702
+ * used to hardcode could not answer the probe shape every manifest reaches
703
+ * for first. Pin `host: "127.0.0.1"` where the port is shared with something
704
+ * else on the host, and probe it with `exec` from inside the container.
591
705
  */
592
706
  readonly probes?: {
593
707
  readonly port: number;
708
+ readonly host?: string;
594
709
  } | false;
595
710
  /**
596
711
  * How long readiness stays false before the runtime is told to stop
@@ -608,6 +723,21 @@ type StartOptions = {
608
723
  * together, in the same manifest.
609
724
  */
610
725
  readonly drainTimeoutMs?: number;
726
+ /**
727
+ * How long the kernel waits for `Serving.stop` and the application scope's
728
+ * finalisers together before it stops waiting and reports
729
+ * `ExitReport.abandonedAt: "stop"`. Unset, it is bound from
730
+ * `STOP_TIMEOUT_MS` in `env` (default `5_000`).
731
+ *
732
+ * It exists because beat 3's deadline covers in-flight WORK and nothing
733
+ * covered the teardown: a `release` that never settles — a socket to a host
734
+ * that stopped answering — left the phase at `stopping` with no exit report,
735
+ * which is the one artefact the lifecycle exists to produce. The three
736
+ * timings are cumulative against the pod's
737
+ * `terminationGracePeriodSeconds`, so the defaults sum to 30 s exactly
738
+ * (`5_000 + 20_000 + 5_000`): raise one and raise the grace period with it.
739
+ */
740
+ readonly stopTimeoutMs?: number;
611
741
  readonly onEvent?: EventSink;
612
742
  };
613
743
  type RunningApp<E, Info = never> = {
@@ -690,6 +820,7 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
690
820
  * | a configuration port that could not be bound (`ConfigInvalid`, `PROBE_PORT` included) | `78` |
691
821
  * | drained with work abandoned | `2` |
692
822
  * | exited with teardown errors | `2` |
823
+ * | stopped waiting for the teardown or a build (`ExitReport.abandonedAt`) | `2` |
693
824
  * | stopped by an uncaught exception or unhandled rejection | `70` |
694
825
  * | a defect | `70` |
695
826
  *