@btravstack/core 0.4.0 → 0.6.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
@@ -16,7 +16,7 @@ pnpm add @btravstack/core @btravstack/config @btravstack/di unthrown
16
16
 
17
17
  `@btravstack/config`, `@btravstack/di` and `unthrown` are **peer
18
18
  dependencies** — install all four. The kernel itself depends on `node:`
19
- builtins only. Node `>=20`.
19
+ builtins only. Node `>=22`.
20
20
 
21
21
  ## A worked example
22
22
 
package/dist/index.cjs CHANGED
@@ -1,9 +1,59 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- let unthrown = require("unthrown");
3
2
  let _btravstack_di = require("@btravstack/di");
3
+ let unthrown = require("unthrown");
4
4
  let _btravstack_config = require("@btravstack/config");
5
5
  let node_http = require("node:http");
6
6
  let node_async_hooks = require("node:async_hooks");
7
+ //#region src/instrumentation.ts
8
+ /**
9
+ * The set port a package contributes its instrumentation to.
10
+ *
11
+ * Nothing collects it unless an OTel SDK is composed, which is the point: a
12
+ * starter DECLARES what it can instrument, and composing
13
+ * `@btravstack/observability/otel` is what turns the declarations on. A graph
14
+ * without it registers nothing and pays nothing — the Spring Boot starter
15
+ * shape, in one port.
16
+ */
17
+ var Instrumentations = class extends _btravstack_di.Port.many("Instrumentations") {};
18
+ //#endregion
19
+ //#region src/health.ts
20
+ /**
21
+ * A component could not answer for itself. Modeled rather than thrown, because
22
+ * a health check that throws is a bug in the check; one that fails is the news
23
+ * `/healthz` exists to carry.
24
+ */
25
+ var HealthCheckFailed = class extends (0, unthrown.TaggedError)("HealthCheckFailed") {};
26
+ /**
27
+ * The set port every starter contributes to and the kernel reads whole.
28
+ *
29
+ * A set port rather than a registry the kernel hands out: a starter DECLARES
30
+ * its check the same way it declares anything else, and a starter an
31
+ * application never composed contributes nothing — no registration call to
32
+ * forget, and no order to get wrong.
33
+ */
34
+ var HealthChecks = class extends _btravstack_di.Port.many("HealthChecks") {};
35
+ /**
36
+ * Runs every check and folds the results into one report — the whole app is
37
+ * unhealthy if any single component is.
38
+ *
39
+ * Each check's failure is recovered into a component line BEFORE `allAsync`
40
+ * sees it, so a failing dependency cannot short-circuit the others: a report
41
+ * naming one component is worth less than one naming all of them. What
42
+ * survives to `allAsync`'s error channel is a `Defect` alone — a check that
43
+ * threw rather than answered, which is a bug in the check.
44
+ */
45
+ const runHealthChecks = (checks) => (0, unthrown.allAsync)(checks.map((health) => health.check().map(() => ({
46
+ name: health.name,
47
+ status: "healthy"
48
+ })).recoverErrCases((matcher) => matcher.with(unthrown.P.tag("HealthCheckFailed"), (error) => ({
49
+ name: health.name,
50
+ status: "unhealthy",
51
+ reason: error.reason
52
+ }))))).map((components) => ({
53
+ status: components.every((c) => c.status === "healthy") ? "healthy" : "unhealthy",
54
+ components
55
+ }));
56
+ //#endregion
7
57
  //#region src/clock.ts
8
58
  const systemClock = {
9
59
  now: () => Date.now(),
@@ -128,17 +178,6 @@ const whenAborted = (signal) => signal.aborted ? (0, unthrown.OkAsync)() : (0, u
128
178
  signal.addEventListener("abort", () => resolve(), { once: true });
129
179
  }));
130
180
  //#endregion
131
- //#region src/deferred.ts
132
- const createDeferred = () => {
133
- let resolve;
134
- return {
135
- promise: new Promise((r) => {
136
- resolve = r;
137
- }),
138
- resolve
139
- };
140
- };
141
- //#endregion
142
181
  //#region src/drain.ts
