@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.cjs CHANGED
@@ -39,6 +39,7 @@ var fs = require('@catbee/utils/fs');
39
39
  var validation = require('@catbee/utils/validation');
40
40
  var async = require('@catbee/utils/async');
41
41
  var id = require('@catbee/utils/id');
42
+ var healthzServer = require('@catbee/utils/healthz-server');
42
43
 
43
44
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
44
45
 
@@ -307,36 +308,53 @@ var ServerConfigBuilder = class {
307
308
  return this.setEnabled("requestLogging", false);
308
309
  }
309
310
  /**
310
- * Configures server health check endpoint.
311
+ * Configures the dedicated Healthz probe HTTP server for Kubernetes.
311
312
  *
312
- * @param opts - Health check configuration options
313
+ * @param opts - Healthz server configuration options or boolean toggle
313
314
  * @returns The builder instance for chaining
314
- * @default - { path: '/healthz', detailed: true, withGlobalPrefix: false }
315
315
  *
316
316
  * @example
317
317
  * ```typescript
318
- * builder.withHealthCheck({
319
- * path: '/health',
320
- * detailed: true
318
+ * builder.withHealthzServer({
319
+ * port: 8282,
320
+ * shutdownDelayMs: 5000,
321
+ * readinessChecks: [
322
+ * { name: 'db', check: () => checkDb() }
323
+ * ]
321
324
  * })
322
325
  * ```
323
326
  */
