@ultimat3/core 27.6.0 → 27.7.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
@@ -494,7 +494,10 @@ match with `sealAll()`; uniqueness cannot be held across keys.
494
494
  **bound**. `ReadinessCheck` is `() => boolean` and must stay synchronous — a probe that awaits its
495
495
  dependency turns a slow dependency into a wedged endpoint and then a restart loop; keep a boolean
496
496
  fresh and let the check read it. It returns an unregister. `HealthReport.checks` is a map of name
497
- → `'ok' | 'failing'`, so "alert on check failures by check name" is writable.
497
+ → `'ok' | 'degraded' | 'failing'`, so "alert on check failures by check name" is writable.
498
+ `registerReadinessCheck(name, check, { onFailure: 'degraded' })` is a dependency the role serves
499
+ without (the bus of a role that only publishes to it): its `false` is `'degraded'`, never a 503
500
+ on `/readyz` in either mode, and a 503 on `/readyz?deep=1`.
498
501
  - **`HealthReport.registered` is the third state.** `checks: {}` reads identically for "every check
499
502
  passed" and "nobody registered one", and an **empty registry is still ready** — reported, never
500
503
  enforced, so a role with no dependency does not have to invent a check to boot. Read `registered`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "27.6.0",
3
+ "version": "27.7.0",
4
4
  "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,6 +37,6 @@
37
37
  "test": "bun test"
38
38
  },
39
39
  "dependencies": {
40
- "@ultimat3/schema": "27.6.0"
40
+ "@ultimat3/schema": "27.7.0"
41
41
  }
42
42
  }
package/src/index.ts CHANGED
@@ -646,7 +646,11 @@ export {
646
646
  READINESS_GRACE_DEFAULT_MS,
647
647
  READINESS_GRACE_MAX_MS,
648
648
  } from './lifecycle-grace';
649
- export type { ReadinessCheck, ReadinessStatus } from './lifecycle-readiness';
649
+ export type {
650
+ ReadinessCheck,
651
+ ReadinessCheckOptions,
652
+ ReadinessStatus,
653
+ } from './lifecycle-readiness';
650
654
  export type { SignalHandlerOptions } from './lifecycle-signals';
651
655
  export { drainSignals, installSignalHandlers } from './lifecycle-signals';
652
656
  export { isSelfOrigin, listeningOrigins, markListening, resetListeners } from './listeners';
@@ -4,7 +4,22 @@
4
4
 
5
5
  import { UltimateError } from './errors';
6
6
 
7
- export type ReadinessStatus = 'ok' | 'failing';
7
+ /**
8
+ * `'degraded'` is a failed check the role serves WITHOUT (`onFailure: 'degraded'`): reported by
9
+ * name, never a 503 on `/readyz` in either mode, and a 503 on `/readyz?deep=1` — the probe routes
10
+ * traffic on the first and a monitor alerts on the second.
11
+ */
12
+ export type ReadinessStatus = 'ok' | 'degraded' | 'failing';
13
+
14
+ export interface ReadinessCheckOptions {
15
+ /**
16
+ * What a `false` (or a throw) from this check is. `'failing'`, the default, is a dependency the
17
+ * role cannot serve without. `'degraded'` is one it can — the bus of a role that only publishes
18
+ * to it: pulling every replica from the ingress for a dependency no served request waits on is
19
+ * the outage the probe exists to prevent.
20
+ */
21
+ readonly onFailure?: 'failing' | 'degraded' | undefined;
22
+ }
8
23
 
9
24
  /**
10
25
  * Synchronous, and that is the design, not a limitation. **Do not widen this to
@@ -23,13 +38,22 @@ export type ReadinessStatus = 'ok' | 'failing';
23
38
  */
24
39
  export type ReadinessCheck = () => boolean;
25
40
 
26
- const checks = new Map<string, ReadinessCheck>();
41
+ interface Registered {
42
+ readonly check: ReadinessCheck;
43
+ readonly onFailure: 'failing' | 'degraded';
44
+ }
45
+
46
+ const checks = new Map<string, Registered>();
27
47
 
28
48
  /**
29
49
  * Register a named readiness check. Returns its unregister — the same shape as `onShutdown`, and
30
50
  * owned by whoever can be started twice, for the same reason.
31
51
  */