143
182
  const drainApp = (args) => {
144
183
  args.onUnready();
@@ -196,6 +235,12 @@ const startProbeServer = (args) => (0, unthrown.fromSafePromise)(new Promise((re
196
235
  respond(response, args.ready(), "ready");
197
236
  return;
198
237
  }
238
+ if (request.url === "/healthz") {
239
+ args.health().map((report) => {
240
+ response.writeHead(report.status === "healthy" ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify(report));
241
+ });
242
+ return;
243
+ }
199
244
  response.writeHead(404).end();
200
245
  });
201
246
  const onBindError = (cause) => {
@@ -334,7 +379,7 @@ const start = (module, options = {}) => {
334
379
  if (phase === "exited") emit({ type: "exited" });
335
380
  });
336
381
  const registry = createUnitRegistry();
337
- const shutdown = createDeferred();
382
+ const shutdown = Promise.withResolvers();
338
383
  const teardownErrors = [];
339
384
  const startedAt = clock.now();
340
385
  const preDrainDelayMs = options.preDrainDelayMs ?? 5e3;
@@ -370,8 +415,10 @@ const start = (module, options = {}) => {
370
415
  requestShutdown("uncaught");
371
416
  });
372
417
  let disposeProbes = () => {};
373
- const probeBound = createDeferred();
374
- const runtimePublished = createDeferred();
418
+ let healthChecks = [];
419
+ const health = () => runHealthChecks(healthChecks);
420
+ const probeBound = Promise.withResolvers();
421
+ const runtimePublished = Promise.withResolvers();
375
422
  emit({ type: "building" });
