@quanticjs/health 3.1.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 ADDED
@@ -0,0 +1,389 @@
1
+ # @quanticjs/health
2
+
3
+ Health check module for QuanticJS applications. Provides liveness, readiness, and startup probes with auto-detection, caching, and multiple transports for both HTTP and non-HTTP apps.
4
+
5
+ ## Installation
6
+
7
+ Already included in the `@quanticjs/quanticjs` umbrella. For standalone use:
8
+
9
+ ```bash
10
+ npm install @quanticjs/health
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ### HTTP API (most common)
16
+
17
+ ```typescript
18
+ import { Module } from '@nestjs/common';
19
+ import { QuanticCoreModule } from '@quanticjs/core';
20
+ import { QuanticHealthModule } from '@quanticjs/health';
21
+
22
+ @Module({
23
+ imports: [
24
+ QuanticCoreModule.forRoot(),
25
+ QuanticHealthModule.forRoot({
26
+ transport: { type: 'controller' },
27
+ }),
28
+ ],
29
+ })
30
+ export class AppModule {}
31
+ ```
32
+
33
+ This registers three endpoints on your existing NestJS server:
34
+
35
+ | Endpoint | Probe | Purpose |
36
+ |----------|-------|---------|
37
+ | `GET /health/live` | Liveness | Is the process alive? Restart if not. |
38
+ | `GET /health/ready` | Readiness | Can this instance serve traffic? Remove from LB if not. |
39
+ | `GET /health/startup` | Startup | Has the app finished initializing? |
40
+
41
+ All endpoints are public (no auth required) and return:
42
+
43
+ ```json
44
+ {
45
+ "status": "ok",
46
+ "checks": {
47
+ "database": { "status": "ok", "latency_ms": 2 },
48
+ "redis": { "status": "ok", "latency_ms": 1 }
49
+ },
50
+ "timestamp": "2026-05-18T10:32:01.123Z"
51
+ }
52
+ ```
53
+
54
+ Status code is `200` when all checks pass, `503` when any check fails.
55
+
56
+ ## Auto-Detection
57
+
58
+ When `autoDetect` is enabled (the default), the module scans the NestJS DI container on startup and registers checks automatically:
59
+
60
+ | Dependency | Detection | Probe | Check |
61
+ |------------|-----------|-------|-------|
62
+ | TypeORM `DataSource` | `typeorm` installed + DataSource in DI | readiness | `SELECT 1` |
63
+ | Redis (`REDIS_CLIENT`) | `QuanticRedisModule` imported | readiness | `redis.ping()` |
64
+ | Event loop | Always | liveness | `setImmediate` responsiveness |
65
+
66
+ No configuration needed for framework-managed dependencies. The module logs what it discovers:
67
+
68
+ ```
69
+ [HealthRegistry] Auto-detected DataSource → database readiness check
70
+ [HealthRegistry] Auto-detected Redis → redis readiness check
71
+ [HealthRegistry] readiness: [database, redis]
72
+ [HealthRegistry] liveness: [event_loop]
73
+ ```
74
+
75
+ Disable with `autoDetect: false`.
76
+
77
+ ## Custom Checks
78
+
79
+ Add product-specific checks for dependencies the framework doesn't know about.
80
+
81
+ ### Function checks
82
+
83
+ ```typescript
84
+ QuanticHealthModule.forRoot({
85
+ transport: { type: 'controller' },
86
+ readiness: [
87
+ {
88
+ name: 'minio',
89
+ check: async () => {
90
+ const exists = await minioClient.bucketExists('my-bucket');
91
+ if (!exists) throw new Error('Bucket not found');
92
+ },
93
+ timeoutMs: 5000, // optional, default 3000
94
+ },
95
+ ],
96
+ startup: [
97
+ {
98
+ name: 'migrations',
99
+ check: async () => {
100
+ if (!migrationService.isComplete()) throw new Error('Migrations pending');
101
+ },
102
+ },
103
+ ],
104
+ })
105
+ ```
106
+
107
+ The check function should **resolve** when healthy and **throw** when unhealthy.
108
+
109
+ ### HTTP checks
110
+
111
+ For external services, pass a `url` instead of a `check` function:
112
+
113
+ ```typescript
114
+ QuanticHealthModule.forRoot({
115
+ transport: { type: 'controller' },
116
+ readiness: [
117
+ { name: 'ai-gateway', url: 'http://ai-gateway:3005/health' },
118
+ { name: 'kogito', url: 'http://kogito:8080/q/health/live', timeoutMs: 5000 },
119
+ ],
120
+ })
121
+ ```
122
+
123
+ HTTP checks expect a 2xx response. Any other status or network error marks the check as unhealthy.
124
+
125
+ ## Transports
126
+
127
+ The transport determines **how** probes reach the app.
128
+
129
+ ### `controller` — HTTP API apps
130
+
131
+ Mounts on the existing NestJS HTTP server. Endpoints: `/health/live`, `/health/ready`, `/health/startup`.
132
+
133
+ ```typescript
134
+ QuanticHealthModule.forRoot({
135
+ transport: { type: 'controller' },
136
+ })
137
+ ```
138
+
139
+ ### `standalone` — Workers, consumers, queue processors
140
+
141
+ Spins up a minimal HTTP server on a **separate port**. No NestJS overhead — just raw `http.createServer`.
142
+
143
+ ```typescript
144
+ QuanticHealthModule.forRoot({
145
+ transport: { type: 'standalone', port: 9091 },
146
+ })
147
+ ```
148
+
149
+ Endpoints: `/live`, `/ready`, `/startup` (no `/health` prefix since this is a dedicated health server).
150
+
151
+ Use this for apps that have no HTTP server but still need K8s/Docker health probes.
152
+
153
+ ### `file` — Cron jobs, one-shot migration scripts
154
+
155
+ Writes a JSON file when healthy, deletes it when unhealthy. Probes check file existence.
156
+
157
+ ```typescript
158
+ QuanticHealthModule.forRoot({
159
+ transport: {
160
+ type: 'file',
161
+ path: '/tmp/.healthy', // optional, default '/tmp/.healthy'
162
+ intervalMs: 10000, // optional, default 10000
163
+ },
164
+ })
165
+ ```
166
+
167
+ Docker healthcheck: `test: ["CMD", "test", "-f", "/tmp/.healthy"]`
168
+
169
+ ### `none` — Programmatic only
170
+
171
+ No endpoints, no files. Inject `HealthRegistry` directly for custom usage:
172
+
173
+ ```typescript
174
+ @Injectable()
175
+ export class MyService {
176
+ constructor(private readonly health: HealthRegistry) {}
177
+
178
+ async checkDeps() {
179
+ const report = await this.health.evaluate('readiness');
180
+ if (report.status === 'error') {
181
+ // handle unhealthy state
182
+ }
183
+ }
184
+ }
185
+ ```
186
+
187
+ ## Result Caching
188
+
189
+ By default, check results are cached for 5 seconds to avoid hammering dependencies on high-frequency probe intervals.
190
+
191
+ ```typescript
192
+ QuanticHealthModule.forRoot({
193
+ transport: { type: 'controller' },
194
+ cacheTtlMs: 10000, // cache for 10s
195
+ })
196
+ ```
197
+
198
+ Set to `0` to disable caching (every probe hit runs all checks).
199
+
200
+ ## Shutdown Integration
201
+
202
+ When the app receives SIGTERM, the module:
203
+
204
+ 1. Flips readiness to `503` immediately (before `GracefulShutdownService` runs)
205
+ 2. Waits `shutdownDelayMs` (default 5s) for the load balancer to observe the change
206
+ 3. Then `GracefulShutdownService` proceeds with its drain + cleanup
207
+
208
+ ```json
209
+ {
210
+ "status": "error",
211
+ "reason": "shutting_down",
212
+ "checks": {},
213
+ "timestamp": "2026-05-18T10:35:00.000Z"
214
+ }
215
+ ```
216
+
217
+ This ensures no new traffic arrives while the app is draining in-flight work.
218
+
219
+ ```typescript
220
+ QuanticHealthModule.forRoot({
221
+ transport: { type: 'controller' },
222
+ shutdownAware: true, // default true
223
+ shutdownDelayMs: 5000, // default 5000
224
+ })
225
+ ```
226
+
227
+ Disable with `shutdownAware: false` if you don't need this behavior.
228
+
229
+ ## Docker Compose
230
+
231
+ ```yaml
232
+ backend:
233
+ healthcheck:
234
+ test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/health/ready"]
235
+ interval: 10s
236
+ timeout: 5s
237
+ retries: 3
238
+ start_period: 30s
239
+
240
+ worker:
241
+ healthcheck:
242
+ test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:9091/ready"]
243
+ interval: 10s
244
+ timeout: 5s
245
+ retries: 3
246
+ start_period: 15s
247
+
248
+ migration-job:
249
+ healthcheck:
250
+ test: ["CMD", "test", "-f", "/tmp/.healthy"]
251
+ interval: 5s
252
+ retries: 3
253
+ ```
254
+
255
+ ## Kubernetes
256
+
257
+ ```yaml
258
+ # HTTP API
259
+ spec:
260
+ containers:
261
+ - name: backend
262
+ livenessProbe:
263
+ httpGet:
264
+ path: /health/live
265
+ port: 3000
266
+ periodSeconds: 10
267
+ failureThreshold: 3
268
+ readinessProbe:
269
+ httpGet:
270
+ path: /health/ready
271
+ port: 3000
272
+ periodSeconds: 10
273
+ failureThreshold: 3
274
+ startupProbe:
275
+ httpGet:
276
+ path: /health/startup
277
+ port: 3000
278
+ periodSeconds: 5
279
+ failureThreshold: 30
280
+
281
+ ---
282
+ # Worker (standalone transport)
283
+ spec:
284
+ containers:
285
+ - name: worker
286
+ livenessProbe:
287
+ httpGet:
288
+ path: /live
289
+ port: 9091
290
+ readinessProbe:
291
+ httpGet:
292
+ path: /ready
293
+ port: 9091
294
+ ```
295
+
296
+ ## Registering Checks Programmatically
297
+
298
+ Inject `HealthRegistry` to register checks at runtime:
299
+
300
+ ```typescript
301
+ @Injectable()
302
+ export class KafkaConsumer implements OnModuleInit {
303
+ constructor(private readonly health: HealthRegistry) {}
304
+
305
+ onModuleInit() {
306
+ this.health.register('readiness', 'kafka', async () => {
307
+ if (!this.consumer.isConnected()) throw new Error('Kafka disconnected');
308
+ });
309
+ }
310
+ }
311
+ ```
312
+
313
+ ## Full Options Reference
314
+
315
+ ```typescript
316
+ interface HealthModuleOptions {
317
+ transport: HealthTransport;
318
+ autoDetect?: boolean; // default: true
319
+ cacheTtlMs?: number; // default: 5000
320
+ shutdownAware?: boolean; // default: true
321
+ shutdownDelayMs?: number; // default: 5000
322
+ readiness?: HealthCheckConfig[];
323
+ startup?: HealthCheckConfig[];
324
+ liveness?: HealthCheckConfig[];
325
+ }
326
+
327
+ // Custom function check
328
+ interface CustomHealthCheck {
329
+ name: string;
330
+ check: () => Promise<void>; // resolve = healthy, throw = unhealthy
331
+ timeoutMs?: number; // default: 3000
332
+ }
333
+
334
+ // HTTP endpoint check
335
+ interface HttpHealthCheck {
336
+ name: string;
337
+ url: string;
338
+ timeoutMs?: number; // default: 3000
339
+ }
340
+
341
+ type HealthTransport =
342
+ | { type: 'controller' }
343
+ | { type: 'standalone'; port: number }
344
+ | { type: 'file'; path?: string; intervalMs?: number }
345
+ | { type: 'none' };
346
+ ```
347
+
348
+ ## Complete Example (AutoFlux-style)
349
+
350
+ ```typescript
351
+ import { Module } from '@nestjs/common';
352
+ import { QuanticCoreModule } from '@quanticjs/core';
353
+ import { QuanticRedisModule } from '@quanticjs/redis';
354
+ import { QuanticHealthModule } from '@quanticjs/health';
355
+
356
+ @Module({
357
+ imports: [
358
+ QuanticCoreModule.forRoot(),
359
+ QuanticRedisModule.forRoot(),
360
+ // TypeOrmModule.forRoot({ ... }),
361
+
362
+ QuanticHealthModule.forRoot({
363
+ transport: { type: 'controller' },
364
+
365
+ // Auto-detects: database (TypeORM), redis, event_loop
366
+ // Add product-specific checks below:
367
+
368
+ readiness: [
369
+ { name: 'minio', check: async () => {
370
+ const exists = await minioClient.bucketExists('autoflux');
371
+ if (!exists) throw new Error('Bucket missing');
372
+ }},
373
+ { name: 'ai-gateway', url: 'http://ai-gateway:3005/health' },
374
+ { name: 'kogito', url: 'http://kogito:8080/q/health/live', timeoutMs: 5000 },
375
+ ],
376
+
377
+ startup: [
378
+ { name: 'templates-seeded', check: async () => {
379
+ if (!seederService.isComplete()) throw new Error('Seeding in progress');
380
+ }},
381
+ ],
382
+
383
+ cacheTtlMs: 5000,
384
+ shutdownDelayMs: 5000,
385
+ }),
386
+ ],
387
+ })
388
+ export class AppModule {}
389
+ ```
@@ -0,0 +1,14 @@
1
+ import { OnModuleInit, OnModuleDestroy } from '@nestjs/common';
2
+ import { HealthRegistry } from './HealthRegistry';
3
+ import { type HealthModuleOptions } from './interfaces';
4
+ export declare class FileHealthWriter implements OnModuleInit, OnModuleDestroy {
5
+ private readonly registry;
6
+ private readonly options;
7
+ private readonly logger;
8
+ private timer?;
9
+ constructor(registry: HealthRegistry, options: HealthModuleOptions);
10
+ onModuleInit(): void;
11
+ onModuleDestroy(): void;
12
+ private write;
13
+ private getPath;
14
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ var FileHealthWriter_1;
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.FileHealthWriter = void 0;
20
+ const common_1 = require("@nestjs/common");
21
+ const promises_1 = __importDefault(require("node:fs/promises"));
22
+ const HealthRegistry_1 = require("./HealthRegistry");
23
+ const interfaces_1 = require("./interfaces");
24
+ const DEFAULT_PATH = '/tmp/.healthy';
25
+ const DEFAULT_INTERVAL_MS = 10_000;
26
+ let FileHealthWriter = FileHealthWriter_1 = class FileHealthWriter {
27
+ registry;
28
+ options;
29
+ logger = new common_1.Logger(FileHealthWriter_1.name);
30
+ timer;
31
+ constructor(registry, options) {
32
+ this.registry = registry;
33
+ this.options = options;
34
+ }
35
+ onModuleInit() {
36
+ if (this.options.transport.type !== 'file')
37
+ return;
38
+ const intervalMs = this.options.transport.intervalMs ?? DEFAULT_INTERVAL_MS;
39
+ this.timer = setInterval(() => this.write(), intervalMs);
40
+ this.write();
41
+ }
42
+ onModuleDestroy() {
43
+ if (this.timer)
44
+ clearInterval(this.timer);
45
+ const path = this.getPath();
46
+ promises_1.default.unlink(path).catch(() => { });
47
+ }
48
+ async write() {
49
+ const path = this.getPath();
50
+ try {
51
+ const report = await this.registry.evaluate('readiness');
52
+ if (report.status === 'ok') {
53
+ await promises_1.default.writeFile(path, JSON.stringify(report), 'utf-8');
54
+ }
55
+ else {
56
+ await promises_1.default.unlink(path).catch(() => { });
57
+ }
58
+ }
59
+ catch (err) {
60
+ this.logger.warn(`Failed to write health file: ${err.message}`);
61
+ await promises_1.default.unlink(path).catch(() => { });
62
+ }
63
+ }
64
+ getPath() {
65
+ return this.options.transport.type === 'file'
66
+ ? (this.options.transport.path ?? DEFAULT_PATH)
67
+ : DEFAULT_PATH;
68
+ }
69
+ };
70
+ exports.FileHealthWriter = FileHealthWriter;
71
+ exports.FileHealthWriter = FileHealthWriter = FileHealthWriter_1 = __decorate([
72
+ (0, common_1.Injectable)(),
73
+ __param(1, (0, common_1.Inject)(interfaces_1.HEALTH_OPTIONS)),
74
+ __metadata("design:paramtypes", [HealthRegistry_1.HealthRegistry, Object])
75
+ ], FileHealthWriter);
@@ -0,0 +1,9 @@
1
+ import { Response } from 'express';
2
+ import { HealthRegistry } from './HealthRegistry';
3
+ export declare class HealthController {
4
+ private readonly registry;
5
+ constructor(registry: HealthRegistry);
6
+ live(res: Response): Promise<void>;
7
+ ready(res: Response): Promise<void>;
8
+ startup(res: Response): Promise<void>;
9
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.HealthController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const core_1 = require("@quanticjs/core");
18
+ const HealthRegistry_1 = require("./HealthRegistry");
19
+ let HealthController = class HealthController {
20
+ registry;
21
+ constructor(registry) {
22
+ this.registry = registry;
23
+ }
24
+ async live(res) {
25
+ const report = await this.registry.evaluate('liveness');
26
+ res.status(report.status === 'ok' ? common_1.HttpStatus.OK : common_1.HttpStatus.SERVICE_UNAVAILABLE).json(report);
27
+ }
28
+ async ready(res) {
29
+ const report = await this.registry.evaluate('readiness');
30
+ res.status(report.status === 'ok' ? common_1.HttpStatus.OK : common_1.HttpStatus.SERVICE_UNAVAILABLE).json(report);
31
+ }
32
+ async startup(res) {
33
+ const report = await this.registry.evaluate('startup');
34
+ res.status(report.status === 'ok' ? common_1.HttpStatus.OK : common_1.HttpStatus.SERVICE_UNAVAILABLE).json(report);
35
+ }
36
+ };
37
+ exports.HealthController = HealthController;
38
+ __decorate([
39
+ (0, common_1.Get)('live'),
40
+ __param(0, (0, common_1.Res)()),
41
+ __metadata("design:type", Function),
42
+ __metadata("design:paramtypes", [Object]),
43
+ __metadata("design:returntype", Promise)
44
+ ], HealthController.prototype, "live", null);
45
+ __decorate([
46
+ (0, common_1.Get)('ready'),
47
+ __param(0, (0, common_1.Res)()),
48
+ __metadata("design:type", Function),
49
+ __metadata("design:paramtypes", [Object]),
50
+ __metadata("design:returntype", Promise)
51
+ ], HealthController.prototype, "ready", null);
52
+ __decorate([
53
+ (0, common_1.Get)('startup'),
54
+ __param(0, (0, common_1.Res)()),
55
+ __metadata("design:type", Function),
56
+ __metadata("design:paramtypes", [Object]),
57
+ __metadata("design:returntype", Promise)
58
+ ], HealthController.prototype, "startup", null);
59
+ exports.HealthController = HealthController = __decorate([
60
+ (0, core_1.Public)(),
61
+ (0, common_1.Controller)('health'),
62
+ __metadata("design:paramtypes", [HealthRegistry_1.HealthRegistry])
63
+ ], HealthController);
@@ -0,0 +1,26 @@
1
+ import { OnModuleInit } from '@nestjs/common';
2
+ import { ModuleRef } from '@nestjs/core';
3
+ import { type HealthModuleOptions, type HealthReport } from './interfaces';
4
+ type ProbeType = 'liveness' | 'readiness' | 'startup';
5
+ export declare class HealthRegistry implements OnModuleInit {
6
+ private readonly options;
7
+ private readonly moduleRef;
8
+ private readonly logger;
9
+ private shuttingDown;
10
+ private readonly checks;
11
+ private readonly cache;
12
+ constructor(options: HealthModuleOptions, moduleRef: ModuleRef);
13
+ onModuleInit(): void;
14
+ register(type: ProbeType, name: string, fn: () => Promise<void>, timeoutMs?: number): void;
15
+ setShuttingDown(): void;
16
+ isShuttingDown(): boolean;
17
+ evaluate(type: ProbeType): Promise<HealthReport>;
18
+ private runCheck;
19
+ private autoDetect;
20
+ private autoDetectDatabase;
21
+ private autoDetectRedis;
22
+ private registerConfigs;
23
+ private httpCheck;
24
+ private logRegistered;
25
+ }
26
+ export {};
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ var HealthRegistry_1;
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.HealthRegistry = void 0;
20
+ const common_1 = require("@nestjs/common");
21
+ const core_1 = require("@nestjs/core");
22
+ const core_2 = require("@quanticjs/core");
23
+ const node_http_1 = __importDefault(require("node:http"));
24
+ const interfaces_1 = require("./interfaces");
25
+ const DEFAULT_TIMEOUT_MS = 3_000;
26
+ const DEFAULT_CACHE_TTL_MS = 5_000;
27
+ let HealthRegistry = HealthRegistry_1 = class HealthRegistry {
28
+ options;
29
+ moduleRef;
30
+ logger = new common_1.Logger(HealthRegistry_1.name);
31
+ shuttingDown = false;
32
+ checks = new Map([
33
+ ['liveness', new Map()],
34
+ ['readiness', new Map()],
35
+ ['startup', new Map()],
36
+ ]);
37
+ cache = new Map();
38
+ constructor(options, moduleRef) {
39
+ this.options = options;
40
+ this.moduleRef = moduleRef;
41
+ }
42
+ onModuleInit() {
43
+ if (this.options.autoDetect !== false) {
44
+ this.autoDetect();
45
+ }
46
+ this.registerConfigs('liveness', this.options.liveness);
47
+ this.registerConfigs('readiness', this.options.readiness);
48
+ this.registerConfigs('startup', this.options.startup);
49
+ this.logRegistered();
50
+ }
51
+ register(type, name, fn, timeoutMs = DEFAULT_TIMEOUT_MS) {
52
+ this.checks.get(type).set(name, { fn, timeoutMs });
53
+ }
54
+ setShuttingDown() {
55
+ this.shuttingDown = true;
56
+ this.cache.clear();
57
+ }
58
+ isShuttingDown() {
59
+ return this.shuttingDown;
60
+ }
61
+ async evaluate(type) {
62
+ if (type === 'readiness' && this.shuttingDown) {
63
+ return {
64
+ status: 'error',
65
+ reason: 'shutting_down',
66
+ checks: {},
67
+ timestamp: new Date().toISOString(),
68
+ };
69
+ }
70
+ const ttl = this.options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
71
+ const cached = this.cache.get(type);
72
+ if (cached && Date.now() < cached.expiry) {
73
+ return cached.report;
74
+ }
75
+ const checkMap = this.checks.get(type);
76
+ const entries = [...checkMap.entries()];
77
+ if (entries.length === 0) {
78
+ const report = { status: 'ok', checks: {}, timestamp: new Date().toISOString() };
79
+ this.cache.set(type, { report, expiry: Date.now() + ttl });
80
+ return report;
81
+ }
82
+ const results = await Promise.allSettled(entries.map(([, { fn, timeoutMs }]) => this.runCheck(fn, timeoutMs)));
83
+ const checks = {};
84
+ let allOk = true;
85
+ for (let i = 0; i < entries.length; i++) {
86
+ const [name] = entries[i];
87
+ const result = results[i];
88
+ checks[name] = result.status === 'fulfilled'
89
+ ? result.value
90
+ : { status: 'error', latency_ms: 0, error: String(result.reason) };
91
+ if (checks[name].status === 'error')
92
+ allOk = false;
93
+ }
94
+ const report = {
95
+ status: allOk ? 'ok' : 'error',
96
+ checks,
97
+ timestamp: new Date().toISOString(),
98
+ };
99
+ this.cache.set(type, { report, expiry: Date.now() + ttl });
100
+ return report;
101
+ }
102
+ async runCheck(fn, timeoutMs) {
103
+ const start = performance.now();
104
+ try {
105
+ await Promise.race([
106
+ fn(),
107
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs)),
108
+ ]);
109
+ return { status: 'ok', latency_ms: Math.round(performance.now() - start) };
110
+ }
111
+ catch (err) {
112
+ return {
113
+ status: 'error',
114
+ latency_ms: Math.round(performance.now() - start),
115
+ error: err.message,
116
+ };
117
+ }
118
+ }
119
+ autoDetect() {
120
+ this.autoDetectDatabase();
121
+ this.autoDetectRedis();
122
+ this.register('liveness', 'event_loop', async () => {
123
+ await new Promise((resolve) => setImmediate(resolve));
124
+ });
125
+ }
126
+ autoDetectDatabase() {
127
+ try {
128
+ // Resolve DataSource without a compile-time dependency on typeorm
129
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
130
+ const { DataSource } = require('typeorm');
131
+ const ds = this.moduleRef.get(DataSource, { strict: false });
132
+ if (ds?.isInitialized) {
133
+ this.register('readiness', 'database', async () => {
134
+ await ds.query('SELECT 1');
135
+ });
136
+ this.logger.log('Auto-detected DataSource → database readiness check');
137
+ }
138
+ }
139
+ catch {
140
+ // typeorm not installed or DataSource not in DI container
141
+ }
142
+ }
143
+ autoDetectRedis() {
144
+ try {
145
+ const redis = this.moduleRef.get(core_2.REDIS_CLIENT, { strict: false });
146
+ if (redis?.ping) {
147
+ this.register('readiness', 'redis', async () => {
148
+ const result = await redis.ping();
149
+ if (result !== 'PONG')
150
+ throw new Error(`Unexpected ping response: ${result}`);
151
+ });
152
+ this.logger.log('Auto-detected Redis → redis readiness check');
153
+ }
154
+ }
155
+ catch {
156
+ // Redis not in DI container
157
+ }
158
+ }
159
+ registerConfigs(type, configs) {
160
+ if (!configs)
161
+ return;
162
+ for (const config of configs) {
163
+ if ((0, interfaces_1.isHttpHealthCheck)(config)) {
164
+ const { name, url, timeoutMs = DEFAULT_TIMEOUT_MS } = config;
165
+ this.register(type, name, () => this.httpCheck(url, timeoutMs), timeoutMs);
166
+ }
167
+ else {
168
+ this.register(type, config.name, config.check, config.timeoutMs);
169
+ }
170
+ }
171
+ }
172
+ httpCheck(url, timeoutMs) {
173
+ return new Promise((resolve, reject) => {
174
+ const req = node_http_1.default.get(url, { timeout: timeoutMs }, (res) => {
175
+ res.resume();
176
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
177
+ resolve();
178
+ }
179
+ else {
180
+ reject(new Error(`HTTP ${res.statusCode}`));
181
+ }
182
+ });
183
+ req.on('error', (err) => reject(err));
184
+ req.on('timeout', () => { req.destroy(); reject(new Error('HTTP timeout')); });
185
+ });
186
+ }
187
+ logRegistered() {
188
+ for (const [type, checks] of this.checks) {
189
+ if (checks.size > 0) {
190
+ this.logger.log(`${type}: [${[...checks.keys()].join(', ')}]`);
191
+ }
192
+ }
193
+ }
194
+ };
195
+ exports.HealthRegistry = HealthRegistry;
196
+ exports.HealthRegistry = HealthRegistry = HealthRegistry_1 = __decorate([
197
+ (0, common_1.Injectable)(),
198
+ __param(0, (0, common_1.Inject)(interfaces_1.HEALTH_OPTIONS)),
199
+ __metadata("design:paramtypes", [Object, core_1.ModuleRef])
200
+ ], HealthRegistry);
@@ -0,0 +1,10 @@
1
+ import { BeforeApplicationShutdown } from '@nestjs/common';
2
+ import { HealthRegistry } from './HealthRegistry';
3
+ import { type HealthModuleOptions } from './interfaces';
4
+ export declare class HealthShutdownHook implements BeforeApplicationShutdown {
5
+ private readonly registry;
6
+ private readonly options;
7
+ private readonly logger;
8
+ constructor(registry: HealthRegistry, options: HealthModuleOptions);
9
+ beforeApplicationShutdown(): Promise<void>;
10
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var HealthShutdownHook_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.HealthShutdownHook = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const HealthRegistry_1 = require("./HealthRegistry");
19
+ const interfaces_1 = require("./interfaces");
20
+ const DEFAULT_SHUTDOWN_DELAY_MS = 5_000;
21
+ let HealthShutdownHook = HealthShutdownHook_1 = class HealthShutdownHook {
22
+ registry;
23
+ options;
24
+ logger = new common_1.Logger(HealthShutdownHook_1.name);
25
+ constructor(registry, options) {
26
+ this.registry = registry;
27
+ this.options = options;
28
+ }
29
+ async beforeApplicationShutdown() {
30
+ if (this.options.shutdownAware === false)
31
+ return;
32
+ this.registry.setShuttingDown();
33
+ this.logger.log('Readiness probe now returns 503');
34
+ const delay = this.options.shutdownDelayMs ?? DEFAULT_SHUTDOWN_DELAY_MS;
35
+ if (delay > 0) {
36
+ this.logger.log(`Waiting ${delay}ms for load balancer to observe readiness change...`);
37
+ await new Promise((resolve) => setTimeout(resolve, delay));
38
+ }
39
+ }
40
+ };
41
+ exports.HealthShutdownHook = HealthShutdownHook;
42
+ exports.HealthShutdownHook = HealthShutdownHook = HealthShutdownHook_1 = __decorate([
43
+ (0, common_1.Injectable)(),
44
+ __param(1, (0, common_1.Inject)(interfaces_1.HEALTH_OPTIONS)),
45
+ __metadata("design:paramtypes", [HealthRegistry_1.HealthRegistry, Object])
46
+ ], HealthShutdownHook);
@@ -0,0 +1,5 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ import { type HealthModuleOptions } from './interfaces';
3
+ export declare class QuanticHealthModule {
4
+ static forRoot(options?: HealthModuleOptions): DynamicModule;
5
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var QuanticHealthModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.QuanticHealthModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const HealthRegistry_1 = require("./HealthRegistry");
13
+ const HealthController_1 = require("./HealthController");
14
+ const StandaloneHealthServer_1 = require("./StandaloneHealthServer");
15
+ const FileHealthWriter_1 = require("./FileHealthWriter");
16
+ const HealthShutdownHook_1 = require("./HealthShutdownHook");
17
+ const interfaces_1 = require("./interfaces");
18
+ let QuanticHealthModule = QuanticHealthModule_1 = class QuanticHealthModule {
19
+ static forRoot(options = { transport: { type: 'controller' } }) {
20
+ const providers = [
21
+ { provide: interfaces_1.HEALTH_OPTIONS, useValue: options },
22
+ HealthRegistry_1.HealthRegistry,
23
+ HealthShutdownHook_1.HealthShutdownHook,
24
+ ];
25
+ const controllers = [];
26
+ switch (options.transport.type) {
27
+ case 'controller':
28
+ controllers.push(HealthController_1.HealthController);
29
+ break;
30
+ case 'standalone':
31
+ providers.push(StandaloneHealthServer_1.StandaloneHealthServer);
32
+ break;
33
+ case 'file':
34
+ providers.push(FileHealthWriter_1.FileHealthWriter);
35
+ break;
36
+ }
37
+ return {
38
+ module: QuanticHealthModule_1,
39
+ controllers,
40
+ providers,
41
+ exports: [HealthRegistry_1.HealthRegistry],
42
+ };
43
+ }
44
+ };
45
+ exports.QuanticHealthModule = QuanticHealthModule;
46
+ exports.QuanticHealthModule = QuanticHealthModule = QuanticHealthModule_1 = __decorate([
47
+ (0, common_1.Global)(),
48
+ (0, common_1.Module)({})
49
+ ], QuanticHealthModule);
@@ -0,0 +1,13 @@
1
+ import { OnModuleInit, OnModuleDestroy } from '@nestjs/common';
2
+ import { HealthRegistry } from './HealthRegistry';
3
+ import { type HealthModuleOptions } from './interfaces';
4
+ export declare class StandaloneHealthServer implements OnModuleInit, OnModuleDestroy {
5
+ private readonly registry;
6
+ private readonly options;
7
+ private readonly logger;
8
+ private server?;
9
+ constructor(registry: HealthRegistry, options: HealthModuleOptions);
10
+ onModuleInit(): Promise<void>;
11
+ onModuleDestroy(): Promise<void>;
12
+ private resolveProbe;
13
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ var StandaloneHealthServer_1;
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.StandaloneHealthServer = void 0;
20
+ const common_1 = require("@nestjs/common");
21
+ const node_http_1 = __importDefault(require("node:http"));
22
+ const HealthRegistry_1 = require("./HealthRegistry");
23
+ const interfaces_1 = require("./interfaces");
24
+ let StandaloneHealthServer = StandaloneHealthServer_1 = class StandaloneHealthServer {
25
+ registry;
26
+ options;
27
+ logger = new common_1.Logger(StandaloneHealthServer_1.name);
28
+ server;
29
+ constructor(registry, options) {
30
+ this.registry = registry;
31
+ this.options = options;
32
+ }
33
+ async onModuleInit() {
34
+ if (this.options.transport.type !== 'standalone')
35
+ return;
36
+ const { port } = this.options.transport;
37
+ this.server = node_http_1.default.createServer(async (req, res) => {
38
+ const probe = this.resolveProbe(req.url);
39
+ if (!probe) {
40
+ res.writeHead(404).end();
41
+ return;
42
+ }
43
+ try {
44
+ const report = await this.registry.evaluate(probe);
45
+ const status = report.status === 'ok' ? 200 : 503;
46
+ res.writeHead(status, { 'Content-Type': 'application/json' });
47
+ res.end(JSON.stringify(report));
48
+ }
49
+ catch (err) {
50
+ res.writeHead(500).end(JSON.stringify({ status: 'error', error: String(err) }));
51
+ }
52
+ });
53
+ await new Promise((resolve) => this.server.listen(port, resolve));
54
+ this.logger.log(`Health probe server listening on port ${port}`);
55
+ }
56
+ async onModuleDestroy() {
57
+ if (!this.server)
58
+ return;
59
+ await new Promise((resolve, reject) => this.server.close((err) => (err ? reject(err) : resolve())));
60
+ this.logger.log('Health probe server closed');
61
+ }
62
+ resolveProbe(url) {
63
+ const path = url?.split('?')[0];
64
+ if (path === '/live')
65
+ return 'liveness';
66
+ if (path === '/ready')
67
+ return 'readiness';
68
+ if (path === '/startup')
69
+ return 'startup';
70
+ return null;
71
+ }
72
+ };
73
+ exports.StandaloneHealthServer = StandaloneHealthServer;
74
+ exports.StandaloneHealthServer = StandaloneHealthServer = StandaloneHealthServer_1 = __decorate([
75
+ (0, common_1.Injectable)(),
76
+ __param(1, (0, common_1.Inject)(interfaces_1.HEALTH_OPTIONS)),
77
+ __metadata("design:paramtypes", [HealthRegistry_1.HealthRegistry, Object])
78
+ ], StandaloneHealthServer);
@@ -0,0 +1,7 @@
1
+ export { QuanticHealthModule } from './QuanticHealthModule';
2
+ export { HealthRegistry } from './HealthRegistry';
3
+ export { HealthController } from './HealthController';
4
+ export { StandaloneHealthServer } from './StandaloneHealthServer';
5
+ export { FileHealthWriter } from './FileHealthWriter';
6
+ export { HealthShutdownHook } from './HealthShutdownHook';
7
+ export { HEALTH_OPTIONS, type HealthModuleOptions, type HealthTransport, type HealthCheckConfig, type CustomHealthCheck, type HttpHealthCheck, type HealthReport, type HealthCheckDetail, } from './interfaces';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HEALTH_OPTIONS = exports.HealthShutdownHook = exports.FileHealthWriter = exports.StandaloneHealthServer = exports.HealthController = exports.HealthRegistry = exports.QuanticHealthModule = void 0;
4
+ var QuanticHealthModule_1 = require("./QuanticHealthModule");
5
+ Object.defineProperty(exports, "QuanticHealthModule", { enumerable: true, get: function () { return QuanticHealthModule_1.QuanticHealthModule; } });
6
+ var HealthRegistry_1 = require("./HealthRegistry");
7
+ Object.defineProperty(exports, "HealthRegistry", { enumerable: true, get: function () { return HealthRegistry_1.HealthRegistry; } });
8
+ var HealthController_1 = require("./HealthController");
9
+ Object.defineProperty(exports, "HealthController", { enumerable: true, get: function () { return HealthController_1.HealthController; } });
10
+ var StandaloneHealthServer_1 = require("./StandaloneHealthServer");
11
+ Object.defineProperty(exports, "StandaloneHealthServer", { enumerable: true, get: function () { return StandaloneHealthServer_1.StandaloneHealthServer; } });
12
+ var FileHealthWriter_1 = require("./FileHealthWriter");
13
+ Object.defineProperty(exports, "FileHealthWriter", { enumerable: true, get: function () { return FileHealthWriter_1.FileHealthWriter; } });
14
+ var HealthShutdownHook_1 = require("./HealthShutdownHook");
15
+ Object.defineProperty(exports, "HealthShutdownHook", { enumerable: true, get: function () { return HealthShutdownHook_1.HealthShutdownHook; } });
16
+ var interfaces_1 = require("./interfaces");
17
+ Object.defineProperty(exports, "HEALTH_OPTIONS", { enumerable: true, get: function () { return interfaces_1.HEALTH_OPTIONS; } });
@@ -0,0 +1,51 @@
1
+ export declare const HEALTH_OPTIONS: unique symbol;
2
+ export type HealthTransport = {
3
+ type: 'controller';
4
+ } | {
5
+ type: 'standalone';
6
+ port: number;
7
+ } | {
8
+ type: 'file';
9
+ path?: string;
10
+ intervalMs?: number;
11
+ } | {
12
+ type: 'none';
13
+ };
14
+ export interface HealthModuleOptions {
15
+ transport: HealthTransport;
16
+ /** Scan DI container for DataSource/Redis and register checks automatically (default: true) */
17
+ autoDetect?: boolean;
18
+ /** Cache check results for this many ms (default: 5000) */
19
+ cacheTtlMs?: number;
20
+ /** Flip readiness to 503 on SIGTERM before draining (default: true) */
21
+ shutdownAware?: boolean;
22
+ /** Ms to wait after flipping readiness before shutdown proceeds (default: 5000) */
23
+ shutdownDelayMs?: number;
24
+ readiness?: HealthCheckConfig[];
25
+ startup?: HealthCheckConfig[];
26
+ liveness?: HealthCheckConfig[];
27
+ }
28
+ export type HealthCheckConfig = CustomHealthCheck | HttpHealthCheck;
29
+ export interface CustomHealthCheck {
30
+ name: string;
31
+ /** Resolves = healthy, throws = unhealthy */
32
+ check: () => Promise<void>;
33
+ timeoutMs?: number;
34
+ }
35
+ export interface HttpHealthCheck {
36
+ name: string;
37
+ url: string;
38
+ timeoutMs?: number;
39
+ }
40
+ export interface HealthReport {
41
+ status: 'ok' | 'error';
42
+ reason?: string;
43
+ checks: Record<string, HealthCheckDetail>;
44
+ timestamp: string;
45
+ }
46
+ export interface HealthCheckDetail {
47
+ status: 'ok' | 'error';
48
+ latency_ms: number;
49
+ error?: string;
50
+ }
51
+ export declare function isHttpHealthCheck(check: HealthCheckConfig): check is HttpHealthCheck;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HEALTH_OPTIONS = void 0;
4
+ exports.isHttpHealthCheck = isHttpHealthCheck;
5
+ exports.HEALTH_OPTIONS = Symbol('HEALTH_OPTIONS');
6
+ function isHttpHealthCheck(check) {
7
+ return 'url' in check;
8
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@quanticjs/health",
3
+ "version": "3.1.0",
4
+ "description": "Health check module — liveness, readiness, startup probes with auto-detection, caching, and multiple transports",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "publishConfig": {
11
+ "registry": "https://registry.npmjs.org",
12
+ "access": "public"
13
+ },
14
+ "license": "MIT",
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "test": "jest --passWithNoTests",
18
+ "clean": "rm -rf dist"
19
+ },
20
+ "dependencies": {
21
+ "@quanticjs/core": "^3.1.0"
22
+ },
23
+ "peerDependencies": {
24
+ "@nestjs/common": "^11.0.0",
25
+ "@nestjs/core": "^11.0.0"
26
+ }
27
+ }