@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/README.md +1 -1
- package/config/index.cjs +5 -6
- package/config/index.mjs +5 -6
- package/healthz-server/index.cjs +42 -5
- package/healthz-server/index.d.ts +49 -23
- package/healthz-server/index.mjs +42 -5
- package/package.json +1 -1
- package/server/index.cjs +180 -101
- package/server/index.d.ts +60 -32
- package/server/index.mjs +183 -104
- package/types/index.d.ts +142 -47
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
|
|
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,
|
|
33
|
+
import { ServiceUnavailableException, NotFoundException } from '@catbee/utils/exception';
|
|
34
34
|
import { getCatbeeServerGlobalConfig } from '@catbee/utils/config';
|
|
35
|
-
import {
|
|
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
|
|
303
|
+
* Configures the dedicated Healthz probe HTTP server for Kubernetes.
|
|
303
304
|
*
|
|
304
|
-
* @param opts -
|
|
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.
|
|
311
|
-
*
|
|
312
|
-
*
|
|
310
|
+
* builder.withHealthzServer({
|
|
311
|
+
* port: 8282,
|
|
312
|
+
* shutdownDelayMs: 5000,
|
|
313
|
+
* readinessChecks: [
|
|
314
|
+
* { name: 'db', check: () => checkDb() }
|
|
315
|
+
* ]
|
|
313
316
|
* })
|
|
314
317
|
* ```
|
|
315
318
|
*/
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
322
|
-
*
|
|
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
|
-
* @
|
|
326
|
-
* ```typescript
|
|
327
|
-
* builder.disableHealthCheck()
|
|
328
|
-
* ```
|
|
346
|
+
* @returns The builder instance for chaining
|
|
329
347
|
*/
|
|
330
|
-
|
|
331
|
-
return this.
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
|
761
|
-
this.
|
|
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
|
-
*
|
|
1162
|
+
* Whether the Healthz probe server is enabled.
|
|
1137
1163
|
*/
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
-
*
|
|
1171
|
+
* Get the graceful shutdown delay in milliseconds configured for HealthzServer.
|
|
1158
1172
|
*/
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
-
*
|
|
1189
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
1211
|
+
* @param ready Whether the service is ready to receive traffic
|
|
1212
|
+
* @returns This instance for method chaining
|
|
1213
1213
|
*/
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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.
|
|
1363
|
-
getLogger().info(`
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
/**
|
|
374
|
-
* - **
|
|
375
|
-
* - **
|
|
376
|
-
*
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
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,
|
|
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 };
|