376
423
  const probesOptions = options.probes === void 0 ? _btravstack_config.Config.port("PROBE_PORT", { default: 9e3 }).parse(env["PROBE_PORT"]).map((port) => ({ port })).mapErrCases((matcher) => matcher.with(unthrown.P.tag("ConfigFieldInvalid"), ({ reason }) => new RuntimeStartFailed({
377
424
  runtime: "probes",
@@ -387,7 +434,8 @@ const start = (module, options = {}) => {
387
434
  const probesStarted = probesOptions.toAsync().flatMap((probes) => probes === false ? (0, unthrown.OkAsync)() : startProbeServer({
388
435
  port: probes.port,
389
436
  live,
390
- ready
437
+ ready,
438
+ health
391
439
  }).tap((server) => {
392
440
  probeBoundPort = server.port;
393
441
  probeBound.resolve(server.port);
@@ -455,6 +503,7 @@ const start = (module, options = {}) => {
455
503
  tracker.advanceTo("starting");
456
504
  const runtime = ctx.get(RuntimePort);
457
505
  runtimeName = runtime.name;
506
+ healthChecks = ctx.get(HealthChecks);
458
507
  const runtimeCtx = ctx;
459
508
  const unit = options.unit;
460
509
  const run = (meta, work) => registry.run(meta, (signal) => {
@@ -565,6 +614,9 @@ const runMain = async (module, options = {}, exit = (code) => {
565
614
  await awaitExit(start(module, options), exit);
566
615
  };
567
616
  //#endregion
617
+ exports.HealthCheckFailed = HealthCheckFailed;
618
+ exports.HealthChecks = HealthChecks;
619
+ exports.Instrumentations = Instrumentations;
568
620
  exports.LEVELS = LEVELS;
569
621
  exports.Logger = Logger;
570
622
  exports.Meter = Meter;
@@ -574,6 +626,7 @@ exports.SPAN_STATUS = SPAN_STATUS;
574
626
  exports.Tracer = Tracer;
575
627
  exports.currentUnit = currentUnit;
576
628
  exports.releasedBy = releasedBy;
629
+ exports.runHealthChecks = runHealthChecks;
577
630
  exports.runMain = runMain;
578
631
  exports.start = start;
579
632
  exports.stderrSink = stderrSink;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,85 @@
1
1
  import { AsyncResult, Result } from "unthrown";
2
2
  import { AnyPort, Context, Module, PortClass, Scope, ServiceOf } from "@btravstack/di";
3
3
  import { Env, Environment } from "@btravstack/config";
4
+ //#region src/instrumentation.d.ts
5
+ /**
6
+ * One package's offer of an OpenTelemetry instrumentation.
7
+ *
8
+ * **Async, and answers `undefined` rather than failing, because the package
9
+ * supplying the instrumentation is an OPTIONAL peer.** A starter cannot know at
10
+ * composition time whether the consumer installed it, so it contributes the
11
+ * ATTEMPT and the collector drops what did not load — which is what lets
12
+ * `@btravstack/prisma` declare engine tracing without making every consumer
13
+ * install `@prisma/instrumentation`. A contributor that wants to say why its
14
+ * load answered nothing logs that itself; it is the one that knows.
15
+ *
16
+ * The instrumentation is `unknown` on purpose. Naming OpenTelemetry's
17
+ * `Instrumentation` would put the vendor in the package every other one peers
18
+ * on — the same reason `Tracer` and `Meter` are declared as narrowings rather
19
+ * than as OTel's own types. The collector
20
+ * (`@btravstack/observability/otel`) is where the vendor is already a
21
+ * dependency, so that is where the cast belongs.
22
+ */
23
+ type InstrumentationLoader = () => Promise<unknown>;
24
+ declare const Instrumentations_base: import("@btravstack/di").ManyPortClass<"Instrumentations">;
25
+ /**
26
+ * The set port a package contributes its instrumentation to.
27
+ *
28
+ * Nothing collects it unless an OTel SDK is composed, which is the point: a
29
+ * starter DECLARES what it can instrument, and composing
30
+ * `@btravstack/observability/otel` is what turns the declarations on. A graph
31
+ * without it registers nothing and pays nothing — the Spring Boot starter
32
+ * shape, in one port.
33
+ */
34
+ declare class Instrumentations extends Instrumentations_base<InstrumentationLoader> {}
35
+ //#endregion
36
+ //#region src/health.d.ts
37
+ declare const HealthCheckFailed_base: import("unthrown").TaggedErrorConstructor<"HealthCheckFailed">;
38
+ /**
39
+ * A component could not answer for itself. Modeled rather than thrown, because
40
+ * a health check that throws is a bug in the check; one that fails is the news
41
+ * `/healthz` exists to carry.
42
+ */
43
+ declare class HealthCheckFailed extends HealthCheckFailed_base<{
44
+ readonly reason: string;
45
+ }> {}
46
+ /** One contribution: what to call the component, and how to ask it. */
47
+ type HealthCheck = {
48
+ readonly name: string;
49
+ readonly check: () => AsyncResult<void, HealthCheckFailed>;
50
+ };
51
+ declare const HealthChecks_base: import("@btravstack/di").ManyPortClass<"HealthChecks">;
52
+ /**
53
+ * The set port every starter contributes to and the kernel reads whole.
54
+ *
55
+ * A set port rather than a registry the kernel hands out: a starter DECLARES
56
+ * its check the same way it declares anything else, and a starter an
57
+ * application never composed contributes nothing — no registration call to
58
+ * forget, and no order to get wrong.
59
+ */
60
+ declare class HealthChecks extends HealthChecks_base<HealthCheck> {}
61
+ type ComponentHealth = {
62
+ readonly name: string;
63
+ readonly status: "healthy" | "unhealthy";
64
+ /** Present only when the component is unhealthy. */
65
+ readonly reason?: string;
66
+ };
67
+ type HealthReport = {
68
+ readonly status: "healthy" | "unhealthy";
69
+ readonly components: readonly ComponentHealth[];
70
+ };
71
+ /**
72
+ * Runs every check and folds the results into one report — the whole app is
73
+ * unhealthy if any single component is.
74
+ *
75
+ * Each check's failure is recovered into a component line BEFORE `allAsync`
76
+ * sees it, so a failing dependency cannot short-circuit the others: a report
77
+ * naming one component is worth less than one naming all of them. What
78
+ * survives to `allAsync`'s error channel is a `Defect` alone — a check that
79
+ * threw rather than answered, which is a bug in the check.
80
+ */
81
+ declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
82
+ //#endregion
4
83
  //#region src/clock.d.ts
5
84
  type Clock = {
6
85
  readonly now: () => number;
@@ -455,4 +534,4 @@ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X,
455
534
  */
456
535
  declare const runMain: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X, E, Scope | Env> & StartGate<X, UnitNeeds>, options?: StartOptions<UnitX, UnitNeeds>, exit?: (code: number) => void) => Promise<void>;
457
536
  //#endregion
458
- export { type Attributes, type Clock, type Counter, type DrainReport, type EventSink, type ExitReport, type Histogram, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, releasedBy, runMain, start, stderrSink, systemClock };
537
+ export { type Attributes, type Clock, type ComponentHealth, type Counter, type DrainReport, type EventSink, type ExitReport, type HealthCheck, HealthCheckFailed, HealthChecks, type HealthReport, type Histogram, type InstrumentationLoader, Instrumentations, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,85 @@
1
- import { AsyncResult, Result } from "unthrown";
2
1
  import { AnyPort, Context, Module, PortClass, Scope, ServiceOf } from "@btravstack/di";
2
+ import { AsyncResult, Result } from "unthrown";
3
3
  import { Env, Environment } from "@btravstack/config";
4
+ //#region src/instrumentation.d.ts
5
+ /**
6
+ * One package's offer of an OpenTelemetry instrumentation.
7
+ *
8
+ * **Async, and answers `undefined` rather than failing, because the package
9
+ * supplying the instrumentation is an OPTIONAL peer.** A starter cannot know at
10
+ * composition time whether the consumer installed it, so it contributes the
11
+ * ATTEMPT and the collector drops what did not load — which is what lets
12
+ * `@btravstack/prisma` declare engine tracing without making every consumer
13
+ * install `@prisma/instrumentation`. A contributor that wants to say why its
14
+ * load answered nothing logs that itself; it is the one that knows.
15
+ *
16
+ * The instrumentation is `unknown` on purpose. Naming OpenTelemetry's
17
+ * `Instrumentation` would put the vendor in the package every other one peers
18
+ * on — the same reason `Tracer` and `Meter` are declared as narrowings rather
19
+ * than as OTel's own types. The collector
20
+ * (`@btravstack/observability/otel`) is where the vendor is already a
21
+ * dependency, so that is where the cast belongs.
22
+ */
23
+ type InstrumentationLoader = () => Promise<unknown>;
24
+ declare const Instrumentations_base: import("@btravstack/di").ManyPortClass<"Instrumentations">;
25
+ /**
26
+ * The set port a package contributes its instrumentation to.
27
+ *
28
+ * Nothing collects it unless an OTel SDK is composed, which is the point: a
29
+ * starter DECLARES what it can instrument, and composing
30
+ * `@btravstack/observability/otel` is what turns the declarations on. A graph
31
+ * without it registers nothing and pays nothing — the Spring Boot starter
32
+ * shape, in one port.
33
+ */
34
+ declare class Instrumentations extends Instrumentations_base<InstrumentationLoader> {}
35
+ //#endregion
36
+ //#region src/health.d.ts
37
+ declare const HealthCheckFailed_base: import("unthrown").TaggedErrorConstructor<"HealthCheckFailed">;
38
+ /**
39
+ * A component could not answer for itself. Modeled rather than thrown, because
40
+ * a health check that throws is a bug in the check; one that fails is the news
41
+ * `/healthz` exists to carry.
42
+ */
43
+ declare class HealthCheckFailed extends HealthCheckFailed_base<{
44
+ readonly reason: string;
45
+ }> {}
46
+ /** One contribution: what to call the component, and how to ask it. */
47
+ type HealthCheck = {
48
+ readonly name: string;
49
+ readonly check: () => AsyncResult<void, HealthCheckFailed>;
50
+ };
51
+ declare const HealthChecks_base: import("@btravstack/di").ManyPortClass<"HealthChecks">;
52
+ /**
53
+ * The set port every starter contributes to and the kernel reads whole.
54
+ *
55
+ * A set port rather than a registry the kernel hands out: a starter DECLARES
56
+ * its check the same way it declares anything else, and a starter an
57
+ * application never composed contributes nothing — no registration call to
58
+ * forget, and no order to get wrong.
59
+ */
60
+ declare class HealthChecks extends HealthChecks_base<HealthCheck> {}
61
+ type ComponentHealth = {
62
+ readonly name: string;
63
+ readonly status: "healthy" | "unhealthy";
64
+ /** Present only when the component is unhealthy. */
65
+ readonly reason?: string;
66
+ };
67
+ type HealthReport = {
68
+ readonly status: "healthy" | "unhealthy";
69
+ readonly components: readonly ComponentHealth[];
70
+ };
71
+ /**
72
+ * Runs every check and folds the results into one report — the whole app is
73
+ * unhealthy if any single component is.
74
+ *
75
+ * Each check's failure is recovered into a component line BEFORE `allAsync`
76
+ * sees it, so a failing dependency cannot short-circuit the others: a report
77
+ * naming one component is worth less than one naming all of them. What
78
+ * survives to `allAsync`'s error channel is a `Defect` alone — a check that
79
+ * threw rather than answered, which is a bug in the check.
80
+ */
81
+ declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
82
+ //#endregion
4
83
  //#region src/clock.d.ts
5
84
  type Clock = {
6
85
  readonly now: () => number;
@@ -455,4 +534,4 @@ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X,
455
534
  */
456
535
  declare const runMain: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X, E, Scope | Env> & StartGate<X, UnitNeeds>, options?: StartOptions<UnitX, UnitNeeds>, exit?: (code: number) => void) => Promise<void>;
457
536
  //#endregion
458
- export { type Attributes, type Clock, type Counter, type DrainReport, type EventSink, type ExitReport, type Histogram, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, releasedBy, runMain, start, stderrSink, systemClock };
537
+ export { type Attributes, type Clock, type ComponentHealth, type Counter, type DrainReport, type EventSink, type ExitReport, type HealthCheck, HealthCheckFailed, HealthChecks, type HealthReport, type Histogram, type InstrumentationLoader, Instrumentations, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
package/dist/index.mjs CHANGED
@@ -1,8 +1,58 @@
1
- import { Err, Ok, OkAsync, P, TaggedError, fromSafePromise } from "unthrown";
2
1
  import { Module, Port, Provider } from "@btravstack/di";
2
+ import { Err, Ok, OkAsync, P, TaggedError, allAsync, fromSafePromise } from "unthrown";
3
3
  import { Config, ConfigInvalid, Env } from "@btravstack/config";
4
4
  import { createServer } from "node:http";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
+ //#region src/instrumentation.ts
7
+ /**
8
+ * The set port a package contributes its instrumentation to.
9
+ *
10
+ * Nothing collects it unless an OTel SDK is composed, which is the point: a
11
+ * starter DECLARES what it can instrument, and composing
12
+ * `@btravstack/observability/otel` is what turns the declarations on. A graph
13
+ * without it registers nothing and pays nothing — the Spring Boot starter
14
+ * shape, in one port.
15
+ */
16
+ var Instrumentations = class extends Port.many("Instrumentations") {};
17
+ //#endregion
18
+ //#region src/health.ts
19
+ /**
20
+ * A component could not answer for itself. Modeled rather than thrown, because
21
+ * a health check that throws is a bug in the check; one that fails is the news
22
+ * `/healthz` exists to carry.
23
+ */
24
+ var HealthCheckFailed = class extends TaggedError("HealthCheckFailed") {};
25
+ /**
26
+ * The set port every starter contributes to and the kernel reads whole.
27
+ *
28
+ * A set port rather than a registry the kernel hands out: a starter DECLARES
29
+ * its check the same way it declares anything else, and a starter an
30
+ * application never composed contributes nothing — no registration call to
31
+ * forget, and no order to get wrong.
32
+ */
33
+ var HealthChecks = class extends Port.many("HealthChecks") {};
34
+ /**
35
+ * Runs every check and folds the results into one report — the whole app is
36
+ * unhealthy if any single component is.
37
+ *
38
+ * Each check's failure is recovered into a component line BEFORE `allAsync`
39
+ * sees it, so a failing dependency cannot short-circuit the others: a report
40
+ * naming one component is worth less than one naming all of them. What
41
+ * survives to `allAsync`'s error channel is a `Defect` alone — a check that
42
+ * threw rather than answered, which is a bug in the check.
43
+ */
44
+ const runHealthChecks = (checks) => allAsync(checks.map((health) => health.check().map(() => ({
45
+ name: health.name,
46
+ status: "healthy"
47
+ })).recoverErrCases((matcher) => matcher.with(P.tag("HealthCheckFailed"), (error) => ({
48
+ name: health.name,
49
+ status: "unhealthy",
50
+ reason: error.reason
51
+ }))))).map((components) => ({
52
+ status: components.every((c) => c.status === "healthy") ? "healthy" : "unhealthy",
53
+ components
54
+ }));
55
+ //#endregion
6
56
  //#region src/clock.ts
7
57
  const systemClock = {
8
58
  now: () => Date.now(),
@@ -127,17 +177,6 @@ const whenAborted = (signal) => signal.aborted ? OkAsync() : fromSafePromise(new
127
177
  signal.addEventListener("abort", () => resolve(), { once: true });
128
178
  }));
129
179
  //#endregion
130
- //#region src/deferred.ts
131
- const createDeferred = () => {
132
- let resolve;
133
- return {
134
- promise: new Promise((r) => {
135
- resolve = r;
136
- }),
137
- resolve
138
- };
139
- };
140
- //#endregion
141
180
  //#region src/drain.ts
142
181
  const drainApp = (args) => {
143
182
  args.onUnready();
@@ -195,6 +234,12 @@ const startProbeServer = (args) => fromSafePromise(new Promise((resolve) => {
195
234
  respond(response, args.ready(), "ready");
196
235
  return;
197
236
  }
237
+ if (request.url === "/healthz") {
238
+ args.health().map((report) => {
239
+ response.writeHead(report.status === "healthy" ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify(report));
240
+ });
241
+ return;
242
+ }
198
243
  response.writeHead(404).end();
199
244
  });
200
245
  const onBindError = (cause) => {
@@ -333,7 +378,7 @@ const start = (module, options = {}) => {
333
378
  if (phase === "exited") emit({ type: "exited" });
334
379
  });
335
380
  const registry = createUnitRegistry();
336
- const shutdown = createDeferred();
381
+ const shutdown = Promise.withResolvers();
337
382
  const teardownErrors = [];
338
383
  const startedAt = clock.now();
339
384
  const preDrainDelayMs = options.preDrainDelayMs ?? 5e3;
@@ -369,8 +414,10 @@ const start = (module, options = {}) => {
369
414
  requestShutdown("uncaught");
370
415
  });
371
416
  let disposeProbes = () => {};
372
- const probeBound = createDeferred();
373
- const runtimePublished = createDeferred();
417
+ let healthChecks = [];
418
+ const health = () => runHealthChecks(healthChecks);
419
+ const probeBound = Promise.withResolvers();
420
+ const runtimePublished = Promise.withResolvers();
374
421
  emit({ type: "building" });
375
422
  const probesOptions = options.probes === void 0 ? Config.port("PROBE_PORT", { default: 9e3 }).parse(env["PROBE_PORT"]).map((port) => ({ port })).mapErrCases((matcher) => matcher.with(P.tag("ConfigFieldInvalid"), ({ reason }) => new RuntimeStartFailed({
376
423
  runtime: "probes",
@@ -386,7 +433,8 @@ const start = (module, options = {}) => {
386
433
  const probesStarted = probesOptions.toAsync().flatMap((probes) => probes === false ? OkAsync() : startProbeServer({
387
434
  port: probes.port,
388
435
  live,
389
- ready
436
+ ready,
437
+ health
390
438
  }).tap((server) => {
391
439
  probeBoundPort = server.port;
392
440
  probeBound.resolve(server.port);
@@ -454,6 +502,7 @@ const start = (module, options = {}) => {
454
502
  tracker.advanceTo("starting");
455
503
  const runtime = ctx.get(RuntimePort);
456
504
  runtimeName = runtime.name;
505
+ healthChecks = ctx.get(HealthChecks);
457
506
  const runtimeCtx = ctx;
458
507
  const unit = options.unit;
459
508
  const run = (meta, work) => registry.run(meta, (signal) => {
@@ -564,4 +613,4 @@ const runMain = async (module, options = {}, exit = (code) => {
564
613
  await awaitExit(start(module, options), exit);
565
614
  };
566
615
  //#endregion
567
- export { LEVELS, Logger, Meter, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, releasedBy, runMain, start, stderrSink, systemClock };
616
+ export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/core",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",
@@ -53,16 +53,16 @@
53
53
  "typescript": "7.0.2",
54
54
  "unthrown": "5.7.0",
55
55
  "vitest": "4.1.11",
56
- "@btravstack/config": "0.4.0",
57
- "@btravstack/di": "0.4.0"
56
+ "@btravstack/config": "0.6.0",
57
+ "@btravstack/di": "0.6.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "unthrown": "^5.0.0",
61
- "@btravstack/config": "^0.4.0",
62
- "@btravstack/di": "^0.4.0"
61
+ "@btravstack/config": "^0.6.0",
62
+ "@btravstack/di": "^0.6.0"
63
63
  },
64
64
  "engines": {
65
- "node": ">=20"
65
+ "node": ">=22"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsdown src/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.build.json",