32
- export function registerReadinessCheck(name: string, check: ReadinessCheck): () => void {
52
+ export function registerReadinessCheck(
53
+ name: string,
54
+ check: ReadinessCheck,
55
+ options: ReadinessCheckOptions = {},
56
+ ): () => void {
33
57
  if (checks.has(name)) {
34
58
  throw new UltimateError({
35
59
  code: 'X_READINESS_CHECK_DUPLICATE',
@@ -38,9 +62,10 @@ export function registerReadinessCheck(name: string, check: ReadinessCheck): ()
38
62
  meta: { name },
39
63
  });
40
64
  }
41
- checks.set(name, check);
65
+ const registered: Registered = { check, onFailure: options.onFailure ?? 'failing' };
66
+ checks.set(name, registered);
42
67
  return () => {
43
- if (checks.get(name) === check) checks.delete(name);
68
+ if (checks.get(name) === registered) checks.delete(name);
44
69
  };
45
70
  }
46
71
 
@@ -55,8 +80,8 @@ export function clearReadinessChecks(): void {
55
80
  }
56
81
 
57
82
  /**
58
- * Every check, run now, by name. A check that throws is `failing` — never an unhandled error —
59
- * and is handed to `onThrow`, which `lifecycle.ts` routes through its total `report`.
83
+ * Every check, run now, by name. A check that throws is its `onFailure` (`failing` unless it was
84
+ * registered degradable) — never an unhandled error — and is handed to `onThrow`, which `lifecycle.ts` routes through its total `report`.
60
85
  *
61
86
  * Built through `Object.fromEntries`, never by assigning `results[name]`: assignment to the one
62
87
  * name `__proto__` sets the PROTOTYPE instead of adding a key, so that check vanished from the
@@ -67,11 +92,11 @@ export function runReadinessChecks(
67
92
  onThrow: (name: string, thrown: unknown) => void,
68
93
  ): Readonly<Record<string, ReadinessStatus>> {
69
94
  const results: [string, ReadinessStatus][] = [];
70
- for (const [name, check] of checks) {
95
+ for (const [name, { check, onFailure }] of checks) {
71
96
  try {
72
- results.push([name, check() ? 'ok' : 'failing']);
97
+ results.push([name, check() ? 'ok' : onFailure]);
73
98
  } catch (thrown) {
74
- results.push([name, 'failing']);
99
+ results.push([name, onFailure]);
75
100
  onThrow(name, thrown);
76
101
  }
77
102
  }
package/src/lifecycle.ts CHANGED
@@ -374,9 +374,15 @@ export function drain(signal = 'manual'): Promise<void> {
374
374
  return drainPromise;
375
375
  }
376
376
 
377
- export function healthReport(mode: ReadinessMode = readinessMode): HealthReport {
377
+ export function healthReport(
378
+ mode: ReadinessMode = readinessMode,
379
+ /** `/readyz?deep=1`: a `'degraded'` check is not ready either. Never the probe's reading. */
380
+ strict = false,
381
+ ): HealthReport {
378
382
  const checks = readinessChecks();
379
- const dependencies = Object.values(checks).every((status) => status === 'ok');
383
+ const dependencies = Object.values(checks).every(
384
+ (status) => status === 'ok' || (!strict && status === 'degraded'),
385
+ );
380
386
  return {
381
387
  state,
382
388
  // `ready` is the same predicate `/readyz` answers on, so a body and its status can never
@@ -405,9 +411,11 @@ export function healthzPayload(): HealthPayload {
405
411
  /**
406
412
  * Readiness: may this instance receive traffic? 503 while starting or draining, and — in the
407
413
  * default `'dependencies'` mode, or always with `deep` (`/readyz?deep=1`) — while any check fails.
414
+ * A `'degraded'` check (`ReadinessCheckOptions.onFailure`) is a 503 under `deep` ALONE.
408
415
  */
409
416
  export function readyzPayload(options: { readonly deep?: boolean } = {}): HealthPayload {
410
- const body = healthReport(options.deep === true ? 'dependencies' : readinessMode);
417
+ const deep = options.deep === true;
418
+ const body = healthReport(deep ? 'dependencies' : readinessMode, deep);
411
419
  return { ok: body.ready, status: body.ready ? 200 : 503, body };
412
420
  }
413
421