@ultimat3/core 27.6.1 → 27.8.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`
@@ -728,7 +731,7 @@ nothing consulted it before deciding to try again. `As of 2026-08-23`.
728
731
  | Export | The one answer | The question it settles |
729
732
  |---|---|---|
730
733
  | `backoffDelay({ attempt, base, max, factor?, curve?, jitter?, random? })` | one curve — `exponential \| linear \| fixed`, `full \| equal \| none` — 1-based `attempt`, clamped to `max` **before** jitter, rounded, and `0` rather than `NaN` | how long to wait. `random` is injectable, so a schedule is a unit test rather than a range |
731
- | `singleFlight({ deadlineMs?, schedule? })` → `run(key, work, join?)`, `size` | N callers on one key are ONE run | who pays for a miss. Eviction is identity-checked, so a load that settles late never drops the load that replaced it; `deadlineMs` frees the KEY a wedged load would hold forever — it never cancels the work and never rejects a joiner |
734
+ | `singleFlight({ deadlineMs?, schedule? })` → `run(key, work, join?)`, `size`, `keys()`, `evict(key)` | N callers on one key are ONE run | who pays for a miss. Eviction is identity-checked, so a load that settles late never drops the load that replaced it; `deadlineMs` frees the KEY a wedged load would hold forever — it never cancels the work and never rejects a joiner. `evict(key)` does the same on demand, for a caller that learns the load in flight began before a write its result must reflect |
732
735
  | `flightGate({ maxConcurrent, maxQueued }, { subject?, overflow? })` | one bound, one queue, one refusal | how many at once. Past the queue the answer is `X_FLIGHT_GATE_OVERLOADED` (503) and never a longer queue; the slot is HANDED to a waiter, never released and re-acquired. Both limits are screened at construction (`finiteCount`, 0 allowed); a width of 0 refuses every caller rather than queueing for a slot that never frees. `run(work, signal?)`: an abort while QUEUED removes the waiter and rejects with the abort's reason |
733
736
  | `generationFence(subject)` → `generation()`, `bump()`, `guard(issued)` | whether an answer still applies | `X_SUPERSEDED` (499) and `isSuperseded(error)` — the piece nothing in the tree had. `guard` compares `!==`, never `<` |
734
737
  | `isRetryableStatus(status)`, `RETRYABLE_STATUSES` | `>= 500`, plus 408, 409, 425, 429 | which HTTP answers are worth repeating |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "27.6.1",
3
+ "version": "27.8.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.1"
40
+ "@ultimat3/schema": "27.8.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';
@@ -675,6 +679,9 @@ export {
675
679
  resetMeasurementActor,
676
680
  } from './measurement-actor';
677
681
  export { nearestName } from './nearest-name';
682
+ // An outage said on failures 1, 2, 4, 8, … and once when it ends.
683
+ export type { OutageLog, OutageLogOptions } from './outage-log';
684
+ export { isOutageMilestone, outageLog } from './outage-log';
678
685
  /** The message pwa's `sw.js` posts and realtime's outbox listens for. */
679
686
  export { OUTBOX_DRAIN_MESSAGE, type OutboxDrainMessage } from './outbox-drain';
680
687
  /** The `<meta name>`s render writes and the page client, realtime and pwa read. */
@@ -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
 
@@ -0,0 +1,53 @@
1
+ // Single responsibility: one condition that KEEPS failing, said rarely — on failures 1, 2, 4, 8, …
2
+ // and once more when it ends. An outage is one fact; a line per retry per pod for as long as it
3
+ // lasts is the fact buried under itself.
4
+
5
+ import { type LogFields, logger } from './logger';
6
+
7
+ export interface OutageLogOptions {
8
+ /** The line's message while the condition fails. Low cardinality: it is what a monitor groups on. */
9
+ readonly event: string;
10
+ /**
11
+ * `'warn'` for a process that is degraded and still serving (a publisher without its bus).
12
+ * `'error'` for one that cannot do its job (a `sync` node without its bus).
13
+ */
14
+ readonly level: 'warn' | 'error';
15
+ }
16
+
17
+ export interface OutageLog {
18
+ /**
19
+ * One more failure. Written — with `failures`, the count since it last worked — on the 1st,
20
+ * 2nd, 4th, 8th, …; answers whether this one was, so a caller thins a second sink the same way.
21
+ */
22
+ failed(fields?: LogFields): boolean;
23
+ /** It works again: ONE `info` line, `<event> recovered`, with `after`. Silent when nothing had failed. */
24
+ recovered(fields?: LogFields): void;
25
+ /** Failures since it last worked. */
26
+ readonly failures: number;
27
+ }
28
+
29
+ /** True on 1, 2, 4, 8, …: the attempts an outage is said on. */
30
+ export function isOutageMilestone(failures: number): boolean {
31
+ return failures > 0 && (failures & (failures - 1)) === 0;
32
+ }
33
+
34
+ export function outageLog(options: OutageLogOptions): OutageLog {
35
+ let failures = 0;
36
+ return {
37
+ get failures(): number {
38
+ return failures;
39
+ },
40
+ failed(fields?: LogFields): boolean {
41
+ failures += 1;
42
+ if (!isOutageMilestone(failures)) return false;
43
+ logger[options.level](options.event, { ...fields, failures });
44
+ return true;
45
+ },
46
+ recovered(fields?: LogFields): void {
47
+ if (failures === 0) return;
48
+ const after = failures;
49
+ failures = 0;
50
+ logger.info(`${options.event} recovered`, { ...fields, after });
51
+ },
52
+ };
53
+ }
@@ -40,6 +40,15 @@ export interface SingleFlight {
40
40
  ): Promise<T>;
41
41
  /** In-flight loads right now. A number that does not fall back to `0` is a leak. */
42
42
  readonly size: number;
43
+ /** The keys held by a load right now — what a caller that must `evict` by a rule walks. */
44
+ keys(): readonly string[];
45
+ /**
46
+ * Free `key` NOW, so the next `run` leads a fresh load instead of joining the one in flight.
47
+ * For a caller that learns the load in flight is already wrong — it began before a write its
48
+ * result must reflect. The load is not cancelled and its own callers still get its value; only
49
+ * later ones stop joining it. `false` when nothing held the key.
50
+ */
51
+ evict(key: string): boolean;
43
52
  }
44
53
 
45
54
  /** The leader's promise, the box its merged context lives in, and its deadline's canceller. */
@@ -73,6 +82,15 @@ export function singleFlight(options?: SingleFlightOptions): SingleFlight {
73
82
  return inflight.size;
74
83
  },
75
84
 
85
+ keys: () => [...inflight.keys()],
86
+
87
+ evict(key: string): boolean {
88
+ const held = inflight.get(key);
89
+ if (held === undefined) return false;
90
+ held.cancelDeadline();
91
+ return inflight.delete(key);
92
+ },
93
+
76
94
  run<T, C = undefined>(
77
95
  key: string,
78
96
  work: (shared: () => C | undefined) => Promise<T>,