@catbee/utils 2.2.0 → 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/healthz-server/index.cjs +287 -62
- package/healthz-server/index.d.ts +134 -22
- package/healthz-server/index.mjs +287 -62
- package/package.json +1 -1
- package/server/index.cjs +234 -92
- package/server/index.d.ts +66 -3
- package/server/index.mjs +234 -92
- package/string/index.cjs +44 -2
- package/string/index.d.ts +36 -1
- package/string/index.mjs +42 -3
- package/types/index.d.ts +43 -15
- package/url/index.cjs +5 -4
- package/url/index.mjs +5 -4
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.
|
|
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.
|
|
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,28 @@ 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
|
-
|
|
1318
|
+
if (this.isHealthzServerEnabled()) {
|
|
1319
|
+
return healthzServer.HealthzServer.isReady();
|
|
1320
|
+
}
|
|
1321
|
+
return this.internalReady && this.isRunning();
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Mark application startup as completed on the Healthz probe server (switches `/startupz` to 200).
|
|
1325
|
+
*
|
|
1326
|
+
* @returns This instance for method chaining
|
|
1327
|
+
*/
|
|
1328
|
+
markStartupComplete() {
|
|
1329
|
+
healthzServer.HealthzServer.markStartupComplete();
|
|
1330
|
+
return this;
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Whether application startup has completed on the Healthz probe server.
|
|
1334
|
+
*/
|
|
1335
|
+
isStartupComplete() {
|
|
1336
|
+
if (this.isHealthzServerEnabled()) {
|
|
1337
|
+
return healthzServer.HealthzServer.isStartupComplete();
|
|
1338
|
+
}
|
|
1339
|
+
return this.isRunning();
|
|
1231
1340
|
}
|
|
1232
1341
|
/**
|
|
1233
1342
|
* Get the running HealthzServer instance (if started).
|
|
@@ -1248,7 +1357,7 @@ var ExpressServer = class {
|
|
|
1248
1357
|
* @returns `true` when ready, otherwise `false`.
|
|
1249
1358
|
*/
|
|
1250
1359
|
ready() {
|
|
1251
|
-
return
|
|
1360
|
+
return this.isReady();
|
|
1252
1361
|
}
|
|
1253
1362
|
/**
|
|
1254
1363
|
* Get the underlying Express application instance.
|
|
@@ -1305,92 +1414,108 @@ var ExpressServer = class {
|
|
|
1305
1414
|
*/
|
|
1306
1415
|
async doStart() {
|
|
1307
1416
|
await this.initPromise;
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
});
|
|
1336
|
-
this.setupConnectionTracking();
|
|
1337
|
-
Promise.resolve(this.runHook("onServerCreated", server)).then(() => {
|
|
1338
|
-
const onListening = /* @__PURE__ */ __name(async () => {
|
|
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));
|
|
1417
|
+
if (this.isHealthzServerEnabled()) {
|
|
1418
|
+
const healthzConfig = {
|
|
1419
|
+
...typeof this.config.healthzServer === "object" ? this.config.healthzServer : {},
|
|
1420
|
+
handleSignals: false,
|
|
1421
|
+
checks: [
|
|
1422
|
+
...this.healthzChecks
|
|
1423
|
+
],
|
|
1424
|
+
...this.hasExplicitReadinessChecks ? {
|
|
1425
|
+
readinessChecks: [
|
|
1426
|
+
...this.healthzReadinessChecks
|
|
1427
|
+
]
|
|
1428
|
+
} : {}
|
|
1429
|
+
};
|
|
1430
|
+
const addr = await healthzServer.HealthzServer.start(healthzConfig);
|
|
1431
|
+
if (!addr) {
|
|
1432
|
+
throw new Error("Healthz probe server failed to start (already running in this process)");
|
|
1433
|
+
}
|
|
1434
|
+
this.healthzAddress = addr;
|
|
1435
|
+
}
|
|
1436
|
+
try {
|
|
1437
|
+
await this.runHook("beforeStart", this.app);
|
|
1438
|
+
const server = this.createServerInstance();
|
|
1439
|
+
this.server = server;
|
|
1440
|
+
return await new Promise((resolve, reject) => {
|
|
1441
|
+
let isListening = false;
|
|
1442
|
+
server.on("error", async (err) => {
|
|
1443
|
+
if (!isListening) {
|
|
1366
1444
|
logger.getLogger().error({
|
|
1367
|
-
err
|
|
1368
|
-
}, "Server
|
|
1369
|
-
if (healthzServer.HealthzServer.isStarted()) {
|
|
1370
|
-
await healthzServer.HealthzServer.stop().catch(() => {
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1445
|
+
err
|
|
1446
|
+
}, "Server failed to start");
|
|
1373
1447
|
try {
|
|
1374
1448
|
server.removeAllListeners();
|
|
1375
1449
|
server.close();
|
|
1450
|
+
if (healthzServer.HealthzServer.isRunning()) {
|
|
1451
|
+
await healthzServer.HealthzServer.stop().catch(() => {
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1376
1454
|
} catch {
|
|
1377
1455
|
}
|
|
1378
1456
|
this.server = null;
|
|
1379
1457
|
this.healthzAddress = null;
|
|
1380
1458
|
this.connections.clear();
|
|
1381
|
-
reject(
|
|
1459
|
+
reject(err);
|
|
1460
|
+
} else {
|
|
1461
|
+
logger.getLogger().error({
|
|
1462
|
+
err
|
|
1463
|
+
}, "Server runtime error");
|
|
1382
1464
|
}
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1465
|
+
});
|
|
1466
|
+
this.setupConnectionTracking();
|
|
1467
|
+
Promise.resolve(this.runHook("onServerCreated", server)).then(() => {
|
|
1468
|
+
const onListening = /* @__PURE__ */ __name(async () => {
|
|
1469
|
+
try {
|
|
1470
|
+
this.logServerStartInfo();
|
|
1471
|
+
await this.runHook("afterStart", server);
|
|
1472
|
+
this.internalReady = true;
|
|
1473
|
+
if (this.isHealthzServerEnabled()) {
|
|
1474
|
+
healthzServer.HealthzServer.markStartupComplete();
|
|
1475
|
+
healthzServer.HealthzServer.setReady(true);
|
|
1476
|
+
}
|
|
1477
|
+
isListening = true;
|
|
1478
|
+
resolve(server);
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1481
|
+
logger.getLogger().error({
|
|
1482
|
+
err: error
|
|
1483
|
+
}, "Server startup failed");
|
|
1484
|
+
if (healthzServer.HealthzServer.isRunning()) {
|
|
1485
|
+
await healthzServer.HealthzServer.stop().catch(() => {
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
try {
|
|
1489
|
+
server.removeAllListeners();
|
|
1490
|
+
server.close();
|
|
1491
|
+
} catch {
|
|
1492
|
+
}
|
|
1493
|
+
this.server = null;
|
|
1494
|
+
this.healthzAddress = null;
|
|
1495
|
+
this.connections.clear();
|
|
1496
|
+
reject(error);
|
|
1497
|
+
}
|
|
1498
|
+
}, "onListening");
|
|
1499
|
+
const listenArgs = [
|
|
1500
|
+
this.config.port,
|
|
1501
|
+
this.config.host,
|
|
1502
|
+
onListening
|
|
1503
|
+
];
|
|
1504
|
+
server.listen(...listenArgs);
|
|
1505
|
+
}).catch((err) => {
|
|
1506
|
+
server.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
1507
|
+
});
|
|
1392
1508
|
});
|
|
1393
|
-
})
|
|
1509
|
+
} catch (err) {
|
|
1510
|
+
if (healthzServer.HealthzServer.isRunning()) {
|
|
1511
|
+
await healthzServer.HealthzServer.stop().catch(() => {
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
this.healthzAddress = null;
|
|
1515
|
+
this.server = null;
|
|
1516
|
+
this.connections.clear();
|
|
1517
|
+
throw err;
|
|
1518
|
+
}
|
|
1394
1519
|
}
|
|
1395
1520
|
/**
|
|
1396
1521
|
* Create HTTP or HTTPS server instance (without listening).
|
|
@@ -1431,7 +1556,8 @@ var ExpressServer = class {
|
|
|
1431
1556
|
const url = `${protocol}://${host}:${port}`;
|
|
1432
1557
|
logger.getLogger().info(`Server running on ${url}`);
|
|
1433
1558
|
if (this.healthzAddress) {
|
|
1434
|
-
|
|
1559
|
+
const healthzHost = this.formatHostForUrl(this.healthzAddress.address);
|
|
1560
|
+
logger.getLogger().info(`Healthz server running on http://${healthzHost}:${this.healthzAddress.port}`);
|
|
1435
1561
|
}
|
|
1436
1562
|
if (this.config.openApi?.enable) {
|
|
1437
1563
|
logger.getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
|
|
@@ -1454,16 +1580,26 @@ var ExpressServer = class {
|
|
|
1454
1580
|
* - Monitoring systems are notified
|
|
1455
1581
|
*/
|
|
1456
1582
|
async stop(force = false) {
|
|
1457
|
-
if (!this.server && !healthzServer.HealthzServer.
|
|
1583
|
+
if (!this.server && !healthzServer.HealthzServer.isRunning()) {
|
|
1458
1584
|
logger.getLogger().warn("Stop called but server is not running");
|
|
1459
1585
|
return;
|
|
1460
1586
|
}
|
|
1461
|
-
if (this.
|
|
1462
|
-
|
|
1463
|
-
|
|
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;
|
|
1464
1595
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Internal implementation of server shutdown.
|
|
1599
|
+
*/
|
|
1600
|
+
async doStop(force = false) {
|
|
1601
|
+
this.internalReady = false;
|
|
1602
|
+
if (healthzServer.HealthzServer.isRunning()) {
|
|
1467
1603
|
healthzServer.HealthzServer.setReady(false);
|
|
1468
1604
|
}
|
|
1469
1605
|
if (this.server) {
|
|
@@ -1471,19 +1607,23 @@ var ExpressServer = class {
|
|
|
1471
1607
|
}
|
|
1472
1608
|
try {
|
|
1473
1609
|
const shutdownDelay = this.getHealthzShutdownDelay();
|
|
1474
|
-
if (shutdownDelay > 0 && !force && healthzServer.HealthzServer.
|
|
1610
|
+
if (shutdownDelay > 0 && !force && healthzServer.HealthzServer.isRunning()) {
|
|
1475
1611
|
logger.getLogger().info(`Waiting ${shutdownDelay}ms for load balancer to drain traffic...`);
|
|
1612
|
+
this.isDraining = true;
|
|
1476
1613
|
await new Promise((resolve) => setTimeout(resolve, shutdownDelay));
|
|
1477
1614
|
}
|
|
1615
|
+
this.isDraining = false;
|
|
1616
|
+
this.isShuttingDown = true;
|
|
1478
1617
|
if (this.server) {
|
|
1479
1618
|
await this.gracefulShutdown(force);
|
|
1480
1619
|
}
|
|
1481
1620
|
} finally {
|
|
1482
|
-
if (healthzServer.HealthzServer.
|
|
1621
|
+
if (healthzServer.HealthzServer.isRunning()) {
|
|
1483
1622
|
await healthzServer.HealthzServer.stop();
|
|
1484
1623
|
this.healthzAddress = null;
|
|
1485
1624
|
}
|
|
1486
1625
|
this.isShuttingDown = false;
|
|
1626
|
+
this.isDraining = false;
|
|
1487
1627
|
}
|
|
1488
1628
|
}
|
|
1489
1629
|
/**
|
|
@@ -1570,10 +1710,11 @@ var ExpressServer = class {
|
|
|
1570
1710
|
signalHandled = true;
|
|
1571
1711
|
logger.getLogger().info(`Received ${signal}, initiating graceful shutdown...`);
|
|
1572
1712
|
try {
|
|
1573
|
-
this.disableGracefulShutdown();
|
|
1574
1713
|
await this.stop(false);
|
|
1714
|
+
this.disableGracefulShutdown();
|
|
1575
1715
|
process.exit(0);
|
|
1576
1716
|
} catch (err) {
|
|
1717
|
+
this.disableGracefulShutdown();
|
|
1577
1718
|
logger.getLogger().fatal({
|
|
1578
1719
|
err
|
|
1579
1720
|
}, "Shutdown failed");
|
|
@@ -1931,7 +2072,8 @@ var ExpressServer = class {
|
|
|
1931
2072
|
}
|
|
1932
2073
|
normalizePath(path, withGlobalPrefix = false) {
|
|
1933
2074
|
const sanitize = /* @__PURE__ */ __name((p) => {
|
|
1934
|
-
|
|
2075
|
+
const inner = string.trimChars(p.trim(), "/");
|
|
2076
|
+
return inner ? "/" + inner.replace(/\/{2,}/g, "/") : "/";
|
|
1935
2077
|
}, "sanitize");
|
|
1936
2078
|
const prefix = withGlobalPrefix && this.globalPrefix ? sanitize(this.globalPrefix) : "";
|
|
1937
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
|
*
|
|
@@ -237,6 +286,16 @@ declare class ExpressServer {
|
|
|
237
286
|
* Whether the service is currently marked as ready for traffic on the Healthz probe server.
|
|
238
287
|
*/
|
|
239
288
|
isReady(): boolean;
|
|
289
|
+
/**
|
|
290
|
+
* Mark application startup as completed on the Healthz probe server (switches `/startupz` to 200).
|
|
291
|
+
*
|
|
292
|
+
* @returns This instance for method chaining
|
|
293
|
+
*/
|
|
294
|
+
markStartupComplete(): this;
|
|
295
|
+
/**
|
|
296
|
+
* Whether application startup has completed on the Healthz probe server.
|
|
297
|
+
*/
|
|
298
|
+
isStartupComplete(): boolean;
|
|
240
299
|
/**
|
|
241
300
|
* Get the running HealthzServer instance (if started).
|
|
242
301
|
*/
|
|
@@ -317,6 +376,10 @@ declare class ExpressServer {
|
|
|
317
376
|
* - Monitoring systems are notified
|
|
318
377
|
*/
|
|
319
378
|
stop(force?: boolean): Promise<void>;
|
|
379
|
+
/**
|
|
380
|
+
* Internal implementation of server shutdown.
|
|
381
|
+
*/
|
|
382
|
+
private doStop;
|
|
320
383
|
/**
|
|
321
384
|
* Perform graceful server shutdown with timeout.
|
|
322
385
|
*/
|