@catbee/utils 2.2.1 → 2.3.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 string = require('@catbee/utils/string');
42
43
  var healthzServer = require('@catbee/utils/healthz-server');
43
44
 
44
45
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -738,8 +739,12 @@ var ExpressServer = class {
738
739
  app;
739
740
  /** Set of active WebSocket connections */
740
741
  connections = /* @__PURE__ */ new Set();
741
- /** Flag indicating if the server is shutting down */
742
+ /** Flag indicating if the server is shutting down (rejects traffic with 503) */
742
743
  isShuttingDown = false;
744
+ /** Flag indicating if the server is in graceful drain delay (sets Connection: close while servicing traffic) */
745
+ isDraining = false;
746
+ /** Internal readiness state (used when HealthzServer is not enabled) */
747
+ internalReady = false;
743
748
  /** Flag indicating if graceful shutdown handlers are registered */
744
749
  gracefulShutdownRegistered = false;
745
750
  /** Map of registered signal listeners for clean teardown */
@@ -750,10 +755,14 @@ var ExpressServer = class {
750
755
  healthzChecks = [];
751
756
  /** Named checks queued for Healthz readiness probe */
752
757
  healthzReadinessChecks = [];
758
+ /** Whether readiness checks were explicitly provided or registered */
759
+ hasExplicitReadinessChecks = false;
753
760
  /** Promise that resolves when initialization (middleware + routes) is complete */
754
761
  initPromise;
755
762
  /** In-flight start promise to protect against concurrent start() calls */
756
763
  startPromise;
764
+ /** In-flight stop promise to protect against concurrent stop() calls */
765
+ stopPromise;
757
766
  /**
758
767
  * Initializes server with intelligent defaults and security best practices.
759
768
  * All settings can be customized via config and hooks.
@@ -794,7 +803,8 @@ var ExpressServer = class {
794
803
  if (this.config.healthzServer.checks) {
795
804
  this.healthzChecks.push(...this.config.healthzServer.checks);
796
805
  }
797
- if (this.config.healthzServer.readinessChecks) {
806
+ if (this.config.healthzServer.readinessChecks !== void 0) {
807
+ this.hasExplicitReadinessChecks = true;
798
808
  this.healthzReadinessChecks.push(...this.config.healthzServer.readinessChecks);
799
809
  }
800
810
  }
@@ -892,6 +902,9 @@ var ExpressServer = class {
892
902
  res.status(httpStatusCodes.HttpStatusCodes.SERVICE_UNAVAILABLE).json(new exception.ServiceUnavailableException("Server is shutting down"));
893
903
  return;
894
904
  }
905
+ if (this.isDraining) {
906
+ res.setHeader("Connection", "close");
907
+ }
895
908
  next();
896
909
  });
897
910
  }
@@ -1205,21 +1218,96 @@ var ExpressServer = class {
1205
1218
  check
1206
1219
  };
1207
1220
  if (probeType === "liveness" || probeType === "both") {
1208
- this.healthzChecks.push(namedCheck);
1221
+ const idx = this.healthzChecks.findIndex((c) => c.name === name);
1222
+ if (idx !== -1) {
1223
+ this.healthzChecks[idx] = namedCheck;
1224
+ } else {
1225
+ this.healthzChecks.push(namedCheck);
1226
+ }
1209
1227
  }
1210
1228
  if (probeType === "readiness" || probeType === "both") {
1211
- this.healthzReadinessChecks.push(namedCheck);
1229
+ this.hasExplicitReadinessChecks = true;
1230
+ const idx = this.healthzReadinessChecks.findIndex((c) => c.name === name);
1231
+ if (idx !== -1) {
1232
+ this.healthzReadinessChecks[idx] = namedCheck;
1233
+ } else {
1234
+ this.healthzReadinessChecks.push(namedCheck);
1235
+ }
1212
1236
  }
1213
1237
  healthzServer.HealthzServer.registerCheck(namedCheck, probeType);
1214
1238
  return this;
1215
1239
  }
1216
1240
  /**
1241
+ * Unregister a health check by name.
1242
+ *
1243
+ * @param name Name of the check to remove
1244
+ * @param options Target probe type ('readiness' | 'liveness' | 'both') or options object
1245
+ * @returns This instance for method chaining
1246
+ */
1247
+ unregisterHealthCheck(name, options) {
1248
+ const probeType = typeof options === "string" ? options : options?.type ?? "both";
1249
+ if (probeType === "liveness" || probeType === "both") {
1250
+ const idx = this.healthzChecks.findIndex((c) => c.name === name);
1251
+ if (idx !== -1) this.healthzChecks.splice(idx, 1);
1252
+ }
1253
+ if (probeType === "readiness" || probeType === "both") {
1254
+ const idx = this.healthzReadinessChecks.findIndex((c) => c.name === name);
1255
+ if (idx !== -1) this.healthzReadinessChecks.splice(idx, 1);
1256
+ }
1257
+ healthzServer.HealthzServer.unregisterCheck(name, probeType);
1258
+ return this;
1259
+ }
1260
+ /**
1261
+ * Get all registered Healthz checks queued for this Express server.
1262
+ */
1263
+ getHealthzChecks() {
1264
+ return {
1265
+ liveness: [
1266
+ ...this.healthzChecks
1267
+ ],
1268
+ readiness: [
1269
+ ...this.healthzReadinessChecks
1270
+ ]
1271
+ };
1272
+ }
1273
+ /**
1274
+ * Register a readiness health check.
1275
+ *
1276
+ * @param name Unique identifier for the check (used in detailed responses)
1277
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
1278
+ * @returns This instance for method chaining
1279
+ */
1280
+ registerReadinessCheck(name, check) {
1281
+ return this.registerHealthCheck(name, check, "readiness");
1282
+ }
1283
+ /**
1284
+ * Register a liveness health check.
1285
+ *
1286
+ * @param name Unique identifier for the check (used in detailed responses)
1287
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
1288
+ * @returns This instance for method chaining
1289
+ */
1290
+ registerLivenessCheck(name, check) {
1291
+ return this.registerHealthCheck(name, check, "liveness");
1292
+ }
1293
+ /**
1294
+ * Register a readiness and liveness health check.
1295
+ *
1296
+ * @param name Unique identifier for the check (used in detailed responses)
1297
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
1298
+ * @returns This instance for method chaining
1299
+ */
1300
+ registerReadinessAndLivenessChecks(name, check) {
1301
+ return this.registerHealthCheck(name, check, "both");
1302
+ }
1303
+ /**
1217
1304
  * Mark the service as ready / not-ready for traffic on the Healthz probe server.
1218
1305
  *
1219
1306
  * @param ready Whether the service is ready to receive traffic
1220
1307
  * @returns This instance for method chaining
1221
1308
  */
1222
1309
  setReady(ready) {
1310
+ this.internalReady = ready;
1223
1311
  healthzServer.HealthzServer.setReady(ready);
1224
1312
  return this;
1225
1313
  }
@@ -1227,7 +1315,10 @@ var ExpressServer = class {
1227
1315
  * Whether the service is currently marked as ready for traffic on the Healthz probe server.
1228
1316
  */
1229
1317
  isReady() {
1230
- return healthzServer.HealthzServer.isReady();
1318
+ if (this.isHealthzServerEnabled()) {
1319
+ return healthzServer.HealthzServer.isReady();
1320
+ }
1321
+ return this.internalReady && this.isRunning();
1231
1322
  }
1232
1323
  /**
1233
1324
  * Mark application startup as completed on the Healthz probe server (switches `/startupz` to 200).
@@ -1242,7 +1333,10 @@ var ExpressServer = class {
1242
1333
  * Whether application startup has completed on the Healthz probe server.
1243
1334
  */
1244
1335
  isStartupComplete() {
1245
- return healthzServer.HealthzServer.isStartupComplete();
1336
+ if (this.isHealthzServerEnabled()) {
1337
+ return healthzServer.HealthzServer.isStartupComplete();
1338
+ }
1339
+ return this.isRunning();
1246
1340
  }
1247
1341
  /**
1248
1342
  * Get the running HealthzServer instance (if started).
@@ -1263,7 +1357,7 @@ var ExpressServer = class {
1263
1357
  * @returns `true` when ready, otherwise `false`.
1264
1358
  */
1265
1359
  ready() {
1266
- return healthzServer.HealthzServer.isReady();
1360
+ return this.isReady();
1267
1361
  }
1268
1362
  /**
1269
1363
  * Get the underlying Express application instance.
@@ -1327,9 +1421,11 @@ var ExpressServer = class {
1327
1421
  checks: [
1328
1422
  ...this.healthzChecks
1329
1423
  ],
1330
- readinessChecks: [
1331
- ...this.healthzReadinessChecks
1332
- ]
1424
+ ...this.hasExplicitReadinessChecks ? {
1425
+ readinessChecks: [
1426
+ ...this.healthzReadinessChecks
1427
+ ]
1428
+ } : {}
1333
1429
  };
1334
1430
  const addr = await healthzServer.HealthzServer.start(healthzConfig);
1335
1431
  if (!addr) {
@@ -1373,6 +1469,7 @@ var ExpressServer = class {
1373
1469
  try {
1374
1470
  this.logServerStartInfo();
1375
1471
  await this.runHook("afterStart", server);
1472
+ this.internalReady = true;
1376
1473
  if (this.isHealthzServerEnabled()) {
1377
1474
  healthzServer.HealthzServer.markStartupComplete();
1378
1475
  healthzServer.HealthzServer.setReady(true);
@@ -1459,7 +1556,8 @@ var ExpressServer = class {
1459
1556
  const url = `${protocol}://${host}:${port}`;
1460
1557
  logger.getLogger().info(`Server running on ${url}`);
1461
1558
  if (this.healthzAddress) {
1462
- logger.getLogger().info(`Healthz probe server running on http://${this.healthzAddress.address}:${this.healthzAddress.port}`);
1559
+ const healthzHost = this.formatHostForUrl(this.healthzAddress.address);
1560
+ logger.getLogger().info(`Healthz server running on http://${healthzHost}:${this.healthzAddress.port}`);
1463
1561
  }
1464
1562
  if (this.config.openApi?.enable) {
1465
1563
  logger.getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
@@ -1486,11 +1584,21 @@ var ExpressServer = class {
1486
1584
  logger.getLogger().warn("Stop called but server is not running");
1487
1585
  return;
1488
1586
  }
1489
- if (this.isShuttingDown) {
1490
- logger.getLogger().warn("Shutdown already in progress");
1491
- return;
1587
+ if (this.stopPromise) {
1588
+ return this.stopPromise;
1589
+ }
1590
+ this.stopPromise = this.doStop(force);
1591
+ try {
1592
+ await this.stopPromise;
1593
+ } finally {
1594
+ this.stopPromise = void 0;
1492
1595
  }
1493
- this.isShuttingDown = true;
1596
+ }
1597
+ /**
1598
+ * Internal implementation of server shutdown.
1599
+ */
1600
+ async doStop(force = false) {
1601
+ this.internalReady = false;
1494
1602
  if (healthzServer.HealthzServer.isRunning()) {
1495
1603
  healthzServer.HealthzServer.setReady(false);
1496
1604
  }
@@ -1501,8 +1609,11 @@ var ExpressServer = class {
1501
1609
  const shutdownDelay = this.getHealthzShutdownDelay();
1502
1610
  if (shutdownDelay > 0 && !force && healthzServer.HealthzServer.isRunning()) {
1503
1611
  logger.getLogger().info(`Waiting ${shutdownDelay}ms for load balancer to drain traffic...`);
1612
+ this.isDraining = true;
1504
1613
  await new Promise((resolve) => setTimeout(resolve, shutdownDelay));
1505
1614
  }
1615
+ this.isDraining = false;
1616
+ this.isShuttingDown = true;
1506
1617
  if (this.server) {
1507
1618
  await this.gracefulShutdown(force);
1508
1619
  }
@@ -1512,6 +1623,7 @@ var ExpressServer = class {
1512
1623
  this.healthzAddress = null;
1513
1624
  }
1514
1625
  this.isShuttingDown = false;
1626
+ this.isDraining = false;
1515
1627
  }
1516
1628
  }
1517
1629
  /**
@@ -1598,10 +1710,11 @@ var ExpressServer = class {
1598
1710
  signalHandled = true;
1599
1711
  logger.getLogger().info(`Received ${signal}, initiating graceful shutdown...`);
1600
1712
  try {
1601
- this.disableGracefulShutdown();
1602
1713
  await this.stop(false);
1714
+ this.disableGracefulShutdown();
1603
1715
  process.exit(0);
1604
1716
  } catch (err) {
1717
+ this.disableGracefulShutdown();
1605
1718
  logger.getLogger().fatal({
1606
1719
  err
1607
1720
  }, "Shutdown failed");
@@ -1959,7 +2072,8 @@ var ExpressServer = class {
1959
2072
  }
1960
2073
  normalizePath(path, withGlobalPrefix = false) {
1961
2074
  const sanitize = /* @__PURE__ */ __name((p) => {
1962
- return "/" + p.trim().replace(/^\/+/, "").replace(/\/{2,}/g, "/").replace(/\/+$/, "");
2075
+ const inner = string.trimChars(p.trim(), "/");
2076
+ return inner ? "/" + inner.replace(/\/{2,}/g, "/") : "/";
1963
2077
  }, "sanitize");
1964
2078
  const prefix = withGlobalPrefix && this.globalPrefix ? sanitize(this.globalPrefix) : "";
1965
2079
  if (typeof path !== "string" || !path.trim()) {
package/server/index.d.ts CHANGED
@@ -26,7 +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
+ import { NamedCheck, HealthzServer, HealthzAddressInfo, CatbeeHealthzServerConfig } from '@catbee/utils/healthz-server';
30
30
 
31
31
  /**
32
32
  * Map of critical dependencies to their error messages.
@@ -71,8 +71,12 @@ declare class ExpressServer {
71
71
  private readonly app;
72
72
  /** Set of active WebSocket connections */
73
73
  private readonly connections;
74
- /** Flag indicating if the server is shutting down */
74
+ /** Flag indicating if the server is shutting down (rejects traffic with 503) */
75
75
  private isShuttingDown;
76
+ /** Flag indicating if the server is in graceful drain delay (sets Connection: close while servicing traffic) */
77
+ private isDraining;
78
+ /** Internal readiness state (used when HealthzServer is not enabled) */
79
+ private internalReady;
76
80
  /** Flag indicating if graceful shutdown handlers are registered */
77
81
  private gracefulShutdownRegistered;
78
82
  /** Map of registered signal listeners for clean teardown */
@@ -83,10 +87,14 @@ declare class ExpressServer {
83
87
  private readonly healthzChecks;
84
88
  /** Named checks queued for Healthz readiness probe */
85
89
  private readonly healthzReadinessChecks;
90
+ /** Whether readiness checks were explicitly provided or registered */
91
+ private hasExplicitReadinessChecks;
86
92
  /** Promise that resolves when initialization (middleware + routes) is complete */
87
93
  private readonly initPromise;
88
94
  /** In-flight start promise to protect against concurrent start() calls */
89
95
  private startPromise?;
96
+ /** In-flight stop promise to protect against concurrent stop() calls */
97
+ private stopPromise?;
90
98
  /**
91
99
  * Initializes server with intelligent defaults and security best practices.
92
100
  * All settings can be customized via config and hooks.
@@ -223,9 +231,50 @@ declare class ExpressServer {
223
231
  * @param options Target probe type ('readiness' | 'liveness' | 'both') or options object
224
232
  * @returns This instance for method chaining
225
233
  */
226
- registerHealthCheck(name: string, check: (signal?: AbortSignal) => Promise<boolean> | boolean, options?: 'readiness' | 'liveness' | 'both' | {
234
+ registerHealthCheck(name: string, check: (signal?: AbortSignal) => Promise<boolean | void> | boolean | void, options?: 'readiness' | 'liveness' | 'both' | {
227
235
  type?: 'readiness' | 'liveness' | 'both';
228
236
  }): this;
237
+ /**
238
+ * Unregister a health check by name.
239
+ *
240
+ * @param name Name of the check to remove
241
+ * @param options Target probe type ('readiness' | 'liveness' | 'both') or options object
242
+ * @returns This instance for method chaining
243
+ */
244
+ unregisterHealthCheck(name: string, options?: 'readiness' | 'liveness' | 'both' | {
245
+ type?: 'readiness' | 'liveness' | 'both';
246
+ }): this;
247
+ /**
248
+ * Get all registered Healthz checks queued for this Express server.
249
+ */
250
+ getHealthzChecks(): {
251
+ liveness: NamedCheck[];
252
+ readiness: NamedCheck[];
253
+ };
254
+ /**
255
+ * Register a readiness health check.
256
+ *
257
+ * @param name Unique identifier for the check (used in detailed responses)
258
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
259
+ * @returns This instance for method chaining
260
+ */
261
+ registerReadinessCheck(name: string, check: (signal?: AbortSignal) => Promise<boolean | void> | boolean | void): this;
262
+ /**
263
+ * Register a liveness health check.
264
+ *
265
+ * @param name Unique identifier for the check (used in detailed responses)
266
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
267
+ * @returns This instance for method chaining
268
+ */
269
+ registerLivenessCheck(name: string, check: (signal?: AbortSignal) => Promise<boolean | void> | boolean | void): this;
270
+ /**
271
+ * Register a readiness and liveness health check.
272
+ *
273
+ * @param name Unique identifier for the check (used in detailed responses)
274
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
275
+ * @returns This instance for method chaining
276
+ */
277
+ registerReadinessAndLivenessChecks(name: string, check: (signal?: AbortSignal) => Promise<boolean | void> | boolean | void): this;
229
278
  /**
230
279
  * Mark the service as ready / not-ready for traffic on the Healthz probe server.
231
280
  *
@@ -327,6 +376,10 @@ declare class ExpressServer {
327
376
  * - Monitoring systems are notified
328
377
  */
329
378
  stop(force?: boolean): Promise<void>;
379
+ /**
380
+ * Internal implementation of server shutdown.
381
+ */
382
+ private doStop;
330
383
  /**
331
384
  * Perform graceful server shutdown with timeout.
332
385
  */
package/server/index.mjs CHANGED
@@ -37,6 +37,7 @@ 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 { trimChars } from '@catbee/utils/string';
40
41
  import { HealthzServer } from '@catbee/utils/healthz-server';
41
42
 
42
43
  var __defProp = Object.defineProperty;
@@ -730,8 +731,12 @@ var ExpressServer = class {
730
731
  app;
731
732
  /** Set of active WebSocket connections */
732
733
  connections = /* @__PURE__ */ new Set();
733
- /** Flag indicating if the server is shutting down */
734
+ /** Flag indicating if the server is shutting down (rejects traffic with 503) */
734
735
  isShuttingDown = false;
736
+ /** Flag indicating if the server is in graceful drain delay (sets Connection: close while servicing traffic) */
737
+ isDraining = false;
738
+ /** Internal readiness state (used when HealthzServer is not enabled) */
739
+ internalReady = false;
735
740
  /** Flag indicating if graceful shutdown handlers are registered */
736
741
  gracefulShutdownRegistered = false;
737
742
  /** Map of registered signal listeners for clean teardown */
@@ -742,10 +747,14 @@ var ExpressServer = class {
742
747
  healthzChecks = [];
743
748
  /** Named checks queued for Healthz readiness probe */
744
749
  healthzReadinessChecks = [];
750
+ /** Whether readiness checks were explicitly provided or registered */
751
+ hasExplicitReadinessChecks = false;
745
752
  /** Promise that resolves when initialization (middleware + routes) is complete */
746
753
  initPromise;
747
754
  /** In-flight start promise to protect against concurrent start() calls */
748
755
  startPromise;
756
+ /** In-flight stop promise to protect against concurrent stop() calls */
757
+ stopPromise;
749
758
  /**
750
759
  * Initializes server with intelligent defaults and security best practices.
751
760
  * All settings can be customized via config and hooks.
@@ -786,7 +795,8 @@ var ExpressServer = class {
786
795
  if (this.config.healthzServer.checks) {
787
796
  this.healthzChecks.push(...this.config.healthzServer.checks);
788
797
  }
789
- if (this.config.healthzServer.readinessChecks) {
798
+ if (this.config.healthzServer.readinessChecks !== void 0) {
799
+ this.hasExplicitReadinessChecks = true;
790
800
  this.healthzReadinessChecks.push(...this.config.healthzServer.readinessChecks);
791
801
  }
792
802
  }
@@ -884,6 +894,9 @@ var ExpressServer = class {
884
894
  res.status(HttpStatusCodes.SERVICE_UNAVAILABLE).json(new ServiceUnavailableException("Server is shutting down"));
885
895
  return;
886
896
  }
897
+ if (this.isDraining) {
898
+ res.setHeader("Connection", "close");
899
+ }
887
900
  next();
888
901
  });
889
902
  }
@@ -1197,21 +1210,96 @@ var ExpressServer = class {
1197
1210
  check
1198
1211
  };
1199
1212
  if (probeType === "liveness" || probeType === "both") {
1200
- this.healthzChecks.push(namedCheck);
1213
+ const idx = this.healthzChecks.findIndex((c) => c.name === name);
1214
+ if (idx !== -1) {
1215
+ this.healthzChecks[idx] = namedCheck;
1216
+ } else {
1217
+ this.healthzChecks.push(namedCheck);
1218
+ }
1201
1219
  }
1202
1220
  if (probeType === "readiness" || probeType === "both") {
1203
- this.healthzReadinessChecks.push(namedCheck);
1221
+ this.hasExplicitReadinessChecks = true;
1222
+ const idx = this.healthzReadinessChecks.findIndex((c) => c.name === name);
1223
+ if (idx !== -1) {
1224
+ this.healthzReadinessChecks[idx] = namedCheck;
1225
+ } else {
1226
+ this.healthzReadinessChecks.push(namedCheck);
1227
+ }
1204
1228
  }
1205
1229
  HealthzServer.registerCheck(namedCheck, probeType);
1206
1230
  return this;
1207
1231
  }
1208
1232
  /**
1233
+ * Unregister a health check by name.
1234
+ *
1235
+ * @param name Name of the check to remove
1236
+ * @param options Target probe type ('readiness' | 'liveness' | 'both') or options object
1237
+ * @returns This instance for method chaining
1238
+ */
1239
+ unregisterHealthCheck(name, options) {
1240
+ const probeType = typeof options === "string" ? options : options?.type ?? "both";
1241
+ if (probeType === "liveness" || probeType === "both") {
1242
+ const idx = this.healthzChecks.findIndex((c) => c.name === name);
1243
+ if (idx !== -1) this.healthzChecks.splice(idx, 1);
1244
+ }
1245
+ if (probeType === "readiness" || probeType === "both") {
1246
+ const idx = this.healthzReadinessChecks.findIndex((c) => c.name === name);
1247
+ if (idx !== -1) this.healthzReadinessChecks.splice(idx, 1);
1248
+ }
1249
+ HealthzServer.unregisterCheck(name, probeType);
1250
+ return this;
1251
+ }
1252
+ /**
1253
+ * Get all registered Healthz checks queued for this Express server.
1254
+ */
1255
+ getHealthzChecks() {
1256
+ return {
1257
+ liveness: [
1258
+ ...this.healthzChecks
1259
+ ],
1260
+ readiness: [
1261
+ ...this.healthzReadinessChecks
1262
+ ]
1263
+ };
1264
+ }
1265
+ /**
1266
+ * Register a readiness health check.
1267
+ *
1268
+ * @param name Unique identifier for the check (used in detailed responses)
1269
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
1270
+ * @returns This instance for method chaining
1271
+ */
1272
+ registerReadinessCheck(name, check) {
1273
+ return this.registerHealthCheck(name, check, "readiness");
1274
+ }
1275
+ /**
1276
+ * Register a liveness health check.
1277
+ *
1278
+ * @param name Unique identifier for the check (used in detailed responses)
1279
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
1280
+ * @returns This instance for method chaining
1281
+ */
1282
+ registerLivenessCheck(name, check) {
1283
+ return this.registerHealthCheck(name, check, "liveness");
1284
+ }
1285
+ /**
1286
+ * Register a readiness and liveness health check.
1287
+ *
1288
+ * @param name Unique identifier for the check (used in detailed responses)
1289
+ * @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
1290
+ * @returns This instance for method chaining
1291
+ */
1292
+ registerReadinessAndLivenessChecks(name, check) {
1293
+ return this.registerHealthCheck(name, check, "both");
1294
+ }
1295
+ /**
1209
1296
  * Mark the service as ready / not-ready for traffic on the Healthz probe server.
1210
1297
  *
1211
1298
  * @param ready Whether the service is ready to receive traffic
1212
1299
  * @returns This instance for method chaining
1213
1300
  */
1214
1301
  setReady(ready) {
1302
+ this.internalReady = ready;
1215
1303
  HealthzServer.setReady(ready);
1216
1304
  return this;
1217
1305
  }
@@ -1219,7 +1307,10 @@ var ExpressServer = class {
1219
1307
  * Whether the service is currently marked as ready for traffic on the Healthz probe server.
1220
1308
  */
1221
1309
  isReady() {
1222
- return HealthzServer.isReady();
1310
+ if (this.isHealthzServerEnabled()) {
1311
+ return HealthzServer.isReady();
1312
+ }
1313
+ return this.internalReady && this.isRunning();
1223
1314
  }
1224
1315
  /**
1225
1316
  * Mark application startup as completed on the Healthz probe server (switches `/startupz` to 200).
@@ -1234,7 +1325,10 @@ var ExpressServer = class {
1234
1325
  * Whether application startup has completed on the Healthz probe server.
1235
1326
  */
1236
1327
  isStartupComplete() {
1237
- return HealthzServer.isStartupComplete();
1328
+ if (this.isHealthzServerEnabled()) {
1329
+ return HealthzServer.isStartupComplete();
1330
+ }
1331
+ return this.isRunning();
1238
1332
  }
1239
1333
  /**
1240
1334
  * Get the running HealthzServer instance (if started).
@@ -1255,7 +1349,7 @@ var ExpressServer = class {
1255
1349
  * @returns `true` when ready, otherwise `false`.
1256
1350
  */
1257
1351
  ready() {
1258
- return HealthzServer.isReady();
1352
+ return this.isReady();
1259
1353
  }
1260
1354
  /**
1261
1355
  * Get the underlying Express application instance.
@@ -1319,9 +1413,11 @@ var ExpressServer = class {
1319
1413
  checks: [
1320
1414
  ...this.healthzChecks
1321
1415
  ],
1322
- readinessChecks: [
1323
- ...this.healthzReadinessChecks
1324
- ]
1416
+ ...this.hasExplicitReadinessChecks ? {
1417
+ readinessChecks: [
1418
+ ...this.healthzReadinessChecks
1419
+ ]
1420
+ } : {}
1325
1421
  };
1326
1422
  const addr = await HealthzServer.start(healthzConfig);
1327
1423
  if (!addr) {
@@ -1365,6 +1461,7 @@ var ExpressServer = class {
1365
1461
  try {
1366
1462
  this.logServerStartInfo();
1367
1463
  await this.runHook("afterStart", server);
1464
+ this.internalReady = true;
1368
1465
  if (this.isHealthzServerEnabled()) {
1369
1466
  HealthzServer.markStartupComplete();
1370
1467
  HealthzServer.setReady(true);
@@ -1451,7 +1548,8 @@ var ExpressServer = class {
1451
1548
  const url = `${protocol}://${host}:${port}`;
1452
1549
  getLogger().info(`Server running on ${url}`);
1453
1550
  if (this.healthzAddress) {
1454
- getLogger().info(`Healthz probe server running on http://${this.healthzAddress.address}:${this.healthzAddress.port}`);
1551
+ const healthzHost = this.formatHostForUrl(this.healthzAddress.address);
1552
+ getLogger().info(`Healthz server running on http://${healthzHost}:${this.healthzAddress.port}`);
1455
1553
  }
1456
1554
  if (this.config.openApi?.enable) {
1457
1555
  getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
@@ -1478,11 +1576,21 @@ var ExpressServer = class {
1478
1576
  getLogger().warn("Stop called but server is not running");
1479
1577
  return;
1480
1578
  }
1481
- if (this.isShuttingDown) {
1482
- getLogger().warn("Shutdown already in progress");
1483
- return;
1579
+ if (this.stopPromise) {
1580
+ return this.stopPromise;
1581
+ }
1582
+ this.stopPromise = this.doStop(force);
1583
+ try {
1584
+ await this.stopPromise;
1585
+ } finally {
1586
+ this.stopPromise = void 0;
1484
1587
  }
1485
- this.isShuttingDown = true;
1588
+ }
1589
+ /**
1590
+ * Internal implementation of server shutdown.
1591
+ */
1592
+ async doStop(force = false) {
1593
+ this.internalReady = false;
1486
1594
  if (HealthzServer.isRunning()) {
1487
1595
  HealthzServer.setReady(false);
1488
1596
  }
@@ -1493,8 +1601,11 @@ var ExpressServer = class {
1493
1601
  const shutdownDelay = this.getHealthzShutdownDelay();
1494
1602
  if (shutdownDelay > 0 && !force && HealthzServer.isRunning()) {
1495
1603
  getLogger().info(`Waiting ${shutdownDelay}ms for load balancer to drain traffic...`);
1604
+ this.isDraining = true;
1496
1605
  await new Promise((resolve) => setTimeout(resolve, shutdownDelay));
1497
1606
  }
1607
+ this.isDraining = false;
1608
+ this.isShuttingDown = true;
1498
1609
  if (this.server) {
1499
1610
  await this.gracefulShutdown(force);
1500
1611
  }
@@ -1504,6 +1615,7 @@ var ExpressServer = class {
1504
1615
  this.healthzAddress = null;
1505
1616
  }
1506
1617
  this.isShuttingDown = false;
1618
+ this.isDraining = false;
1507
1619
  }
1508
1620
  }
1509
1621
  /**
@@ -1590,10 +1702,11 @@ var ExpressServer = class {
1590
1702
  signalHandled = true;
1591
1703
  getLogger().info(`Received ${signal}, initiating graceful shutdown...`);
1592
1704
  try {
1593
- this.disableGracefulShutdown();
1594
1705
  await this.stop(false);
1706
+ this.disableGracefulShutdown();
1595
1707
  process.exit(0);
1596
1708
  } catch (err) {
1709
+ this.disableGracefulShutdown();
1597
1710
  getLogger().fatal({
1598
1711
  err
1599
1712
  }, "Shutdown failed");
@@ -1951,7 +2064,8 @@ var ExpressServer = class {
1951
2064
  }
1952
2065
  normalizePath(path, withGlobalPrefix = false) {
1953
2066
  const sanitize = /* @__PURE__ */ __name((p) => {
1954
- return "/" + p.trim().replace(/^\/+/, "").replace(/\/{2,}/g, "/").replace(/\/+$/, "");
2067
+ const inner = trimChars(p.trim(), "/");
2068
+ return inner ? "/" + inner.replace(/\/{2,}/g, "/") : "/";
1955
2069
  }, "sanitize");
1956
2070
  const prefix = withGlobalPrefix && this.globalPrefix ? sanitize(this.globalPrefix) : "";
1957
2071
  if (typeof path !== "string" || !path.trim()) {