@catbee/utils 2.1.1 → 2.2.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/server/index.mjs CHANGED
@@ -26,17 +26,18 @@ import express from 'express';
26
26
  import http from 'http';
27
27
  import https from 'https';
28
28
  import { HttpStatusCodes } from '@catbee/utils/http-status-codes';
29
- import { createFinalErrorResponse, SuccessResponse } from '@catbee/utils/response';
29
+ import { createFinalErrorResponse } from '@catbee/utils/response';
30
30
  import { requestId, setupRequestContext, timeout, responseTime, errorHandler } from '@catbee/utils/middleware';
31
31
  import { Env } from '@catbee/utils/env';
32
32
  import { getLogger } from '@catbee/utils/logger';
33
- import { ServiceUnavailableException, InternalServerErrorException, NotFoundException } from '@catbee/utils/exception';
33
+ import { ServiceUnavailableException, NotFoundException } from '@catbee/utils/exception';
34
34
  import { getCatbeeServerGlobalConfig } from '@catbee/utils/config';
35
- import { deepObjMerge, isPlainObject, deepClone } from '@catbee/utils/object';
35
+ import { isPlainObject, deepClone, deepObjMerge } from '@catbee/utils/object';
36
36
  import { fileExists, readFile, readFileSync } from '@catbee/utils/fs';
37
37
  import { isPort, isHostname } from '@catbee/utils/validation';
38
38
  import { optionalRequire } from '@catbee/utils/async';
39
39
  import { uuid } from '@catbee/utils/id';
40
+ import { HealthzServer } from '@catbee/utils/healthz-server';
40
41
 
41
42
  var __defProp = Object.defineProperty;
42
43
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
@@ -299,36 +300,53 @@ var ServerConfigBuilder = class {
299
300
  return this.setEnabled("requestLogging", false);
300
301
  }
301
302
  /**
302
- * Configures server health check endpoint.
303
+ * Configures the dedicated Healthz probe HTTP server for Kubernetes.
303
304
  *
304
- * @param opts - Health check configuration options
305
+ * @param opts - Healthz server configuration options or boolean toggle
305
306
  * @returns The builder instance for chaining
306
- * @default - { path: '/healthz', detailed: true, withGlobalPrefix: false }
307
307
  *
308
308
  * @example
309
309
  * ```typescript
310
- * builder.withHealthCheck({
311
- * path: '/health',
312
- * detailed: true
310
+ * builder.withHealthzServer({
311
+ * port: 8282,
312
+ * shutdownDelayMs: 5000,
313
+ * readinessChecks: [
314
+ * { name: 'db', check: () => checkDb() }
315
+ * ]
313
316
  * })
314
317
  * ```
315
318
  */
