@btravstack/core 0.45.0 → 0.46.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 +6 -0
- package/dist/index.cjs +50 -4
- package/dist/index.d.cts +12 -3
- package/dist/index.d.mts +12 -3
- package/dist/index.mjs +50 -4
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -166,6 +166,12 @@ ports hidden. Application starters use it alongside their own runtime exports.
|
|
|
166
166
|
- **Probes from the state machine.** `GET /livez` and `GET /readyz` on
|
|
167
167
|
`PROBE_PORT` (default `9000`) and `PROBE_HOST` (default `0.0.0.0`, so a
|
|
168
168
|
kubelet `httpGet` probe reaches them), up before the graph is built.
|
|
169
|
+
A `HealthCheck` can opt into gating `/readyz` with `readiness: true`
|
|
170
|
+
(1000 ms cache) or `readiness: { intervalMs: 5000 }`. Concurrent probes and
|
|
171
|
+
`/healthz` share that check's cache; ordinary checks only affect `/healthz`.
|
|
172
|
+
Failed checks can recover without a restart; drain and crash readiness
|
|
173
|
+
remain latched. `RunningApp.ready()` reads only the lifecycle.
|
|
174
|
+
See [probes](https://btravstack.github.io/btravstack/reference/core/probes).
|
|
169
175
|
- **Configuration from the environment, typed.** The kernel provides
|
|
170
176
|
[`@btravstack/config`](../config)'s `Env` port to every graph it boots; a
|
|
171
177
|
bad value is a `ConfigInvalid` naming every fault at once, and exit `78`.
|
package/dist/index.cjs
CHANGED
|
@@ -332,6 +332,46 @@ const drainApp = (args) => {
|
|
|
332
332
|
});
|
|
333
333
|
};
|
|
334
334
|
//#endregion
|
|
335
|
+
//#region src/health-monitor.ts
|
|
336
|
+
const cachedCheck = (check, clock) => {
|
|
337
|
+
const interval = typeof check.readiness === "object" ? check.readiness.intervalMs : 1e3;
|
|
338
|
+
let cached;
|
|
339
|
+
let expiresAt = 0;
|
|
340
|
+
let pending;
|
|
341
|
+
return () => {
|
|
342
|
+
if (!Number.isFinite(interval) || interval <= 0) return (0, unthrown.OkAsync)({
|
|
343
|
+
status: "unhealthy",
|
|
344
|
+
components: [{
|
|
345
|
+
name: check.name,
|
|
346
|
+
status: "unhealthy",
|
|
347
|
+
reason: "readiness interval must be a positive finite number"
|
|
348
|
+
}]
|
|
349
|
+
});
|
|
350
|
+
if (cached !== void 0 && clock.now() < expiresAt) return (0, unthrown.OkAsync)(cached);
|
|
351
|
+
pending ??= runHealthChecks([check]).map((report) => {
|
|
352
|
+
cached = report;
|
|
353
|
+
expiresAt = clock.now() + interval;
|
|
354
|
+
pending = void 0;
|
|
355
|
+
return report;
|
|
356
|
+
});
|
|
357
|
+
return pending;
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
/** Per-application checks; only readiness contributors retain a cached report. */
|
|
361
|
+
const healthMonitor = (checks, clock) => {
|
|
362
|
+
const entries = checks.map((check) => ({
|
|
363
|
+
readiness: Boolean(check.readiness),
|
|
364
|
+
run: check.readiness ? cachedCheck(check, clock) : () => runHealthChecks([check])
|
|
365
|
+
}));
|
|
366
|
+
return {
|
|
367
|
+
health: () => (0, unthrown.allAsync)(entries.map((entry) => entry.run())).map((reports) => ({
|
|
368
|
+
status: reports.every((report) => report.status === "healthy") ? "healthy" : "unhealthy",
|
|
369
|
+
components: reports.flatMap((report) => report.components)
|
|
370
|
+
})),
|
|
371
|
+
readiness: () => (0, unthrown.allAsync)(entries.filter((entry) => entry.readiness).map((entry) => entry.run())).map((reports) => reports.every((report) => report.status === "healthy"))
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
//#endregion
|
|
335
375
|
//#region src/phase.ts
|
|
336
376
|
const ORDER = [
|
|
337
377
|
"building",
|
|
@@ -363,7 +403,11 @@ const startProbeServer = (args) => (0, unthrown.fromSafePromise)(new Promise((re
|
|
|
363
403
|
return;
|
|
364
404
|
}
|
|
365
405
|
if (request.url === "/readyz") {
|
|
366
|
-
|
|
406
|
+
if (!args.ready() || args.readiness === void 0) {
|
|
407
|
+
respond(response, args.ready(), "ready");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
args.readiness().map((healthy) => respond(response, args.ready() && healthy, "ready"));
|
|
367
411
|
return;
|
|
368
412
|
}
|
|
369
413
|
if (request.url === "/healthz") {
|
|
@@ -716,8 +760,9 @@ const start = (module, options = {}) => {
|
|
|
716
760
|
uptimeMs: clock.now() - startedAt,
|
|
717
761
|
...abandonedAt === void 0 ? {} : { abandonedAt }
|
|
718
762
|
});
|
|
719
|
-
let
|
|
720
|
-
const health = () =>
|
|
763
|
+
let monitor = healthMonitor([], clock);
|
|
764
|
+
const health = () => monitor.health();
|
|
765
|
+
const readiness = () => monitor.readiness();
|
|
721
766
|
const probeBound = Promise.withResolvers();
|
|
722
767
|
const runtimePublished = Promise.withResolvers();
|
|
723
768
|
const leaveStartup = () => {
|
|
@@ -774,6 +819,7 @@ const start = (module, options = {}) => {
|
|
|
774
819
|
hostname: probes.hostname,
|
|
775
820
|
live,
|
|
776
821
|
ready,
|
|
822
|
+
readiness,
|
|
777
823
|
health
|
|
778
824
|
}).tap((server) => {
|
|
779
825
|
probeBoundPort = server.port;
|
|
@@ -840,7 +886,7 @@ const start = (module, options = {}) => {
|
|
|
840
886
|
tracker.advanceTo("starting");
|
|
841
887
|
const runtime = ctx.get(RuntimePort);
|
|
842
888
|
runtimeName = runtime.name;
|
|
843
|
-
|
|
889
|
+
monitor = healthMonitor(ctx.get(HealthChecks), clock);
|
|
844
890
|
const runtimeCtx = ctx;
|
|
845
891
|
const rebuilt = rebuildGuard(runtime.name, root);
|
|
846
892
|
for (const unit of Object.values(runtime.units ?? {})) {
|
package/dist/index.d.cts
CHANGED
|
@@ -58,6 +58,15 @@ type HealthCheck = {
|
|
|
58
58
|
* its own latency omits it.
|
|
59
59
|
*/
|
|
60
60
|
readonly timeoutMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Opt into gating `/readyz`. `true` caches the result for 1000 ms;
|
|
63
|
+
* an object selects a positive, finite interval. Concurrent probes share
|
|
64
|
+
* one evaluation, and `/healthz` uses the same cache for this check.
|
|
65
|
+
* Omitted or `false` leaves readiness independent of this component.
|
|
66
|
+
*/
|
|
67
|
+
readonly readiness?: boolean | {
|
|
68
|
+
readonly intervalMs: number;
|
|
69
|
+
};
|
|
61
70
|
};
|
|
62
71
|
declare const HealthChecks_base: import("@btravstack/di").ManyPortClass<"HealthChecks">;
|
|
63
72
|
/**
|
|
@@ -864,9 +873,9 @@ type RunningApp<E, Info = never> = {
|
|
|
864
873
|
readonly requestDrain: () => void;
|
|
865
874
|
readonly phase: () => Phase;
|
|
866
875
|
/**
|
|
867
|
-
* The
|
|
868
|
-
* a drain or an uncaught exception
|
|
869
|
-
*
|
|
876
|
+
* The synchronous lifecycle predicate: serving, and not forced unready by
|
|
877
|
+
* a drain or an uncaught exception. `/readyz` additionally consults opted-in
|
|
878
|
+
* dependency checks; this method does not evaluate them.
|
|
870
879
|
*/
|
|
871
880
|
readonly ready: () => boolean;
|
|
872
881
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -58,6 +58,15 @@ type HealthCheck = {
|
|
|
58
58
|
* its own latency omits it.
|
|
59
59
|
*/
|
|
60
60
|
readonly timeoutMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Opt into gating `/readyz`. `true` caches the result for 1000 ms;
|
|
63
|
+
* an object selects a positive, finite interval. Concurrent probes share
|
|
64
|
+
* one evaluation, and `/healthz` uses the same cache for this check.
|
|
65
|
+
* Omitted or `false` leaves readiness independent of this component.
|
|
66
|
+
*/
|
|
67
|
+
readonly readiness?: boolean | {
|
|
68
|
+
readonly intervalMs: number;
|
|
69
|
+
};
|
|
61
70
|
};
|
|
62
71
|
declare const HealthChecks_base: import("@btravstack/di").ManyPortClass<"HealthChecks">;
|
|
63
72
|
/**
|
|
@@ -864,9 +873,9 @@ type RunningApp<E, Info = never> = {
|
|
|
864
873
|
readonly requestDrain: () => void;
|
|
865
874
|
readonly phase: () => Phase;
|
|
866
875
|
/**
|
|
867
|
-
* The
|
|
868
|
-
* a drain or an uncaught exception
|
|
869
|
-
*
|
|
876
|
+
* The synchronous lifecycle predicate: serving, and not forced unready by
|
|
877
|
+
* a drain or an uncaught exception. `/readyz` additionally consults opted-in
|
|
878
|
+
* dependency checks; this method does not evaluate them.
|
|
870
879
|
*/
|
|
871
880
|
readonly ready: () => boolean;
|
|
872
881
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -331,6 +331,46 @@ const drainApp = (args) => {
|
|
|
331
331
|
});
|
|
332
332
|
};
|
|
333
333
|
//#endregion
|
|
334
|
+
//#region src/health-monitor.ts
|
|
335
|
+
const cachedCheck = (check, clock) => {
|
|
336
|
+
const interval = typeof check.readiness === "object" ? check.readiness.intervalMs : 1e3;
|
|
337
|
+
let cached;
|
|
338
|
+
let expiresAt = 0;
|
|
339
|
+
let pending;
|
|
340
|
+
return () => {
|
|
341
|
+
if (!Number.isFinite(interval) || interval <= 0) return OkAsync({
|
|
342
|
+
status: "unhealthy",
|
|
343
|
+
components: [{
|
|
344
|
+
name: check.name,
|
|
345
|
+
status: "unhealthy",
|
|
346
|
+
reason: "readiness interval must be a positive finite number"
|
|
347
|
+
}]
|
|
348
|
+
});
|
|
349
|
+
if (cached !== void 0 && clock.now() < expiresAt) return OkAsync(cached);
|
|
350
|
+
pending ??= runHealthChecks([check]).map((report) => {
|
|
351
|
+
cached = report;
|
|
352
|
+
expiresAt = clock.now() + interval;
|
|
353
|
+
pending = void 0;
|
|
354
|
+
return report;
|
|
355
|
+
});
|
|
356
|
+
return pending;
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
/** Per-application checks; only readiness contributors retain a cached report. */
|
|
360
|
+
const healthMonitor = (checks, clock) => {
|
|
361
|
+
const entries = checks.map((check) => ({
|
|
362
|
+
readiness: Boolean(check.readiness),
|
|
363
|
+
run: check.readiness ? cachedCheck(check, clock) : () => runHealthChecks([check])
|
|
364
|
+
}));
|
|
365
|
+
return {
|
|
366
|
+
health: () => allAsync(entries.map((entry) => entry.run())).map((reports) => ({
|
|
367
|
+
status: reports.every((report) => report.status === "healthy") ? "healthy" : "unhealthy",
|
|
368
|
+
components: reports.flatMap((report) => report.components)
|
|
369
|
+
})),
|
|
370
|
+
readiness: () => allAsync(entries.filter((entry) => entry.readiness).map((entry) => entry.run())).map((reports) => reports.every((report) => report.status === "healthy"))
|
|
371
|
+
};
|
|
372
|
+
};
|
|
373
|
+
//#endregion
|
|
334
374
|
//#region src/phase.ts
|
|
335
375
|
const ORDER = [
|
|
336
376
|
"building",
|
|
@@ -362,7 +402,11 @@ const startProbeServer = (args) => fromSafePromise(new Promise((resolve) => {
|
|
|
362
402
|
return;
|
|
363
403
|
}
|
|
364
404
|
if (request.url === "/readyz") {
|
|
365
|
-
|
|
405
|
+
if (!args.ready() || args.readiness === void 0) {
|
|
406
|
+
respond(response, args.ready(), "ready");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
args.readiness().map((healthy) => respond(response, args.ready() && healthy, "ready"));
|
|
366
410
|
return;
|
|
367
411
|
}
|
|
368
412
|
if (request.url === "/healthz") {
|
|
@@ -715,8 +759,9 @@ const start = (module, options = {}) => {
|
|
|
715
759
|
uptimeMs: clock.now() - startedAt,
|
|
716
760
|
...abandonedAt === void 0 ? {} : { abandonedAt }
|
|
717
761
|
});
|
|
718
|
-
let
|
|
719
|
-
const health = () =>
|
|
762
|
+
let monitor = healthMonitor([], clock);
|
|
763
|
+
const health = () => monitor.health();
|
|
764
|
+
const readiness = () => monitor.readiness();
|
|
720
765
|
const probeBound = Promise.withResolvers();
|
|
721
766
|
const runtimePublished = Promise.withResolvers();
|
|
722
767
|
const leaveStartup = () => {
|
|
@@ -773,6 +818,7 @@ const start = (module, options = {}) => {
|
|
|
773
818
|
hostname: probes.hostname,
|
|
774
819
|
live,
|
|
775
820
|
ready,
|
|
821
|
+
readiness,
|
|
776
822
|
health
|
|
777
823
|
}).tap((server) => {
|
|
778
824
|
probeBoundPort = server.port;
|
|
@@ -839,7 +885,7 @@ const start = (module, options = {}) => {
|
|
|
839
885
|
tracker.advanceTo("starting");
|
|
840
886
|
const runtime = ctx.get(RuntimePort);
|
|
841
887
|
runtimeName = runtime.name;
|
|
842
|
-
|
|
888
|
+
monitor = healthMonitor(ctx.get(HealthChecks), clock);
|
|
843
889
|
const runtimeCtx = ctx;
|
|
844
890
|
const rebuilt = rebuildGuard(runtime.name, root);
|
|
845
891
|
for (const unit of Object.values(runtime.units ?? {})) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "The application kernel: boot a di module into a running process, and stop it without losing work",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"application",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"./package.json": "./package.json"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@btravstack/config": "0.
|
|
49
|
-
"@btravstack/di": "0.
|
|
48
|
+
"@btravstack/config": "0.46.0",
|
|
49
|
+
"@btravstack/di": "0.46.0",
|
|
50
50
|
"@btravstack/tsconfig": "0.4.0",
|
|
51
51
|
"@stryker-mutator/core": "10.0.0",
|
|
52
52
|
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"vitest": "5.0.3"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"@btravstack/config": "^0.
|
|
64
|
-
"@btravstack/di": "^0.
|
|
63
|
+
"@btravstack/config": "^0.46.0",
|
|
64
|
+
"@btravstack/di": "^0.46.0",
|
|
65
65
|
"unthrown": "^5.0.0"
|
|
66
66
|
},
|
|
67
67
|
"engines": {
|