@studnicky/health-registry
Named async health-check registry with worst-status-wins aggregation.
Install
pnpm add @studnicky/health-registryUsage
Register named async check functions, each resolving to { status, metadata? }. evaluate() runs every registered check in parallel via Promise.allSettled; a configured timeoutMs races that check against a local timer with Promise.race. A rejecting or timed-out check is folded into the results as 'unhealthy' instead of crashing the evaluation of the other checks:
import type { HealthStatusEntity } from '../src/entities/index.js';
import type { HealthCheckResultInterface } from '../src/interfaces/index.js';
import { HealthRegistry } from '../src/index.js';
class TelemetryHealthRegistry extends HealthRegistry {
readonly registeredChecks: string[] = [];
readonly checkResults: { 'name': string; 'status': HealthStatusEntity.Type }[] = [];
readonly timeouts: { 'name': string; 'timeoutMs': number }[] = [];
protected override onCheckRegistered(name: string): void {
console.log(`[health] registered '${name}'`);
this.registeredChecks.push(name);
}
protected override onCheckResult(name: string, result: HealthCheckResultInterface): void {
console.log(`[health] '${name}' -> ${result.status}${result.metadata !== undefined ? ` (${JSON.stringify(result.metadata)})` : ''}`);
this.checkResults.push({ 'name': name, 'status': result.status });
}
protected override onCheckTimeout(name: string, timeoutMs: number): void {
console.log(`[health] '${name}' exceeded its ${timeoutMs}ms timeout`);
this.timeouts.push({ 'name': name, 'timeoutMs': timeoutMs });
}
protected override onAggregate(overall: HealthStatusEntity.Type, results: ReadonlyMap<string, HealthCheckResultInterface>): void {
console.log(`[health] overall: ${overall} (${String(results.size)} checks)`);
}
}
const registry = TelemetryHealthRegistry.create();
registry.register('database', async () => { await Promise.resolve(); return { 'status': 'healthy' }; });
registry.register('cache', async () => {
await Promise.resolve();
return {
'metadata': { 'hitRate': 0.42 },
'status': 'degraded'
};
});
registry.register('downstream-api', async () => {
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
return { 'status': 'healthy' };
}, { 'timeoutMs': 20 });
const evaluation = await registry.evaluate();
console.log('Overall status:', evaluation.status);
console.log('Per-check results:', evaluation.results);Try it
The output shows onCheckResult reporting database as healthy, cache as degraded with its metadata, and downstream-api timing out via onCheckTimeout, then onAggregate folding all three into an overall 'unhealthy' status.
Aggregation
The overall status is worst-status-wins: any 'unhealthy' check makes the overall status 'unhealthy', else any 'degraded' check makes it 'degraded', else 'healthy'. An empty registry evaluates to 'healthy' with an empty results map.
| Method | Description |
|---|---|
HealthRegistry.create() | Creates an empty registry |
register(name, check, options?) | Registers (or replaces) a named async check. options.timeoutMs bounds how long the check may run before it counts as 'unhealthy' |
unregister(name) | Removes a named check; no-op if it was never registered |
has(name) | Whether a check is currently registered under name |
list() | The names of every currently registered check |
evaluate() | Runs every registered check in parallel and returns { status, results } |
hookErrorCount | Count of hook failures recorded since construction |
getHookErrors() | Defensive copy of every hook failure recorded since construction |
Hooks
| Hook | Fires |
|---|---|
onCheckRegistered(name) | After a check is registered (or replaces an existing registration under the same name) |
onCheckResult(name, status, metadata?) | Once per check as it settles during evaluate() — success, rejection, or timeout |
onCheckTimeout(name, timeoutMs) | When a check exceeds its configured timeoutMs, in addition to onCheckResult |
onAggregate(overall, results) | Once per evaluate() call, after every registered check has settled |
A hook override that throws or rejects does not abort evaluate() — the failure is recorded instead of propagating; inspect it via hookErrorCount (a running total) and getHookErrors() (a defensive copy of every recorded { hookName, cause } entry), backed internally by @studnicky/errors's HookInvoker.
Scope
HealthRegistry owns only the registry-and-aggregate logic — the same boundary MachineRegistry draws for actors. It performs no HTTP endpoint wiring and makes no Kubernetes-specific liveness/readiness distinction; a consuming application wires evaluate() into whatever route or probe its runtime expects.
Documentation
Full reference: https://studnicky.github.io/substrate/packages/health-registry
Entities
@studnicky/health-registry/entities exports every schema namespace in src/entities.
import { HealthStatusEntity } from '@studnicky/health-registry/entities';Interfaces
@studnicky/health-registry/interfaces exports every TypeScript interface in src/interfaces, including configuration and state contracts.
import type { HealthCheckResultInterface } from '@studnicky/health-registry/interfaces';Exports
| Symbol | Purpose | Import path |
|---|---|---|
HealthRegistry | Provides health registry functionality. | @studnicky/health-registry |
HealthCheckInterface | Defines the health check contract. | @studnicky/health-registry |