324
- withHealthCheck(opts) {
325
- this.mergeConfig("healthCheck", opts);
327
+ withHealthzServer(opts) {
328
+ if (typeof opts === "boolean") {
329
+ this.config.healthzServer = opts;
330
+ } else {
331
+ const current = object.isPlainObject(this.config.healthzServer) ? object.deepClone(this.config.healthzServer) : {};
332
+ this.config.healthzServer = object.deepObjMerge({}, current, {
333
+ enable: true,
334
+ ...opts
335
+ });
336
+ }
326
337
  return this;
327
338
  }
328
339
  /**
329
- * Enables health check endpoint with default or custom settings
330
- * @param opts - Optional health check configuration
340
+ * Enables the dedicated Healthz probe HTTP server.
341
+ *
342
+ * @param opts - Optional Healthz server configuration options
331
343
  * @returns The builder instance for chaining
344
+ */
345
+ enableHealthzServer(opts = {}) {
346
+ return this.withHealthzServer({
347
+ ...opts,
348
+ enable: true
349
+ });
350
+ }
351
+ /**
352
+ * Disables the dedicated Healthz probe HTTP server.
332
353
  *
333
- * @example
334
- * ```typescript
335
- * builder.disableHealthCheck()
336
- * ```
354
+ * @returns The builder instance for chaining
337
355
  */
338
- disableHealthCheck() {
339
- return this.setEnabled("healthCheck", false);
356
+ disableHealthzServer() {
357
+ return this.withHealthzServer(false);
340
358
  }
341
359
  /**
342
360
  * Configures OpenAPI/Swagger documentation for the API.
@@ -726,11 +744,12 @@ var ExpressServer = class {
726
744
  gracefulShutdownRegistered = false;
727
745
  /** Map of registered signal listeners for clean teardown */
728
746
  signalListeners = /* @__PURE__ */ new Map();
729
- /**
730
- * Collection of registered health check functions.
731
- * These are executed when the health check endpoint is accessed.
732
- */
733
- healthChecks = [];
747
+ /** Running address info for the Healthz probe server */
748
+ healthzAddress;
749
+ /** Named checks queued for Healthz liveness probe */
750
+ healthzChecks = [];
751
+ /** Named checks queued for Healthz readiness probe */
752
+ healthzReadinessChecks = [];
734
753
  /** Promise that resolves when initialization (middleware + routes) is complete */
735
754
  initPromise;
736
755
  /** In-flight start promise to protect against concurrent start() calls */
@@ -765,8 +784,19 @@ var ExpressServer = class {
765
784
  logger.getLogger().error(msg);
766
785
  throw new Error(msg);
767
786
  }
768
- if (config$1?.healthCheck?.checks) {
769
- this.healthChecks.push(...config$1.healthCheck.checks);
787
+ if (typeof this.config.healthzServer === "boolean") {
788
+ this.config.healthzServer = {
789
+ ...healthzServer.HealthzServer.getDefaultConfig(),
790
+ enable: this.config.healthzServer
791
+ };
792
+ }
793
+ if (this.config.healthzServer && typeof this.config.healthzServer === "object") {
794
+ if (this.config.healthzServer.checks) {
795
+ this.healthzChecks.push(...this.config.healthzServer.checks);
796
+ }
797
+ if (this.config.healthzServer.readinessChecks) {
798
+ this.healthzReadinessChecks.push(...this.config.healthzServer.readinessChecks);
799
+ }
770
800
  }
771
801
  this.globalPrefix = this.normalizePath(this.config.globalPrefix ?? "", false);
772
802
  this.hooks = hooks;
@@ -1114,10 +1144,6 @@ var ExpressServer = class {
1114
1144
  * 4. Error handler
1115
1145
  */
1116
1146
  async setupRoutes() {
1117
- const healthCheckPath = this.normalizePath(this.config.healthCheck?.path || "/healthz", this.config.healthCheck?.withGlobalPrefix);
1118
- this.app.get(healthCheckPath, async (_req, res) => {
1119
- return this.handleHealthCheckRequest(res);
1120
- });
1121
1147
  this.app.use(this.globalPrefix, this.rootRouter);
1122
1148
  await this.runHook("afterRoutes", this.app);
1123
1149
  this.app.use((req, res) => {
@@ -1141,60 +1167,25 @@ var ExpressServer = class {
1141
1167
  });
1142
1168
  }
1143
1169
  /**
1144
- * Execute health check and return response.
1170
+ * Whether the Healthz probe server is enabled.
1145
1171
  */
1146
- async handleHealthCheckRequest(res) {
1147
- try {
1148
- if (!this.healthChecks.length || config.getCatbeeServerGlobalConfig().skipHealthzChecksValidation) {
1149
- return res.status(httpStatusCodes.HttpStatusCodes.OK).json(new response.SuccessResponse("OK"));
1150
- }
1151
- const results = await this.executeHealthChecks();
1152
- const allOk = results.every((r) => r.status);
1153
- const status = allOk ? httpStatusCodes.HttpStatusCodes.OK : httpStatusCodes.HttpStatusCodes.SERVICE_UNAVAILABLE;
1154
- const response$1 = new response.SuccessResponse(allOk ? "OK" : "Service unavailable");
1155
- if (!allOk) response$1.error = true;
1156
- if (this.config.healthCheck?.detailed) response$1.data = {
1157
- checks: results
1158
- };
1159
- return res.status(status).json(response$1);
1160
- } catch {
1161
- return res.status(httpStatusCodes.HttpStatusCodes.INTERNAL_SERVER_ERROR).json(new exception.InternalServerErrorException("Health check failed"));
1172
+ isHealthzServerEnabled() {
1173
+ if (typeof this.config.healthzServer === "boolean") {
1174
+ return this.config.healthzServer;
1162
1175
  }
1176
+ return this.config.healthzServer?.enable === true;
1163
1177
  }
1164
1178
  /**
1165
- * Execute all registered health checks and return results.
1179
+ * Get the graceful shutdown delay in milliseconds configured for HealthzServer.
1166
1180
  */
1167
- async executeHealthChecks() {
1168
- const checkResults = await Promise.allSettled(this.healthChecks.map(async ({ name, check }) => {
1169
- try {
1170
- const status = await Promise.resolve(check());
1171
- return {
1172
- name,
1173
- status,
1174
- error: null
1175
- };
1176
- } catch (error) {
1177
- return {
1178
- name,
1179
- status: false,
1180
- error: error.message
1181
- };
1182
- }
1183
- }));
1184
- return checkResults.map((result) => {
1185
- if (result.status === "fulfilled") return result.value;
1186
- return {
1187
- name: "unknown",
1188
- status: false,
1189
- error: result.reason
1190
- };
1191
- });
1181
+ getHealthzShutdownDelay() {
1182
+ return typeof this.config.healthzServer === "object" ? this.config.healthzServer.shutdownDelayMs ?? 0 : 0;
1192
1183
  }
1193
1184
  /**
1194
- * Register a new health check function for monitoring service dependencies.
1185
+ * Register a new health check function for monitoring service dependencies on the Healthz probe server.
1195
1186
  *
1196
- * Health checks are executed when the health endpoint is accessed and
1197
- * help determine if the service is ready to handle requests.
1187
+ * By default, external dependencies (DB, Redis, etc.) are registered as `readiness` checks.
1188
+ * Can also be registered as `liveness` or `both`.
1198
1189
  *
1199
1190
  * Examples:
1200
1191
  * - Database connectivity
@@ -1203,33 +1194,61 @@ var ExpressServer = class {
1203
1194
  * - Memory/CPU usage checks
1204
1195
  *
1205
1196
  * @param name Unique identifier for the check (used in detailed responses)
1206
- * @param check Function returning boolean or Promise<boolean> indicating health
1197
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
1198
+ * @param options Target probe type ('readiness' | 'liveness' | 'both') or options object
1207
1199
  * @returns This instance for method chaining
1208
1200
  */
1209
- registerHealthCheck(name, check) {
1210
- this.healthChecks.push({
1201
+ registerHealthCheck(name, check, options) {
1202
+ const probeType = typeof options === "string" ? options : options?.type ?? "readiness";
1203
+ const namedCheck = {
1211
1204
  name,
1212
1205
  check
1213
- });
1206
+ };
1207
+ if (probeType === "liveness" || probeType === "both") {
1208
+ this.healthzChecks.push(namedCheck);
1209
+ }
1210
+ if (probeType === "readiness" || probeType === "both") {
1211
+ this.healthzReadinessChecks.push(namedCheck);
1212
+ }
1213
+ healthzServer.HealthzServer.registerCheck(namedCheck, probeType);
1214
1214
  return this;
1215
1215
  }
1216
1216
  /**
1217
- * Run registered health checks and return whether the service is ready.
1218
- * Useful for readiness probes in deployment tooling.
1217
+ * Mark the service as ready / not-ready for traffic on the Healthz probe server.
1219
1218
  *
1220
- * @returns Promise resolving to `true` when all checks pass, otherwise `false`.
1219
+ * @param ready Whether the service is ready to receive traffic
1220
+ * @returns This instance for method chaining
1221
1221
  */
1222
- async ready() {
1223
- try {
1224
- if (!this.healthChecks.length || config.getCatbeeServerGlobalConfig().skipHealthzChecksValidation) return true;
1225
- const results = await this.executeHealthChecks();
1226
- return results.every((r) => r.status === true);
1227
- } catch (err) {
1228
- logger.getLogger().error({
1229
- err
1230
- }, "Error while running readiness checks");
1231
- return false;
1232
- }
1222
+ setReady(ready) {
1223
+ healthzServer.HealthzServer.setReady(ready);
1224
+ return this;
1225
+ }
1226
+ /**
1227
+ * Whether the service is currently marked as ready for traffic on the Healthz probe server.
1228
+ */
1229
+ isReady() {
1230
+ return healthzServer.HealthzServer.isReady();
1231
+ }
1232
+ /**
1233
+ * Get the running HealthzServer instance (if started).
1234
+ */
1235
+ getHealthzServer() {
1236
+ return healthzServer.HealthzServer.getInstance();
1237
+ }
1238
+ /**
1239
+ * Get the address info of the running HealthzServer (if started).
1240
+ */
1241
+ getHealthzAddress() {
1242
+ return this.healthzAddress;
1243
+ }
1244
+ /**
1245
+ * Returns whether the service is currently marked as ready for traffic on the Healthz probe server.
1246
+ * Useful for readiness checks in deployment tooling.
1247
+ *
1248
+ * @returns `true` when ready, otherwise `false`.
1249
+ */
1250
+ ready() {
1251
+ return healthzServer.HealthzServer.isReady();
1233
1252
  }
1234
1253
  /**
1235
1254
  * Get the underlying Express application instance.
@@ -1299,6 +1318,10 @@ var ExpressServer = class {
1299
1318
  try {
1300
1319
  server.removeAllListeners();
1301
1320
  server.close();
1321
+ if (healthzServer.HealthzServer.isStarted()) {
1322
+ healthzServer.HealthzServer.stop().catch(() => {
1323
+ });
1324
+ }
1302
1325
  } catch {
1303
1326
  }
1304
1327
  this.server = null;
@@ -1313,10 +1336,50 @@ var ExpressServer = class {
1313
1336
  this.setupConnectionTracking();
1314
1337
  Promise.resolve(this.runHook("onServerCreated", server)).then(() => {
1315
1338
  const onListening = /* @__PURE__ */ __name(async () => {
1316
- isListening = true;
1317
- this.logServerStartInfo();
1318
- await this.runHook("afterStart", server);
1319
- resolve(server);
1339
+ try {
1340
+ if (this.isHealthzServerEnabled()) {
1341
+ const healthzConfig = {
1342
+ ...typeof this.config.healthzServer === "object" ? this.config.healthzServer : {},
1343
+ handleSignals: false,
1344
+ checks: [
1345
+ ...this.healthzChecks
1346
+ ],
1347
+ readinessChecks: [
1348
+ ...this.healthzReadinessChecks
1349
+ ]
1350
+ };
1351
+ const addr = await healthzServer.HealthzServer.start(healthzConfig);
1352
+ if (!addr) {
1353
+ throw new Error("Healthz probe server failed to start (already running in this process)");
1354
+ }
1355
+ this.healthzAddress = addr;
1356
+ }
1357
+ this.logServerStartInfo();
1358
+ await this.runHook("afterStart", server);
1359
+ if (this.isHealthzServerEnabled()) {
1360
+ healthzServer.HealthzServer.setReady(true);
1361
+ }
1362
+ isListening = true;
1363
+ resolve(server);
1364
+ } catch (err) {
1365
+ const error = err instanceof Error ? err : new Error(String(err));
1366
+ logger.getLogger().error({
1367
+ err: error
1368
+ }, "Server startup failed");
1369
+ if (healthzServer.HealthzServer.isStarted()) {
1370
+ await healthzServer.HealthzServer.stop().catch(() => {
1371
+ });
1372
+ }
1373
+ try {
1374
+ server.removeAllListeners();
1375
+ server.close();
1376
+ } catch {
1377
+ }
1378
+ this.server = null;
1379
+ this.healthzAddress = null;
1380
+ this.connections.clear();
1381
+ reject(error);
1382
+ }
1320
1383
  }, "onListening");
1321
1384
  const listenArgs = [
1322
1385
  this.config.port,
@@ -1367,8 +1430,8 @@ var ExpressServer = class {
1367
1430
  const host = this.formatHostForUrl(this.config.host || "localhost");
1368
1431
  const url = `${protocol}://${host}:${port}`;
1369
1432
  logger.getLogger().info(`Server running on ${url}`);
1370
- if (this.config.healthCheck && this.config.healthCheck?.path) {
1371
- logger.getLogger().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`);
1433
+ if (this.healthzAddress) {
1434
+ logger.getLogger().info(`Healthz probe server running on http://${this.healthzAddress.address}:${this.healthzAddress.port}`);
1372
1435
  }
1373
1436
  if (this.config.openApi?.enable) {
1374
1437
  logger.getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
@@ -1391,7 +1454,7 @@ var ExpressServer = class {
1391
1454
  * - Monitoring systems are notified
1392
1455
  */
1393
1456
  async stop(force = false) {
1394
- if (!this.server) {
1457
+ if (!this.server && !healthzServer.HealthzServer.isStarted()) {
1395
1458
  logger.getLogger().warn("Stop called but server is not running");
1396
1459
  return;
1397
1460
  }
@@ -1400,10 +1463,26 @@ var ExpressServer = class {
1400
1463
  return;
1401
1464
  }
1402
1465
  this.isShuttingDown = true;
1403
- await this.runHook("beforeStop", this.server);
1466
+ if (healthzServer.HealthzServer.isStarted()) {
1467
+ healthzServer.HealthzServer.setReady(false);
1468
+ }
1469
+ if (this.server) {
1470
+ await this.runHook("beforeStop", this.server);
1471
+ }
1404
1472
  try {
1405
- await this.gracefulShutdown(force);
1473
+ const shutdownDelay = this.getHealthzShutdownDelay();
1474
+ if (shutdownDelay > 0 && !force && healthzServer.HealthzServer.isStarted()) {
1475
+ logger.getLogger().info(`Waiting ${shutdownDelay}ms for load balancer to drain traffic...`);
1476
+ await new Promise((resolve) => setTimeout(resolve, shutdownDelay));
1477
+ }
1478
+ if (this.server) {
1479
+ await this.gracefulShutdown(force);
1480
+ }
1406
1481
  } finally {
1482
+ if (healthzServer.HealthzServer.isStarted()) {
1483
+ await healthzServer.HealthzServer.stop();
1484
+ this.healthzAddress = null;
1485
+ }
1407
1486
  this.isShuttingDown = false;
1408
1487
  }
1409
1488
  }
@@ -1492,7 +1571,7 @@ var ExpressServer = class {
1492
1571
  logger.getLogger().info(`Received ${signal}, initiating graceful shutdown...`);
1493
1572
  try {
1494
1573
  this.disableGracefulShutdown();
1495
- await this.stop(true);
1574
+ await this.stop(false);
1496
1575
  process.exit(0);
1497
1576
  } catch (err) {
1498
1577
  logger.getLogger().fatal({
package/server/index.d.ts CHANGED
@@ -26,6 +26,7 @@ import express, { Express, Router } from 'express';
26
26
  import http from 'node:http';
27
27
  import https from 'node:https';
28
28
  import { CatbeeServerConfig, CatbeeServerHooks } from '@catbee/utils/types';
29
+ import { HealthzServer, HealthzAddressInfo, CatbeeHealthzServerConfig } from '@catbee/utils/healthz-server';
29
30
 
30
31
  /**
31
32
  * Map of critical dependencies to their error messages.
@@ -76,11 +77,12 @@ declare class ExpressServer {
76
77
  private gracefulShutdownRegistered;
77
78
  /** Map of registered signal listeners for clean teardown */
78
79
  private readonly signalListeners;
79
- /**
80
- * Collection of registered health check functions.
81
- * These are executed when the health check endpoint is accessed.
82
- */
83
- private readonly healthChecks;
80
+ /** Running address info for the Healthz probe server */
81
+ private healthzAddress?;
82
+ /** Named checks queued for Healthz liveness probe */
83
+ private readonly healthzChecks;
84
+ /** Named checks queued for Healthz readiness probe */
85
+ private readonly healthzReadinessChecks;
84
86
  /** Promise that resolves when initialization (middleware + routes) is complete */
85
87
  private readonly initPromise;
86
88
  /** In-flight start promise to protect against concurrent start() calls */
@@ -197,18 +199,18 @@ declare class ExpressServer {
197
199
  */
198
200
  protected setupRoutes(): Promise<void>;
199
201
  /**
200
- * Execute health check and return response.
202
+ * Whether the Healthz probe server is enabled.
201
203
  */
202
- private handleHealthCheckRequest;
204
+ isHealthzServerEnabled(): boolean;
203
205
  /**
204
- * Execute all registered health checks and return results.
206
+ * Get the graceful shutdown delay in milliseconds configured for HealthzServer.
205
207
  */
206
- private executeHealthChecks;
208
+ private getHealthzShutdownDelay;
207
209
  /**
208
- * Register a new health check function for monitoring service dependencies.
210
+ * Register a new health check function for monitoring service dependencies on the Healthz probe server.
209
211
  *
210
- * Health checks are executed when the health endpoint is accessed and
211
- * help determine if the service is ready to handle requests.
212
+ * By default, external dependencies (DB, Redis, etc.) are registered as `readiness` checks.
213
+ * Can also be registered as `liveness` or `both`.
212
214
  *
213
215
  * Examples:
214
216
  * - Database connectivity
@@ -217,17 +219,39 @@ declare class ExpressServer {
217
219
  * - Memory/CPU usage checks
218
220
  *
219
221
  * @param name Unique identifier for the check (used in detailed responses)
220
- * @param check Function returning boolean or Promise<boolean> indicating health
222
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
223
+ * @param options Target probe type ('readiness' | 'liveness' | 'both') or options object
224
+ * @returns This instance for method chaining
225
+ */
226
+ registerHealthCheck(name: string, check: (signal?: AbortSignal) => Promise<boolean> | boolean, options?: 'readiness' | 'liveness' | 'both' | {
227
+ type?: 'readiness' | 'liveness' | 'both';
228
+ }): this;
229
+ /**
230
+ * Mark the service as ready / not-ready for traffic on the Healthz probe server.
231
+ *
232
+ * @param ready Whether the service is ready to receive traffic
221
233
  * @returns This instance for method chaining
222
234
  */
223
- registerHealthCheck(name: string, check: () => Promise<boolean> | boolean): this;
235
+ setReady(ready: boolean): this;
236
+ /**
237
+ * Whether the service is currently marked as ready for traffic on the Healthz probe server.
238
+ */
239
+ isReady(): boolean;
240
+ /**
241
+ * Get the running HealthzServer instance (if started).
242
+ */
243
+ getHealthzServer(): HealthzServer | undefined;
244
+ /**
245
+ * Get the address info of the running HealthzServer (if started).
246
+ */
247
+ getHealthzAddress(): HealthzAddressInfo | null | undefined;
224
248
  /**
225
- * Run registered health checks and return whether the service is ready.
226
- * Useful for readiness probes in deployment tooling.
249
+ * Returns whether the service is currently marked as ready for traffic on the Healthz probe server.
250
+ * Useful for readiness checks in deployment tooling.
227
251
  *
228
- * @returns Promise resolving to `true` when all checks pass, otherwise `false`.
252
+ * @returns `true` when ready, otherwise `false`.
229
253
  */
230
- ready(): Promise<boolean>;
254
+ ready(): boolean;
231
255
  /**
232
256
  * Get the underlying Express application instance.
233
257
  * Use this for advanced Express features not exposed by this wrapper.
@@ -715,32 +739,36 @@ declare class ServerConfigBuilder {
715
739
  */
716
740
  disableRequestLogging(): this;
717
741
  /**
718
- * Configures server health check endpoint.
742
+ * Configures the dedicated Healthz probe HTTP server for Kubernetes.
719
743
  *
720
- * @param opts - Health check configuration options
744
+ * @param opts - Healthz server configuration options or boolean toggle
721
745
  * @returns The builder instance for chaining
722
- * @default - { path: '/healthz', detailed: true, withGlobalPrefix: false }
723
746
  *
724
747
  * @example
725
748
  * ```typescript
726
- * builder.withHealthCheck({
727
- * path: '/health',
728
- * detailed: true
749
+ * builder.withHealthzServer({
750
+ * port: 8282,
751
+ * shutdownDelayMs: 5000,
752
+ * readinessChecks: [
753
+ * { name: 'db', check: () => checkDb() }
754
+ * ]
729
755
  * })
730
756
  * ```
731
757
  */
732
- withHealthCheck(opts: Partial<NonNullable<CatbeeServerConfig['healthCheck']>>): this;
758
+ withHealthzServer(opts: NonNullable<CatbeeServerConfig['healthzServer']>): this;
733
759
  /**
734
- * Enables health check endpoint with default or custom settings
735
- * @param opts - Optional health check configuration
760
+ * Enables the dedicated Healthz probe HTTP server.
761
+ *
762
+ * @param opts - Optional Healthz server configuration options
736
763
  * @returns The builder instance for chaining
764
+ */
765
+ enableHealthzServer(opts?: Partial<CatbeeHealthzServerConfig>): this;
766
+ /**
767
+ * Disables the dedicated Healthz probe HTTP server.
737
768
  *
738
- * @example
739
- * ```typescript
740
- * builder.disableHealthCheck()
741
- * ```
769
+ * @returns The builder instance for chaining
742
770
  */
743
- disableHealthCheck(): this;
771
+ disableHealthzServer(): this;
744
772
  /**
745
773
  * Configures OpenAPI/Swagger documentation for the API.
746
774
  *