316
- withHealthCheck(opts) {
317
- this.mergeConfig("healthCheck", opts);
319
+ withHealthzServer(opts) {
320
+ if (typeof opts === "boolean") {
321
+ this.config.healthzServer = opts;
322
+ } else {
323
+ const current = isPlainObject(this.config.healthzServer) ? deepClone(this.config.healthzServer) : {};
324
+ this.config.healthzServer = deepObjMerge({}, current, {
325
+ enable: true,
326
+ ...opts
327
+ });
328
+ }
318
329
  return this;
319
330
  }
320
331
  /**
321
- * Enables health check endpoint with default or custom settings
322
- * @param opts - Optional health check configuration
332
+ * Enables the dedicated Healthz probe HTTP server.
333
+ *
334
+ * @param opts - Optional Healthz server configuration options
323
335
  * @returns The builder instance for chaining
336
+ */
337
+ enableHealthzServer(opts = {}) {
338
+ return this.withHealthzServer({
339
+ ...opts,
340
+ enable: true
341
+ });
342
+ }
343
+ /**
344
+ * Disables the dedicated Healthz probe HTTP server.
324
345
  *
325
- * @example
326
- * ```typescript
327
- * builder.disableHealthCheck()
328
- * ```
346
+ * @returns The builder instance for chaining
329
347
  */
330
- disableHealthCheck() {
331
- return this.setEnabled("healthCheck", false);
348
+ disableHealthzServer() {
349
+ return this.withHealthzServer(false);
332
350
  }
333
351
  /**
334
352
  * Configures OpenAPI/Swagger documentation for the API.
@@ -718,11 +736,12 @@ var ExpressServer = class {
718
736
  gracefulShutdownRegistered = false;
719
737
  /** Map of registered signal listeners for clean teardown */
720
738
  signalListeners = /* @__PURE__ */ new Map();
721
- /**
722
- * Collection of registered health check functions.
723
- * These are executed when the health check endpoint is accessed.
724
- */
725
- healthChecks = [];
739
+ /** Running address info for the Healthz probe server */
740
+ healthzAddress;
741
+ /** Named checks queued for Healthz liveness probe */
742
+ healthzChecks = [];
743
+ /** Named checks queued for Healthz readiness probe */
744
+ healthzReadinessChecks = [];
726
745
  /** Promise that resolves when initialization (middleware + routes) is complete */
727
746
  initPromise;
728
747
  /** In-flight start promise to protect against concurrent start() calls */
@@ -757,8 +776,19 @@ var ExpressServer = class {
757
776
  getLogger().error(msg);
758
777
  throw new Error(msg);
759
778
  }
760
- if (config?.healthCheck?.checks) {
761
- this.healthChecks.push(...config.healthCheck.checks);
779
+ if (typeof this.config.healthzServer === "boolean") {
780
+ this.config.healthzServer = {
781
+ ...HealthzServer.getDefaultConfig(),
782
+ enable: this.config.healthzServer
783
+ };
784
+ }
785
+ if (this.config.healthzServer && typeof this.config.healthzServer === "object") {
786
+ if (this.config.healthzServer.checks) {
787
+ this.healthzChecks.push(...this.config.healthzServer.checks);
788
+ }
789
+ if (this.config.healthzServer.readinessChecks) {
790
+ this.healthzReadinessChecks.push(...this.config.healthzServer.readinessChecks);
791
+ }
762
792
  }
763
793
  this.globalPrefix = this.normalizePath(this.config.globalPrefix ?? "", false);
764
794
  this.hooks = hooks;
@@ -1106,10 +1136,6 @@ var ExpressServer = class {
1106
1136
  * 4. Error handler
1107
1137
  */
1108
1138
  async setupRoutes() {
1109
- const healthCheckPath = this.normalizePath(this.config.healthCheck?.path || "/healthz", this.config.healthCheck?.withGlobalPrefix);
1110
- this.app.get(healthCheckPath, async (_req, res) => {
1111
- return this.handleHealthCheckRequest(res);
1112
- });
1113
1139
  this.app.use(this.globalPrefix, this.rootRouter);
1114
1140
  await this.runHook("afterRoutes", this.app);
1115
1141
  this.app.use((req, res) => {
@@ -1133,60 +1159,25 @@ var ExpressServer = class {
1133
1159
  });
1134
1160
  }
1135
1161
  /**
1136
- * Execute health check and return response.
1162
+ * Whether the Healthz probe server is enabled.
1137
1163
  */
1138
- async handleHealthCheckRequest(res) {
1139
- try {
1140
- if (!this.healthChecks.length || getCatbeeServerGlobalConfig().skipHealthzChecksValidation) {
1141
- return res.status(HttpStatusCodes.OK).json(new SuccessResponse("OK"));
1142
- }
1143
- const results = await this.executeHealthChecks();
1144
- const allOk = results.every((r) => r.status);
1145
- const status = allOk ? HttpStatusCodes.OK : HttpStatusCodes.SERVICE_UNAVAILABLE;
1146
- const response = new SuccessResponse(allOk ? "OK" : "Service unavailable");
1147
- if (!allOk) response.error = true;
1148
- if (this.config.healthCheck?.detailed) response.data = {
1149
- checks: results
1150
- };
1151
- return res.status(status).json(response);
1152
- } catch {
1153
- return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json(new InternalServerErrorException("Health check failed"));
1164
+ isHealthzServerEnabled() {
1165
+ if (typeof this.config.healthzServer === "boolean") {
1166
+ return this.config.healthzServer;
1154
1167
  }
1168
+ return this.config.healthzServer?.enable === true;
1155
1169
  }
1156
1170
  /**
1157
- * Execute all registered health checks and return results.
1171
+ * Get the graceful shutdown delay in milliseconds configured for HealthzServer.
1158
1172
  */
1159
- async executeHealthChecks() {
1160
- const checkResults = await Promise.allSettled(this.healthChecks.map(async ({ name, check }) => {
1161
- try {
1162
- const status = await Promise.resolve(check());
1163
- return {
1164
- name,
1165
- status,
1166
- error: null
1167
- };
1168
- } catch (error) {
1169
- return {
1170
- name,
1171
- status: false,
1172
- error: error.message
1173
- };
1174
- }
1175
- }));
1176
- return checkResults.map((result) => {
1177
- if (result.status === "fulfilled") return result.value;
1178
- return {
1179
- name: "unknown",
1180
- status: false,
1181
- error: result.reason
1182
- };
1183
- });
1173
+ getHealthzShutdownDelay() {
1174
+ return typeof this.config.healthzServer === "object" ? this.config.healthzServer.shutdownDelayMs ?? 0 : 0;
1184
1175
  }
1185
1176
  /**
1186
- * Register a new health check function for monitoring service dependencies.
1177
+ * Register a new health check function for monitoring service dependencies on the Healthz probe server.
1187
1178
  *
1188
- * Health checks are executed when the health endpoint is accessed and
1189
- * help determine if the service is ready to handle requests.
1179
+ * By default, external dependencies (DB, Redis, etc.) are registered as `readiness` checks.
1180
+ * Can also be registered as `liveness` or `both`.
1190
1181
  *
1191
1182
  * Examples:
1192
1183
  * - Database connectivity
@@ -1195,33 +1186,61 @@ var ExpressServer = class {
1195
1186
  * - Memory/CPU usage checks
1196
1187
  *
1197
1188
  * @param name Unique identifier for the check (used in detailed responses)
1198
- * @param check Function returning boolean or Promise<boolean> indicating health
1189
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
1190
+ * @param options Target probe type ('readiness' | 'liveness' | 'both') or options object
1199
1191
  * @returns This instance for method chaining
1200
1192
  */
1201
- registerHealthCheck(name, check) {
1202
- this.healthChecks.push({
1193
+ registerHealthCheck(name, check, options) {
1194
+ const probeType = typeof options === "string" ? options : options?.type ?? "readiness";
1195
+ const namedCheck = {
1203
1196
  name,
1204
1197
  check
1205
- });
1198
+ };
1199
+ if (probeType === "liveness" || probeType === "both") {
1200
+ this.healthzChecks.push(namedCheck);
1201
+ }
1202
+ if (probeType === "readiness" || probeType === "both") {
1203
+ this.healthzReadinessChecks.push(namedCheck);
1204
+ }
1205
+ HealthzServer.registerCheck(namedCheck, probeType);
1206
1206
  return this;
1207
1207
  }
1208
1208
  /**
1209
- * Run registered health checks and return whether the service is ready.
1210
- * Useful for readiness probes in deployment tooling.
1209
+ * Mark the service as ready / not-ready for traffic on the Healthz probe server.
1211
1210
  *
1212
- * @returns Promise resolving to `true` when all checks pass, otherwise `false`.
1211
+ * @param ready Whether the service is ready to receive traffic
1212
+ * @returns This instance for method chaining
1213
1213
  */
1214
- async ready() {
1215
- try {
1216
- if (!this.healthChecks.length || getCatbeeServerGlobalConfig().skipHealthzChecksValidation) return true;
1217
- const results = await this.executeHealthChecks();
1218
- return results.every((r) => r.status === true);
1219
- } catch (err) {
1220
- getLogger().error({
1221
- err
1222
- }, "Error while running readiness checks");
1223
- return false;
1224
- }
1214
+ setReady(ready) {
1215
+ HealthzServer.setReady(ready);
1216
+ return this;
1217
+ }
1218
+ /**
1219
+ * Whether the service is currently marked as ready for traffic on the Healthz probe server.
1220
+ */
1221
+ isReady() {
1222
+ return HealthzServer.isReady();
1223
+ }
1224
+ /**
1225
+ * Get the running HealthzServer instance (if started).
1226
+ */
1227
+ getHealthzServer() {
1228
+ return HealthzServer.getInstance();
1229
+ }
1230
+ /**
1231
+ * Get the address info of the running HealthzServer (if started).
1232
+ */
1233
+ getHealthzAddress() {
1234
+ return this.healthzAddress;
1235
+ }
1236
+ /**
1237
+ * Returns whether the service is currently marked as ready for traffic on the Healthz probe server.
1238
+ * Useful for readiness checks in deployment tooling.
1239
+ *
1240
+ * @returns `true` when ready, otherwise `false`.
1241
+ */
1242
+ ready() {
1243
+ return HealthzServer.isReady();
1225
1244
  }
1226
1245
  /**
1227
1246
  * Get the underlying Express application instance.
@@ -1291,6 +1310,10 @@ var ExpressServer = class {
1291
1310
  try {
1292
1311
  server.removeAllListeners();
1293
1312
  server.close();
1313
+ if (HealthzServer.isStarted()) {
1314
+ HealthzServer.stop().catch(() => {
1315
+ });
1316
+ }
1294
1317
  } catch {
1295
1318
  }
1296
1319
  this.server = null;
@@ -1305,10 +1328,50 @@ var ExpressServer = class {
1305
1328
  this.setupConnectionTracking();
1306
1329
  Promise.resolve(this.runHook("onServerCreated", server)).then(() => {
1307
1330
  const onListening = /* @__PURE__ */ __name(async () => {
1308
- isListening = true;
1309
- this.logServerStartInfo();
1310
- await this.runHook("afterStart", server);
1311
- resolve(server);
1331
+ try {
1332
+ if (this.isHealthzServerEnabled()) {
1333
+ const healthzConfig = {
1334
+ ...typeof this.config.healthzServer === "object" ? this.config.healthzServer : {},
1335
+ handleSignals: false,
1336
+ checks: [
1337
+ ...this.healthzChecks
1338
+ ],
1339
+ readinessChecks: [
1340
+ ...this.healthzReadinessChecks
1341
+ ]
1342
+ };
1343
+ const addr = await HealthzServer.start(healthzConfig);
1344
+ if (!addr) {
1345
+ throw new Error("Healthz probe server failed to start (already running in this process)");
1346
+ }
1347
+ this.healthzAddress = addr;
1348
+ }
1349
+ this.logServerStartInfo();
1350
+ await this.runHook("afterStart", server);
1351
+ if (this.isHealthzServerEnabled()) {
1352
+ HealthzServer.setReady(true);
1353
+ }
1354
+ isListening = true;
1355
+ resolve(server);
1356
+ } catch (err) {
1357
+ const error = err instanceof Error ? err : new Error(String(err));
1358
+ getLogger().error({
1359
+ err: error
1360
+ }, "Server startup failed");
1361
+ if (HealthzServer.isStarted()) {
1362
+ await HealthzServer.stop().catch(() => {
1363
+ });
1364
+ }
1365
+ try {
1366
+ server.removeAllListeners();
1367
+ server.close();
1368
+ } catch {
1369
+ }
1370
+ this.server = null;
1371
+ this.healthzAddress = null;
1372
+ this.connections.clear();
1373
+ reject(error);
1374
+ }
1312
1375
  }, "onListening");
1313
1376
  const listenArgs = [
1314
1377
  this.config.port,
@@ -1359,8 +1422,8 @@ var ExpressServer = class {
1359
1422
  const host = this.formatHostForUrl(this.config.host || "localhost");
1360
1423
  const url = `${protocol}://${host}:${port}`;
1361
1424
  getLogger().info(`Server running on ${url}`);
1362
- if (this.config.healthCheck && this.config.healthCheck?.path) {
1363
- getLogger().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`);
1425
+ if (this.healthzAddress) {
1426
+ getLogger().info(`Healthz probe server running on http://${this.healthzAddress.address}:${this.healthzAddress.port}`);
1364
1427
  }
1365
1428
  if (this.config.openApi?.enable) {
1366
1429
  getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
@@ -1383,7 +1446,7 @@ var ExpressServer = class {
1383
1446
  * - Monitoring systems are notified
1384
1447
  */
1385
1448
  async stop(force = false) {
1386
- if (!this.server) {
1449
+ if (!this.server && !HealthzServer.isStarted()) {
1387
1450
  getLogger().warn("Stop called but server is not running");
1388
1451
  return;
1389
1452
  }
@@ -1392,10 +1455,26 @@ var ExpressServer = class {
1392
1455
  return;
1393
1456
  }
1394
1457
  this.isShuttingDown = true;
1395
- await this.runHook("beforeStop", this.server);
1458
+ if (HealthzServer.isStarted()) {
1459
+ HealthzServer.setReady(false);
1460
+ }
1461
+ if (this.server) {
1462
+ await this.runHook("beforeStop", this.server);
1463
+ }
1396
1464
  try {
1397
- await this.gracefulShutdown(force);
1465
+ const shutdownDelay = this.getHealthzShutdownDelay();
1466
+ if (shutdownDelay > 0 && !force && HealthzServer.isStarted()) {
1467
+ getLogger().info(`Waiting ${shutdownDelay}ms for load balancer to drain traffic...`);
1468
+ await new Promise((resolve) => setTimeout(resolve, shutdownDelay));
1469
+ }
1470
+ if (this.server) {
1471
+ await this.gracefulShutdown(force);
1472
+ }
1398
1473
  } finally {
1474
+ if (HealthzServer.isStarted()) {
1475
+ await HealthzServer.stop();
1476
+ this.healthzAddress = null;
1477
+ }
1399
1478
  this.isShuttingDown = false;
1400
1479
  }
1401
1480
  }
@@ -1484,7 +1563,7 @@ var ExpressServer = class {
1484
1563
  getLogger().info(`Received ${signal}, initiating graceful shutdown...`);
1485
1564
  try {
1486
1565
  this.disableGracefulShutdown();
1487
- await this.stop(true);
1566
+ await this.stop(false);
1488
1567
  process.exit(0);
1489
1568
  } catch (err) {
1490
1569
  getLogger().fatal({
package/types/index.d.ts CHANGED
@@ -157,6 +157,134 @@ type Without<T, U> = {
157
157
  [P in keyof T as T[P] extends U ? never : P]: T[P];
158
158
  };
159
159
 
160
+ /**
161
+ * A health check function that can be synchronous or asynchronous.
162
+ * Receives an AbortSignal that is triggered when `checkTimeoutMs` is exceeded.
163
+ *
164
+ * > **Cancellation Note**: Cancellation is cooperative. The check function must listen to
165
+ * > `signal.aborted` or pass `signal` to underlying asynchronous APIs (e.g. database drivers, `fetch`).
166
+ * > Synchronous CPU-bound loops or operations that ignore `signal` cannot be forcibly stopped by JavaScript.
167
+ *
168
+ * - Return `true` (or resolve to true) to signal healthy.
169
+ * - Return `false` (or resolve to false) to signal unhealthy.
170
+ * - Throw an error (or reject) to signal unhealthy with an error message.
171
+ */
172
+ type HealthCheckFn = (signal?: AbortSignal) => boolean | Promise<boolean>;
173
+ /**
174
+ * A readiness check function.
175
+ * Receives an AbortSignal that is triggered when `checkTimeoutMs` is exceeded.
176
+ *
177
+ * > **Cancellation Note**: Cancellation is cooperative. The check function must listen to
178
+ * > `signal.aborted` or pass `signal` to underlying asynchronous APIs (e.g. database drivers, `fetch`).
179
+ * > Synchronous CPU-bound loops or operations that ignore `signal` cannot be forcibly stopped by JavaScript.
180
+ *
181
+ * - Return `true` (or resolve to true) to signal ready.
182
+ * - Return `false` (or resolve to false) to signal not ready.
183
+ * - Throw an error (or reject) to signal not ready with an error.
184
+ */
185
+ type ReadinessCheckFn = (signal?: AbortSignal) => boolean | Promise<boolean>;
186
+ /**
187
+ * A named health check with an associated check function.
188
+ */
189
+ interface NamedCheck {
190
+ /** Human-readable name for this check (e.g. 'database', 'redis', 'disk') */
191
+ name: string;
192
+ /**
193
+ * Check function — return false or throw to indicate failure.
194
+ * Receives an AbortSignal that is triggered when the check times out.
195
+ * Cancellation via the signal is cooperative.
196
+ */
197
+ check: (signal?: AbortSignal) => boolean | Promise<boolean>;
198
+ }
199
+ /**
200
+ * Configuration for the standalone healthz HTTP server.
201
+ */
202
+ interface CatbeeHealthzServerConfig {
203
+ /** Hostname / IP to bind to
204
+ * - **default**: `'0.0.0.0'`
205
+ * - **env**: `HEALTHZ_HOST` (fallback: `SERVER_HOST`, `HOST`)
206
+ */
207
+ host?: string;
208
+ /** Port to listen on
209
+ * - **default**: `8282`
210
+ * - **env**: `HEALTHZ_PORT` (fallback: `SERVER_HEALTHZ_PORT`)
211
+ */
212
+ port?: number;
213
+ /** Liveness probe path — Kubernetes `livenessProbe.httpGet.path`
214
+ * - **default**: `'/healthz'`
215
+ * - **env**: `HEALTHZ_PATH` (fallback: `SERVER_HEALTH_CHECK_PATH`)
216
+ */
217
+ healthzPath?: string;
218
+ /** Readiness probe path — Kubernetes `readinessProbe.httpGet.path`
219
+ * - **default**: `'/readyz'`
220
+ * - **env**: `HEALTHZ_READYZ_PATH` (fallback: `SERVER_READYZ_PATH`)
221
+ */
222
+ readyzPath?: string;
223
+ /** Startup probe path — Kubernetes `startupProbe.httpGet.path`
224
+ * - **default**: `'/startupz'`
225
+ * - **env**: `HEALTHZ_STARTUPZ_PATH` (fallback: `SERVER_STARTUPZ_PATH`)
226
+ */
227
+ startupzPath?: string;
228
+ /** Include individual check results in the JSON response
229
+ * - **default**: `true`
230
+ * - **env**: `HEALTHZ_DETAILED` (fallback: `SERVER_HEALTH_CHECK_DETAILED_OUTPUT`)
231
+ */
232
+ detailed?: boolean;
233
+ /**
234
+ * Named checks to run on the liveness endpoint (`/healthz`).
235
+ *
236
+ * > **Kubernetes Best Practice**: Keep liveness checks very lightweight (e.g. process is responsive,
237
+ * > event loop not blocked). Avoid placing external dependencies (DB, Redis, downstream APIs) here;
238
+ * > if a shared dependency encounters transient downtime, failing liveness causes Kubernetes to restart
239
+ * > the container, risking cascading restart storms.
240
+ * >
241
+ * > Place external dependency checks in `readinessChecks` instead.
242
+ */
243
+ checks?: NamedCheck[];
244
+ /**
245
+ * Named checks to run on the readiness endpoint (`/readyz`).
246
+ *
247
+ * Use this for external dependencies (DB, Redis, cache, message broker).
248
+ * If a dependency goes down, Kubernetes will temporarily remove the pod from traffic rotation
249
+ * without killing/restarting the container, allowing it to recover cleanly.
250
+ *
251
+ * - If **omitted** (`undefined`): falls back to `checks`.
252
+ * - If **explicitly empty** (`[]`): no readiness checks are run (traffic gated purely by `setReady(true)`).
253
+ */
254
+ readinessChecks?: NamedCheck[];
255
+ /**
256
+ * Custom liveness check function. Runs *in addition to* `checks`.
257
+ * Return `false` or throw to indicate unhealthy.
258
+ */
259
+ onHealthCheck?: HealthCheckFn;
260
+ /**
261
+ * Custom readiness check function. Runs *in addition to* `readinessChecks`.
262
+ * Return `false` or throw to indicate not ready.
263
+ */
264
+ onReadinessCheck?: ReadinessCheckFn;
265
+ /** Timeout (ms) per individual check before it's considered failed
266
+ * - **default**: `5000`
267
+ * - **env**: `HEALTHZ_CHECK_TIMEOUT_MS`
268
+ */
269
+ checkTimeoutMs?: number;
270
+ /**
271
+ * Graceful shutdown delay in milliseconds.
272
+ * After receiving SIGTERM/SIGINT the server immediately flips readiness
273
+ * to `false` and waits this many ms before closing — giving the load
274
+ * balancer time to drain traffic.
275
+ * - **default**: `5000`
276
+ * - **env**: `HEALTHZ_SHUTDOWN_DELAY_MS`
277
+ */
278
+ shutdownDelayMs?: number;
279
+ /**
280
+ * Whether the HealthzServer should register its own SIGTERM/SIGINT process signal listeners.
281
+ * When managed by an orchestrating server (such as Catbee ExpressServer), set this to `false`
282
+ * to prevent signal listener conflicts and allow coordinated teardown.
283
+ * - **default**: `true`
284
+ */
285
+ handleSignals?: boolean;
286
+ }
287
+
160
288
  /**
161
289
  * Server configuration for Catbee HTTP/Express server.
162
290
  * Designed with secure and high-performance defaults for production use.
@@ -370,35 +498,19 @@ interface CatbeeServerConfig {
370
498
  */
371
499
  skipNotFoundRoutes?: boolean;
372
500
  };
373
- /** Health-check configuration
374
- * - **path**: `/healthz`
375
- * - **detailed**: `true`
376
- * - **withGlobalPrefix**: `false`
501
+ /** Standalone Healthz probe HTTP server configuration (for Kubernetes liveness, readiness, startup probes)
502
+ * - **default**: `false`
503
+ * - **env**: `SERVER_HEALTHZ_ENABLE` || `HEALTHZ_ENABLE`
504
+ *
505
+ * When enabled, ExpressServer automatically:
506
+ * - Starts `HealthzServer` on `server.start()`
507
+ * - Sets `HealthzServer.setReady(true)` after Express server is listening and ready
508
+ * - Sets `HealthzServer.setReady(false)` and gracefully drains/stops `HealthzServer` on `server.stop()`
509
+ * - Syncs health checks registered via `server.registerHealthCheck()` to `HealthzServer`
377
510
  */
378
- healthCheck?: {
379
- /** Health-check endpoint path
380
- * - **default**: `'/healthz'`
381
- * - **env**: `SERVER_HEALTH_CHECK_PATH`
382
- */
383
- path?: string;
384
- /** Include detailed check results in the response
385
- * - **default**: `true`
386
- * - **env**: `SERVER_HEALTH_CHECK_DETAILED_OUTPUT`
387
- */
388
- detailed?: boolean;
389
- /** Apply global route prefix
390
- * - **default**: `false`
391
- * - **env**: `SERVER_HEALTH_CHECK_WITH_GLOBAL_PREFIX`
392
- */
393
- withGlobalPrefix?: boolean;
394
- /** Custom health checks */
395
- checks?: Array<{
396
- /** Name of the health check */
397
- name: string;
398
- /** Check function that returns boolean or Promise<boolean> */
399
- check: () => Promise<boolean> | boolean;
400
- }>;
401
- };
511
+ healthzServer?: ToggleConfig<CatbeeHealthzServerConfig & {
512
+ enable?: boolean;
513
+ }>;
402
514
  /** Request timeout in ms
403
515
  * - **default**: `30000` (30 seconds)
404
516
  * - **env**: `SERVER_REQUEST_TIMEOUT_MS`
@@ -557,25 +669,8 @@ interface CatbeeServerHooks {
557
669
  /** Called before response is sent */
558
670
  onResponse?: (req: Request, res: Response, next: NextFunction) => void;
559
671
  }
560
- interface GlobalServerAddons {
561
- /**
562
- * Skip healthz endpoint even if health checks are configured
563
- * - **default**: `false`
564
- * - **env**: `SERVER_SKIP_HEALTHZ_CHECKS_VALIDATION`
565
- *
566
- * @additionalInfo
567
- * Set to true to return `200 OK` for `/healthz` without checks
568
- * Useful in environments where a simple liveness probe is needed
569
- * without performing actual health checks
570
- * Example: Kubernetes liveness probe
571
- * Note: This does not disable the health check functionality itself
572
- * Health checks can still be performed programmatically
573
- * or via other endpoints if needed
574
- */
575
- skipHealthzChecksValidation: boolean;
576
- }
577
672
  /** Combined global server configuration type */
578
- type CatbeeGlobalServerConfig = CatbeeServerConfig & GlobalServerAddons;
673
+ type CatbeeGlobalServerConfig = CatbeeServerConfig;
579
674
 
580
675
  /**
581
676
  * Generic API response format.
@@ -781,4 +876,4 @@ interface CatbeeConfig {
781
876
  }
782
877
 
783
878
  export { SortDirection };
784
- export type { ApiErrorResponse, ApiResponse, ApiSuccessResponse, AsyncOperationResponse, Awaited, BatchResponse, CatbeeConfig, CatbeeGlobalServerConfig, CatbeeServerConfig, CatbeeServerHooks, DeepPartial, DeepReadonly, DeepRequired, DeepStringifyOrNull, Func, GlobalServerAddons, IsEqual, KeysOfType, MaybePromise, Mutable, NonEmptyArray, Nullable, Optional, Optional2, Pagination, PaginationParams, PaginationResponse, PartialPick, PickByType, Primitive, RecordOptional, RequireAtLeastOne, StreamResponse, StringKeyedRecord, ToggleConfig, UnionToIntersection, ValueOf, WithPagination, Without, Writable };
879
+ export type { ApiErrorResponse, ApiResponse, ApiSuccessResponse, AsyncOperationResponse, Awaited, BatchResponse, CatbeeConfig, CatbeeGlobalServerConfig, CatbeeServerConfig, CatbeeServerHooks, DeepPartial, DeepReadonly, DeepRequired, DeepStringifyOrNull, Func, IsEqual, KeysOfType, MaybePromise, Mutable, NonEmptyArray, Nullable, Optional, Optional2, Pagination, PaginationParams, PaginationResponse, PartialPick, PickByType, Primitive, RecordOptional, RequireAtLeastOne, StreamResponse, StringKeyedRecord, ToggleConfig, UnionToIntersection, ValueOf, WithPagination, Without, Writable };