@catbee/utils 2.0.5 → 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 +2 -1
- package/config/index.cjs +5 -6
- package/config/index.mjs +5 -6
- package/context-store/index.cjs +97 -76
- package/context-store/index.d.ts +74 -57
- package/context-store/index.mjs +97 -77
- package/healthz-server/index.cjs +393 -0
- package/healthz-server/index.d.ts +317 -0
- package/healthz-server/index.mjs +389 -0
- package/index.cjs +7 -0
- package/index.d.ts +1 -0
- package/index.mjs +1 -0
- package/logger/index.cjs +174 -76
- package/logger/index.d.ts +29 -18
- package/logger/index.mjs +174 -77
- package/package.json +12 -7
- package/server/index.cjs +393 -172
- package/server/index.d.ts +132 -46
- package/server/index.mjs +395 -175
- package/types/index.d.ts +142 -47
package/server/index.cjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
'use strict';
|
|
26
26
|
|
|
27
27
|
var express = require('express');
|
|
28
|
+
var http = require('http');
|
|
28
29
|
var https = require('https');
|
|
29
30
|
var httpStatusCodes = require('@catbee/utils/http-status-codes');
|
|
30
31
|
var response = require('@catbee/utils/response');
|
|
@@ -38,10 +39,12 @@ var fs = require('@catbee/utils/fs');
|
|
|
38
39
|
var validation = require('@catbee/utils/validation');
|
|
39
40
|
var async = require('@catbee/utils/async');
|
|
40
41
|
var id = require('@catbee/utils/id');
|
|
42
|
+
var healthzServer = require('@catbee/utils/healthz-server');
|
|
41
43
|
|
|
42
44
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
43
45
|
|
|
44
46
|
var express__default = /*#__PURE__*/_interopDefault(express);
|
|
47
|
+
var http__default = /*#__PURE__*/_interopDefault(http);
|
|
45
48
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
46
49
|
|
|
47
50
|
var __defProp = Object.defineProperty;
|
|
@@ -305,36 +308,53 @@ var ServerConfigBuilder = class {
|
|
|
305
308
|
return this.setEnabled("requestLogging", false);
|
|
306
309
|
}
|
|
307
310
|
/**
|
|
308
|
-
* Configures server
|
|
311
|
+
* Configures the dedicated Healthz probe HTTP server for Kubernetes.
|
|
309
312
|
*
|
|
310
|
-
* @param opts -
|
|
313
|
+
* @param opts - Healthz server configuration options or boolean toggle
|
|
311
314
|
* @returns The builder instance for chaining
|
|
312
|
-
* @default - { path: '/healthz', detailed: true, withGlobalPrefix: false }
|
|
313
315
|
*
|
|
314
316
|
* @example
|
|
315
317
|
* ```typescript
|
|
316
|
-
* builder.
|
|
317
|
-
*
|
|
318
|
-
*
|
|
318
|
+
* builder.withHealthzServer({
|
|
319
|
+
* port: 8282,
|
|
320
|
+
* shutdownDelayMs: 5000,
|
|
321
|
+
* readinessChecks: [
|
|
322
|
+
* { name: 'db', check: () => checkDb() }
|
|
323
|
+
* ]
|
|
319
324
|
* })
|
|
320
325
|
* ```
|
|
321
326
|
*/
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
}
|
|
324
337
|
return this;
|
|
325
338
|
}
|
|
326
339
|
/**
|
|
327
|
-
* Enables
|
|
328
|
-
*
|
|
340
|
+
* Enables the dedicated Healthz probe HTTP server.
|
|
341
|
+
*
|
|
342
|
+
* @param opts - Optional Healthz server configuration options
|
|
329
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.
|
|
330
353
|
*
|
|
331
|
-
* @
|
|
332
|
-
* ```typescript
|
|
333
|
-
* builder.disableHealthCheck()
|
|
334
|
-
* ```
|
|
354
|
+
* @returns The builder instance for chaining
|
|
335
355
|
*/
|
|
336
|
-
|
|
337
|
-
return this.
|
|
356
|
+
disableHealthzServer() {
|
|
357
|
+
return this.withHealthzServer(false);
|
|
338
358
|
}
|
|
339
359
|
/**
|
|
340
360
|
* Configures OpenAPI/Swagger documentation for the API.
|
|
@@ -689,6 +709,15 @@ var DependencyErrors = {
|
|
|
689
709
|
"cookie-parser": getDependencyErrorMessage("cookie-parser"),
|
|
690
710
|
"@scalar/express-api-reference": getDependencyErrorMessage("@scalar/express-api-reference")
|
|
691
711
|
};
|
|
712
|
+
var SUPPORTED_HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
713
|
+
"get",
|
|
714
|
+
"post",
|
|
715
|
+
"put",
|
|
716
|
+
"delete",
|
|
717
|
+
"patch",
|
|
718
|
+
"options",
|
|
719
|
+
"head"
|
|
720
|
+
]);
|
|
692
721
|
var ExpressServer = class {
|
|
693
722
|
static {
|
|
694
723
|
__name(this, "ExpressServer");
|
|
@@ -701,11 +730,11 @@ var ExpressServer = class {
|
|
|
701
730
|
hooks;
|
|
702
731
|
/** Global API prefix (from config) */
|
|
703
732
|
globalPrefix;
|
|
704
|
-
/**
|
|
733
|
+
/** Primary root router mounted to the application */
|
|
705
734
|
rootRouter;
|
|
706
|
-
/**
|
|
707
|
-
|
|
708
|
-
/**
|
|
735
|
+
/** Set of registered sub-routers to prevent duplicate mounting */
|
|
736
|
+
mountedRouters = /* @__PURE__ */ new Set();
|
|
737
|
+
/** Express app instance */
|
|
709
738
|
app;
|
|
710
739
|
/** Set of active WebSocket connections */
|
|
711
740
|
connections = /* @__PURE__ */ new Set();
|
|
@@ -713,13 +742,18 @@ var ExpressServer = class {
|
|
|
713
742
|
isShuttingDown = false;
|
|
714
743
|
/** Flag indicating if graceful shutdown handlers are registered */
|
|
715
744
|
gracefulShutdownRegistered = false;
|
|
716
|
-
/**
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
745
|
+
/** Map of registered signal listeners for clean teardown */
|
|
746
|
+
signalListeners = /* @__PURE__ */ new Map();
|
|
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 = [];
|
|
721
753
|
/** Promise that resolves when initialization (middleware + routes) is complete */
|
|
722
754
|
initPromise;
|
|
755
|
+
/** In-flight start promise to protect against concurrent start() calls */
|
|
756
|
+
startPromise;
|
|
723
757
|
/**
|
|
724
758
|
* Initializes server with intelligent defaults and security best practices.
|
|
725
759
|
* All settings can be customized via config and hooks.
|
|
@@ -750,8 +784,19 @@ var ExpressServer = class {
|
|
|
750
784
|
logger.getLogger().error(msg);
|
|
751
785
|
throw new Error(msg);
|
|
752
786
|
}
|
|
753
|
-
if (config
|
|
754
|
-
this.
|
|
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
|
+
}
|
|
755
800
|
}
|
|
756
801
|
this.globalPrefix = this.normalizePath(this.config.globalPrefix ?? "", false);
|
|
757
802
|
this.hooks = hooks;
|
|
@@ -822,6 +867,7 @@ var ExpressServer = class {
|
|
|
822
867
|
this.setupBodyParsingMiddleware();
|
|
823
868
|
this.setupCookieParsingMiddleware();
|
|
824
869
|
await this.setupOpenApiMiddleware();
|
|
870
|
+
this.setupResponseHook();
|
|
825
871
|
}
|
|
826
872
|
/**
|
|
827
873
|
* Set up basic middleware (trust proxy, request ID, context).
|
|
@@ -876,10 +922,15 @@ var ExpressServer = class {
|
|
|
876
922
|
* Set up global headers middleware.
|
|
877
923
|
*/
|
|
878
924
|
setupGlobalHeaders() {
|
|
925
|
+
const hasCustomHeaders = Boolean(this.config.globalHeaders && Object.keys(this.config.globalHeaders).length > 0);
|
|
926
|
+
const isMicroservice = Boolean(this.config.isMicroservice);
|
|
927
|
+
const hasServiceVersion = Boolean(this.config.serviceVersion?.enable);
|
|
928
|
+
if (!hasCustomHeaders && !isMicroservice && !hasServiceVersion) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
879
931
|
this.app.use((_req, res, next) => {
|
|
880
932
|
if (this.config.globalHeaders) {
|
|
881
|
-
for (const key
|
|
882
|
-
const value = this.config.globalHeaders[key];
|
|
933
|
+
for (const [key, value] of Object.entries(this.config.globalHeaders)) {
|
|
883
934
|
res.setHeader(key, typeof value === "function" ? value() : value);
|
|
884
935
|
}
|
|
885
936
|
}
|
|
@@ -1074,6 +1125,11 @@ var ExpressServer = class {
|
|
|
1074
1125
|
}, "Failed to mount OpenAPI docs");
|
|
1075
1126
|
}
|
|
1076
1127
|
}
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Set up response preprocessing hook (applies global prefix if set).
|
|
1131
|
+
*/
|
|
1132
|
+
setupResponseHook() {
|
|
1077
1133
|
if (this.hooks.onResponse) {
|
|
1078
1134
|
this.app.use(this.globalPrefix, this.hooks.onResponse);
|
|
1079
1135
|
}
|
|
@@ -1088,12 +1144,7 @@ var ExpressServer = class {
|
|
|
1088
1144
|
* 4. Error handler
|
|
1089
1145
|
*/
|
|
1090
1146
|
async setupRoutes() {
|
|
1091
|
-
|
|
1092
|
-
this.app.get(healthCheckPath, async (_req, res) => {
|
|
1093
|
-
return this.handleHealthCheckRequest(res);
|
|
1094
|
-
});
|
|
1095
|
-
const routerToUse = this.externalRouter || this.rootRouter;
|
|
1096
|
-
this.app.use(this.globalPrefix, routerToUse);
|
|
1147
|
+
this.app.use(this.globalPrefix, this.rootRouter);
|
|
1097
1148
|
await this.runHook("afterRoutes", this.app);
|
|
1098
1149
|
this.app.use((req, res) => {
|
|
1099
1150
|
const status = httpStatusCodes.HttpStatusCodes.NOT_FOUND;
|
|
@@ -1116,60 +1167,25 @@ var ExpressServer = class {
|
|
|
1116
1167
|
});
|
|
1117
1168
|
}
|
|
1118
1169
|
/**
|
|
1119
|
-
*
|
|
1170
|
+
* Whether the Healthz probe server is enabled.
|
|
1120
1171
|
*/
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
return res.status(httpStatusCodes.HttpStatusCodes.OK).json(new response.SuccessResponse("OK"));
|
|
1125
|
-
}
|
|
1126
|
-
const results = await this.executeHealthChecks();
|
|
1127
|
-
const allOk = results.every((r) => r.status);
|
|
1128
|
-
const status = allOk ? httpStatusCodes.HttpStatusCodes.OK : httpStatusCodes.HttpStatusCodes.SERVICE_UNAVAILABLE;
|
|
1129
|
-
const response$1 = new response.SuccessResponse(allOk ? "OK" : "Service unavailable");
|
|
1130
|
-
if (!allOk) response$1.error = true;
|
|
1131
|
-
if (this.config.healthCheck?.detailed) response$1.data = {
|
|
1132
|
-
checks: results
|
|
1133
|
-
};
|
|
1134
|
-
return res.status(status).json(response$1);
|
|
1135
|
-
} catch {
|
|
1136
|
-
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;
|
|
1137
1175
|
}
|
|
1176
|
+
return this.config.healthzServer?.enable === true;
|
|
1138
1177
|
}
|
|
1139
1178
|
/**
|
|
1140
|
-
*
|
|
1179
|
+
* Get the graceful shutdown delay in milliseconds configured for HealthzServer.
|
|
1141
1180
|
*/
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
try {
|
|
1145
|
-
const status = await Promise.resolve(check());
|
|
1146
|
-
return {
|
|
1147
|
-
name,
|
|
1148
|
-
status,
|
|
1149
|
-
error: null
|
|
1150
|
-
};
|
|
1151
|
-
} catch (error) {
|
|
1152
|
-
return {
|
|
1153
|
-
name,
|
|
1154
|
-
status: false,
|
|
1155
|
-
error: error.message
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
}));
|
|
1159
|
-
return checkResults.map((result) => {
|
|
1160
|
-
if (result.status === "fulfilled") return result.value;
|
|
1161
|
-
return {
|
|
1162
|
-
name: "unknown",
|
|
1163
|
-
status: false,
|
|
1164
|
-
error: result.reason
|
|
1165
|
-
};
|
|
1166
|
-
});
|
|
1181
|
+
getHealthzShutdownDelay() {
|
|
1182
|
+
return typeof this.config.healthzServer === "object" ? this.config.healthzServer.shutdownDelayMs ?? 0 : 0;
|
|
1167
1183
|
}
|
|
1168
1184
|
/**
|
|
1169
|
-
* 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.
|
|
1170
1186
|
*
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1187
|
+
* By default, external dependencies (DB, Redis, etc.) are registered as `readiness` checks.
|
|
1188
|
+
* Can also be registered as `liveness` or `both`.
|
|
1173
1189
|
*
|
|
1174
1190
|
* Examples:
|
|
1175
1191
|
* - Database connectivity
|
|
@@ -1178,33 +1194,61 @@ var ExpressServer = class {
|
|
|
1178
1194
|
* - Memory/CPU usage checks
|
|
1179
1195
|
*
|
|
1180
1196
|
* @param name Unique identifier for the check (used in detailed responses)
|
|
1181
|
-
* @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
|
|
1182
1199
|
* @returns This instance for method chaining
|
|
1183
1200
|
*/
|
|
1184
|
-
registerHealthCheck(name, check) {
|
|
1185
|
-
|
|
1201
|
+
registerHealthCheck(name, check, options) {
|
|
1202
|
+
const probeType = typeof options === "string" ? options : options?.type ?? "readiness";
|
|
1203
|
+
const namedCheck = {
|
|
1186
1204
|
name,
|
|
1187
1205
|
check
|
|
1188
|
-
}
|
|
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);
|
|
1189
1214
|
return this;
|
|
1190
1215
|
}
|
|
1191
1216
|
/**
|
|
1192
|
-
*
|
|
1193
|
-
* Useful for readiness probes in deployment tooling.
|
|
1217
|
+
* Mark the service as ready / not-ready for traffic on the Healthz probe server.
|
|
1194
1218
|
*
|
|
1195
|
-
* @
|
|
1219
|
+
* @param ready Whether the service is ready to receive traffic
|
|
1220
|
+
* @returns This instance for method chaining
|
|
1196
1221
|
*/
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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();
|
|
1208
1252
|
}
|
|
1209
1253
|
/**
|
|
1210
1254
|
* Get the underlying Express application instance.
|
|
@@ -1228,11 +1272,15 @@ var ExpressServer = class {
|
|
|
1228
1272
|
* Start the HTTP server and begin listening for requests.
|
|
1229
1273
|
*
|
|
1230
1274
|
* This method:
|
|
1275
|
+
* - Protects against concurrent start() invocations
|
|
1276
|
+
* - Awaits server initialization (middleware + routes)
|
|
1231
1277
|
* - Executes beforeStart hooks
|
|
1278
|
+
* - Creates the HTTP/HTTPS server instance
|
|
1279
|
+
* - Sets up error handling and connection tracking BEFORE listening
|
|
1280
|
+
* - Executes onServerCreated hook BEFORE listening
|
|
1232
1281
|
* - Binds to the configured host/port
|
|
1233
|
-
* -
|
|
1234
|
-
* -
|
|
1235
|
-
* - Logs startup information
|
|
1282
|
+
* - Executes afterStart hooks on successful listen
|
|
1283
|
+
* - Cleans up server reference and listeners on startup failure
|
|
1236
1284
|
*
|
|
1237
1285
|
* @returns Promise resolving to the running HTTP server instance
|
|
1238
1286
|
* @throws Error if server fails to start or port is already in use
|
|
@@ -1242,33 +1290,112 @@ var ExpressServer = class {
|
|
|
1242
1290
|
logger.getLogger().warn("Server is already running, returning existing instance");
|
|
1243
1291
|
return this.server;
|
|
1244
1292
|
}
|
|
1293
|
+
if (this.startPromise) {
|
|
1294
|
+
return this.startPromise;
|
|
1295
|
+
}
|
|
1296
|
+
this.startPromise = this.doStart();
|
|
1297
|
+
try {
|
|
1298
|
+
return await this.startPromise;
|
|
1299
|
+
} finally {
|
|
1300
|
+
this.startPromise = void 0;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Internal implementation of server startup.
|
|
1305
|
+
*/
|
|
1306
|
+
async doStart() {
|
|
1245
1307
|
await this.initPromise;
|
|
1246
1308
|
await this.runHook("beforeStart", this.app);
|
|
1309
|
+
const server = this.createServerInstance();
|
|
1310
|
+
this.server = server;
|
|
1247
1311
|
return new Promise((resolve, reject) => {
|
|
1248
|
-
|
|
1312
|
+
let isListening = false;
|
|
1313
|
+
server.on("error", (err) => {
|
|
1314
|
+
if (!isListening) {
|
|
1315
|
+
logger.getLogger().error({
|
|
1316
|
+
err
|
|
1317
|
+
}, "Server failed to start");
|
|
1318
|
+
try {
|
|
1319
|
+
server.removeAllListeners();
|
|
1320
|
+
server.close();
|
|
1321
|
+
if (healthzServer.HealthzServer.isStarted()) {
|
|
1322
|
+
healthzServer.HealthzServer.stop().catch(() => {
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
} catch {
|
|
1326
|
+
}
|
|
1327
|
+
this.server = null;
|
|
1328
|
+
this.connections.clear();
|
|
1329
|
+
reject(err);
|
|
1330
|
+
} else {
|
|
1331
|
+
logger.getLogger().error({
|
|
1332
|
+
err
|
|
1333
|
+
}, "Server runtime error");
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
this.setupConnectionTracking();
|
|
1337
|
+
Promise.resolve(this.runHook("onServerCreated", server)).then(() => {
|
|
1249
1338
|
const onListening = /* @__PURE__ */ __name(async () => {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
+
}
|
|
1253
1383
|
}, "onListening");
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1384
|
+
const listenArgs = [
|
|
1385
|
+
this.config.port,
|
|
1386
|
+
this.config.host,
|
|
1387
|
+
onListening
|
|
1388
|
+
];
|
|
1389
|
+
server.listen(...listenArgs);
|
|
1390
|
+
}).catch((err) => {
|
|
1391
|
+
server.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
1392
|
+
});
|
|
1261
1393
|
});
|
|
1262
1394
|
}
|
|
1263
1395
|
/**
|
|
1264
|
-
* Create HTTP or HTTPS server instance.
|
|
1396
|
+
* Create HTTP or HTTPS server instance (without listening).
|
|
1265
1397
|
*/
|
|
1266
|
-
createServerInstance(
|
|
1267
|
-
const listenArgs = [
|
|
1268
|
-
this.config.port,
|
|
1269
|
-
this.config.host,
|
|
1270
|
-
onListening
|
|
1271
|
-
];
|
|
1398
|
+
createServerInstance() {
|
|
1272
1399
|
if (this.config.https) {
|
|
1273
1400
|
const httpsOptions = {
|
|
1274
1401
|
...this.config.https,
|
|
@@ -1281,9 +1408,9 @@ var ExpressServer = class {
|
|
|
1281
1408
|
if (this.config.https.passphrase) {
|
|
1282
1409
|
httpsOptions.passphrase = this.config.https.passphrase;
|
|
1283
1410
|
}
|
|
1284
|
-
return https__default.default.createServer(httpsOptions, this.app)
|
|
1411
|
+
return https__default.default.createServer(httpsOptions, this.app);
|
|
1285
1412
|
}
|
|
1286
|
-
return this.app
|
|
1413
|
+
return http__default.default.createServer(this.app);
|
|
1287
1414
|
}
|
|
1288
1415
|
/**
|
|
1289
1416
|
* Set up connection tracking for graceful shutdown.
|
|
@@ -1295,17 +1422,6 @@ var ExpressServer = class {
|
|
|
1295
1422
|
});
|
|
1296
1423
|
}
|
|
1297
1424
|
/**
|
|
1298
|
-
* Set up error handling for server startup.
|
|
1299
|
-
*/
|
|
1300
|
-
setupServerErrorHandling(reject) {
|
|
1301
|
-
this.server.on("error", (err) => {
|
|
1302
|
-
logger.getLogger().error({
|
|
1303
|
-
err
|
|
1304
|
-
}, "Server failed to start");
|
|
1305
|
-
reject(err);
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
1425
|
* Log server startup information.
|
|
1310
1426
|
*/
|
|
1311
1427
|
logServerStartInfo() {
|
|
@@ -1314,8 +1430,8 @@ var ExpressServer = class {
|
|
|
1314
1430
|
const host = this.formatHostForUrl(this.config.host || "localhost");
|
|
1315
1431
|
const url = `${protocol}://${host}:${port}`;
|
|
1316
1432
|
logger.getLogger().info(`Server running on ${url}`);
|
|
1317
|
-
if (this.
|
|
1318
|
-
logger.getLogger().info(`
|
|
1433
|
+
if (this.healthzAddress) {
|
|
1434
|
+
logger.getLogger().info(`Healthz probe server running on http://${this.healthzAddress.address}:${this.healthzAddress.port}`);
|
|
1319
1435
|
}
|
|
1320
1436
|
if (this.config.openApi?.enable) {
|
|
1321
1437
|
logger.getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
|
|
@@ -1338,7 +1454,7 @@ var ExpressServer = class {
|
|
|
1338
1454
|
* - Monitoring systems are notified
|
|
1339
1455
|
*/
|
|
1340
1456
|
async stop(force = false) {
|
|
1341
|
-
if (!this.server) {
|
|
1457
|
+
if (!this.server && !healthzServer.HealthzServer.isStarted()) {
|
|
1342
1458
|
logger.getLogger().warn("Stop called but server is not running");
|
|
1343
1459
|
return;
|
|
1344
1460
|
}
|
|
@@ -1347,10 +1463,26 @@ var ExpressServer = class {
|
|
|
1347
1463
|
return;
|
|
1348
1464
|
}
|
|
1349
1465
|
this.isShuttingDown = true;
|
|
1350
|
-
|
|
1466
|
+
if (healthzServer.HealthzServer.isStarted()) {
|
|
1467
|
+
healthzServer.HealthzServer.setReady(false);
|
|
1468
|
+
}
|
|
1469
|
+
if (this.server) {
|
|
1470
|
+
await this.runHook("beforeStop", this.server);
|
|
1471
|
+
}
|
|
1351
1472
|
try {
|
|
1352
|
-
|
|
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
|
+
}
|
|
1353
1481
|
} finally {
|
|
1482
|
+
if (healthzServer.HealthzServer.isStarted()) {
|
|
1483
|
+
await healthzServer.HealthzServer.stop();
|
|
1484
|
+
this.healthzAddress = null;
|
|
1485
|
+
}
|
|
1354
1486
|
this.isShuttingDown = false;
|
|
1355
1487
|
}
|
|
1356
1488
|
}
|
|
@@ -1367,6 +1499,7 @@ var ExpressServer = class {
|
|
|
1367
1499
|
afterStopCalled = true;
|
|
1368
1500
|
await this.runHook("afterStop");
|
|
1369
1501
|
}, "runAfterStop");
|
|
1502
|
+
server.closeIdleConnections?.();
|
|
1370
1503
|
const serverClosePromise = new Promise((resolve, reject) => {
|
|
1371
1504
|
server.close(async (err) => {
|
|
1372
1505
|
if (timer) clearTimeout(timer);
|
|
@@ -1429,7 +1562,7 @@ var ExpressServer = class {
|
|
|
1429
1562
|
}
|
|
1430
1563
|
let signalHandled = false;
|
|
1431
1564
|
signals.forEach((signal) => {
|
|
1432
|
-
|
|
1565
|
+
const handler = /* @__PURE__ */ __name(async () => {
|
|
1433
1566
|
if (signalHandled) {
|
|
1434
1567
|
logger.getLogger().warn(`Ignoring duplicate ${signal}`);
|
|
1435
1568
|
return;
|
|
@@ -1437,7 +1570,8 @@ var ExpressServer = class {
|
|
|
1437
1570
|
signalHandled = true;
|
|
1438
1571
|
logger.getLogger().info(`Received ${signal}, initiating graceful shutdown...`);
|
|
1439
1572
|
try {
|
|
1440
|
-
|
|
1573
|
+
this.disableGracefulShutdown();
|
|
1574
|
+
await this.stop(false);
|
|
1441
1575
|
process.exit(0);
|
|
1442
1576
|
} catch (err) {
|
|
1443
1577
|
logger.getLogger().fatal({
|
|
@@ -1445,12 +1579,29 @@ var ExpressServer = class {
|
|
|
1445
1579
|
}, "Shutdown failed");
|
|
1446
1580
|
process.exit(1);
|
|
1447
1581
|
}
|
|
1448
|
-
});
|
|
1582
|
+
}, "handler");
|
|
1583
|
+
this.signalListeners.set(signal, handler);
|
|
1584
|
+
process.on(signal, handler);
|
|
1449
1585
|
});
|
|
1450
1586
|
this.gracefulShutdownRegistered = true;
|
|
1451
1587
|
return this;
|
|
1452
1588
|
}
|
|
1453
1589
|
/**
|
|
1590
|
+
* Unregister graceful shutdown signal listeners.
|
|
1591
|
+
* Useful for testing and dynamic server lifecycles to prevent memory and listener leaks.
|
|
1592
|
+
*/
|
|
1593
|
+
disableGracefulShutdown() {
|
|
1594
|
+
if (!this.gracefulShutdownRegistered) {
|
|
1595
|
+
return this;
|
|
1596
|
+
}
|
|
1597
|
+
for (const [signal, handler] of this.signalListeners.entries()) {
|
|
1598
|
+
process.removeListener(signal, handler);
|
|
1599
|
+
}
|
|
1600
|
+
this.signalListeners.clear();
|
|
1601
|
+
this.gracefulShutdownRegistered = false;
|
|
1602
|
+
return this;
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1454
1605
|
* Destroy all active connections (gracefully if possible).
|
|
1455
1606
|
* If a connection does not close cleanly, it will be force-destroyed.
|
|
1456
1607
|
*/
|
|
@@ -1463,32 +1614,56 @@ var ExpressServer = class {
|
|
|
1463
1614
|
];
|
|
1464
1615
|
await Promise.allSettled(sockets.map((socket) => new Promise((resolve) => {
|
|
1465
1616
|
socket.end();
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
clearTimeout(timer);
|
|
1617
|
+
let timer;
|
|
1618
|
+
const cleanup = /* @__PURE__ */ __name(() => {
|
|
1619
|
+
if (timer) clearTimeout(timer);
|
|
1620
|
+
socket.removeListener("close", onClose);
|
|
1621
|
+
socket.removeListener("error", onError);
|
|
1472
1622
|
resolve();
|
|
1473
|
-
});
|
|
1474
|
-
|
|
1475
|
-
|
|
1623
|
+
}, "cleanup");
|
|
1624
|
+
const onClose = /* @__PURE__ */ __name(() => cleanup(), "onClose");
|
|
1625
|
+
const onError = /* @__PURE__ */ __name(() => {
|
|
1476
1626
|
socket.destroy();
|
|
1477
|
-
|
|
1478
|
-
});
|
|
1627
|
+
cleanup();
|
|
1628
|
+
}, "onError");
|
|
1629
|
+
timer = setTimeout(() => {
|
|
1630
|
+
socket.destroy();
|
|
1631
|
+
cleanup();
|
|
1632
|
+
}, 1e3);
|
|
1633
|
+
socket.once("close", onClose);
|
|
1634
|
+
socket.once("error", onError);
|
|
1479
1635
|
})));
|
|
1480
1636
|
this.connections.clear();
|
|
1481
1637
|
logger.getLogger().info(`Closed ${sockets.length} active connection(s)`);
|
|
1482
1638
|
}
|
|
1483
1639
|
/**
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1640
|
+
* Mount a base router onto the server's root router.
|
|
1641
|
+
*
|
|
1642
|
+
* Note: This attaches the supplied router to the root router pipeline.
|
|
1643
|
+
* Duplicate mounting of the same router instance is ignored.
|
|
1644
|
+
*
|
|
1645
|
+
* @param router The Express router instance to mount
|
|
1646
|
+
* @returns This instance for method chaining
|
|
1486
1647
|
*/
|
|
1487
|
-
|
|
1488
|
-
this.
|
|
1648
|
+
addBaseRouter(router) {
|
|
1649
|
+
if (this.mountedRouters.has(router)) {
|
|
1650
|
+
return this;
|
|
1651
|
+
}
|
|
1652
|
+
this.mountedRouters.add(router);
|
|
1653
|
+
this.rootRouter.use(router);
|
|
1489
1654
|
return this;
|
|
1490
1655
|
}
|
|
1491
1656
|
/**
|
|
1657
|
+
* Alias for `addBaseRouter` (maintained for backward compatibility).
|
|
1658
|
+
* Mounts the supplied router onto the server's root router.
|
|
1659
|
+
*
|
|
1660
|
+
* @param router The Express router instance to mount
|
|
1661
|
+
* @returns This instance for method chaining
|
|
1662
|
+
*/
|
|
1663
|
+
setBaseRouter(router) {
|
|
1664
|
+
return this.addBaseRouter(router);
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1492
1667
|
* Create and register a new router (only used if not injecting one externally - use `setBaseRouter` instead).
|
|
1493
1668
|
*/
|
|
1494
1669
|
createRouter(prefix = "") {
|
|
@@ -1508,27 +1683,73 @@ var ExpressServer = class {
|
|
|
1508
1683
|
*/
|
|
1509
1684
|
registerRoute(methods, path, ...handlers) {
|
|
1510
1685
|
const fullPath = this.normalizePath(path, true);
|
|
1511
|
-
const routerToUse = this.
|
|
1512
|
-
const methodMap = {
|
|
1513
|
-
get: routerToUse.get.bind(routerToUse),
|
|
1514
|
-
post: routerToUse.post.bind(routerToUse),
|
|
1515
|
-
put: routerToUse.put.bind(routerToUse),
|
|
1516
|
-
delete: routerToUse.delete.bind(routerToUse),
|
|
1517
|
-
patch: routerToUse.patch.bind(routerToUse),
|
|
1518
|
-
options: routerToUse.options.bind(routerToUse),
|
|
1519
|
-
head: routerToUse.head.bind(routerToUse)
|
|
1520
|
-
};
|
|
1686
|
+
const routerToUse = this.rootRouter;
|
|
1521
1687
|
methods.forEach((m) => {
|
|
1522
|
-
const
|
|
1523
|
-
if (
|
|
1524
|
-
fn(fullPath, ...handlers);
|
|
1525
|
-
} else {
|
|
1688
|
+
const method = m.toLowerCase();
|
|
1689
|
+
if (!SUPPORTED_HTTP_METHODS.has(method) || typeof routerToUse[method] !== "function") {
|
|
1526
1690
|
throw new Error(`Unsupported HTTP method: ${m}`);
|
|
1527
1691
|
}
|
|
1692
|
+
routerToUse[method](fullPath, ...handlers);
|
|
1528
1693
|
});
|
|
1529
1694
|
return this;
|
|
1530
1695
|
}
|
|
1531
1696
|
/**
|
|
1697
|
+
* Register a GET route handler.
|
|
1698
|
+
*/
|
|
1699
|
+
get(path, ...handlers) {
|
|
1700
|
+
return this.registerRoute([
|
|
1701
|
+
"get"
|
|
1702
|
+
], path, ...handlers);
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Register a POST route handler.
|
|
1706
|
+
*/
|
|
1707
|
+
post(path, ...handlers) {
|
|
1708
|
+
return this.registerRoute([
|
|
1709
|
+
"post"
|
|
1710
|
+
], path, ...handlers);
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Register a PUT route handler.
|
|
1714
|
+
*/
|
|
1715
|
+
put(path, ...handlers) {
|
|
1716
|
+
return this.registerRoute([
|
|
1717
|
+
"put"
|
|
1718
|
+
], path, ...handlers);
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Register a DELETE route handler.
|
|
1722
|
+
*/
|
|
1723
|
+
delete(path, ...handlers) {
|
|
1724
|
+
return this.registerRoute([
|
|
1725
|
+
"delete"
|
|
1726
|
+
], path, ...handlers);
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Register a PATCH route handler.
|
|
1730
|
+
*/
|
|
1731
|
+
patch(path, ...handlers) {
|
|
1732
|
+
return this.registerRoute([
|
|
1733
|
+
"patch"
|
|
1734
|
+
], path, ...handlers);
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Register an OPTIONS route handler.
|
|
1738
|
+
*/
|
|
1739
|
+
options(path, ...handlers) {
|
|
1740
|
+
return this.registerRoute([
|
|
1741
|
+
"options"
|
|
1742
|
+
], path, ...handlers);
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Register a HEAD route handler.
|
|
1746
|
+
*/
|
|
1747
|
+
head(path, ...handlers) {
|
|
1748
|
+
return this.registerRoute([
|
|
1749
|
+
"head"
|
|
1750
|
+
], path, ...handlers);
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1532
1753
|
* Register custom middleware with optional path restriction.
|
|
1533
1754
|
*
|
|
1534
1755
|
* Use this for:
|
|
@@ -1542,7 +1763,7 @@ var ExpressServer = class {
|
|
|
1542
1763
|
* @returns This instance for method chaining
|
|
1543
1764
|
*/
|
|
1544
1765
|
registerMiddleware(path, middleware) {
|
|
1545
|
-
const routerToUse = this.
|
|
1766
|
+
const routerToUse = this.rootRouter;
|
|
1546
1767
|
if (typeof path === "string") {
|
|
1547
1768
|
const normalizedPath = this.normalizePath(path);
|
|
1548
1769
|
if (normalizedPath) {
|
|
@@ -1565,7 +1786,7 @@ var ExpressServer = class {
|
|
|
1565
1786
|
*/
|
|
1566
1787
|
useMiddleware(...middlewares) {
|
|
1567
1788
|
middlewares.forEach((middleware) => {
|
|
1568
|
-
|
|
1789
|
+
this.rootRouter.use(middleware);
|
|
1569
1790
|
});
|
|
1570
1791
|
return this;
|
|
1571
1792
|
}
|