@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.mjs
CHANGED
|
@@ -23,19 +23,21 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import express from 'express';
|
|
26
|
+
import http from 'http';
|
|
26
27
|
import https from 'https';
|
|
27
28
|
import { HttpStatusCodes } from '@catbee/utils/http-status-codes';
|
|
28
|
-
import { createFinalErrorResponse
|
|
29
|
+
import { createFinalErrorResponse } from '@catbee/utils/response';
|
|
29
30
|
import { requestId, setupRequestContext, timeout, responseTime, errorHandler } from '@catbee/utils/middleware';
|
|
30
31
|
import { Env } from '@catbee/utils/env';
|
|
31
32
|
import { getLogger } from '@catbee/utils/logger';
|
|
32
|
-
import { ServiceUnavailableException,
|
|
33
|
+
import { ServiceUnavailableException, NotFoundException } from '@catbee/utils/exception';
|
|
33
34
|
import { getCatbeeServerGlobalConfig } from '@catbee/utils/config';
|
|
34
|
-
import {
|
|
35
|
+
import { isPlainObject, deepClone, deepObjMerge } from '@catbee/utils/object';
|
|
35
36
|
import { fileExists, readFile, readFileSync } from '@catbee/utils/fs';
|
|
36
37
|
import { isPort, isHostname } from '@catbee/utils/validation';
|
|
37
38
|
import { optionalRequire } from '@catbee/utils/async';
|
|
38
39
|
import { uuid } from '@catbee/utils/id';
|
|
40
|
+
import { HealthzServer } from '@catbee/utils/healthz-server';
|
|
39
41
|
|
|
40
42
|
var __defProp = Object.defineProperty;
|
|
41
43
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
@@ -298,36 +300,53 @@ var ServerConfigBuilder = class {
|
|
|
298
300
|
return this.setEnabled("requestLogging", false);
|
|
299
301
|
}
|
|
300
302
|
/**
|
|
301
|
-
* Configures server
|
|
303
|
+
* Configures the dedicated Healthz probe HTTP server for Kubernetes.
|
|
302
304
|
*
|
|
303
|
-
* @param opts -
|
|
305
|
+
* @param opts - Healthz server configuration options or boolean toggle
|
|
304
306
|
* @returns The builder instance for chaining
|
|
305
|
-
* @default - { path: '/healthz', detailed: true, withGlobalPrefix: false }
|
|
306
307
|
*
|
|
307
308
|
* @example
|
|
308
309
|
* ```typescript
|
|
309
|
-
* builder.
|
|
310
|
-
*
|
|
311
|
-
*
|
|
310
|
+
* builder.withHealthzServer({
|
|
311
|
+
* port: 8282,
|
|
312
|
+
* shutdownDelayMs: 5000,
|
|
313
|
+
* readinessChecks: [
|
|
314
|
+
* { name: 'db', check: () => checkDb() }
|
|
315
|
+
* ]
|
|
312
316
|
* })
|
|
313
317
|
* ```
|
|
314
318
|
*/
|
|
315
|
-
|
|
316
|
-
|
|
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
|
+
}
|
|
317
329
|
return this;
|
|
318
330
|
}
|
|
319
331
|
/**
|
|
320
|
-
* Enables
|
|
321
|
-
*
|
|
332
|
+
* Enables the dedicated Healthz probe HTTP server.
|
|
333
|
+
*
|
|
334
|
+
* @param opts - Optional Healthz server configuration options
|
|
322
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.
|
|
323
345
|
*
|
|
324
|
-
* @
|
|
325
|
-
* ```typescript
|
|
326
|
-
* builder.disableHealthCheck()
|
|
327
|
-
* ```
|
|
346
|
+
* @returns The builder instance for chaining
|
|
328
347
|
*/
|
|
329
|
-
|
|
330
|
-
return this.
|
|
348
|
+
disableHealthzServer() {
|
|
349
|
+
return this.withHealthzServer(false);
|
|
331
350
|
}
|
|
332
351
|
/**
|
|
333
352
|
* Configures OpenAPI/Swagger documentation for the API.
|
|
@@ -682,6 +701,15 @@ var DependencyErrors = {
|
|
|
682
701
|
"cookie-parser": getDependencyErrorMessage("cookie-parser"),
|
|
683
702
|
"@scalar/express-api-reference": getDependencyErrorMessage("@scalar/express-api-reference")
|
|
684
703
|
};
|
|
704
|
+
var SUPPORTED_HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
705
|
+
"get",
|
|
706
|
+
"post",
|
|
707
|
+
"put",
|
|
708
|
+
"delete",
|
|
709
|
+
"patch",
|
|
710
|
+
"options",
|
|
711
|
+
"head"
|
|
712
|
+
]);
|
|
685
713
|
var ExpressServer = class {
|
|
686
714
|
static {
|
|
687
715
|
__name(this, "ExpressServer");
|
|
@@ -694,11 +722,11 @@ var ExpressServer = class {
|
|
|
694
722
|
hooks;
|
|
695
723
|
/** Global API prefix (from config) */
|
|
696
724
|
globalPrefix;
|
|
697
|
-
/**
|
|
725
|
+
/** Primary root router mounted to the application */
|
|
698
726
|
rootRouter;
|
|
699
|
-
/**
|
|
700
|
-
|
|
701
|
-
/**
|
|
727
|
+
/** Set of registered sub-routers to prevent duplicate mounting */
|
|
728
|
+
mountedRouters = /* @__PURE__ */ new Set();
|
|
729
|
+
/** Express app instance */
|
|
702
730
|
app;
|
|
703
731
|
/** Set of active WebSocket connections */
|
|
704
732
|
connections = /* @__PURE__ */ new Set();
|
|
@@ -706,13 +734,18 @@ var ExpressServer = class {
|
|
|
706
734
|
isShuttingDown = false;
|
|
707
735
|
/** Flag indicating if graceful shutdown handlers are registered */
|
|
708
736
|
gracefulShutdownRegistered = false;
|
|
709
|
-
/**
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
737
|
+
/** Map of registered signal listeners for clean teardown */
|
|
738
|
+
signalListeners = /* @__PURE__ */ new Map();
|
|
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 = [];
|
|
714
745
|
/** Promise that resolves when initialization (middleware + routes) is complete */
|
|
715
746
|
initPromise;
|
|
747
|
+
/** In-flight start promise to protect against concurrent start() calls */
|
|
748
|
+
startPromise;
|
|
716
749
|
/**
|
|
717
750
|
* Initializes server with intelligent defaults and security best practices.
|
|
718
751
|
* All settings can be customized via config and hooks.
|
|
@@ -743,8 +776,19 @@ var ExpressServer = class {
|
|
|
743
776
|
getLogger().error(msg);
|
|
744
777
|
throw new Error(msg);
|
|
745
778
|
}
|
|
746
|
-
if (config
|
|
747
|
-
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
|
+
}
|
|
748
792
|
}
|
|
749
793
|
this.globalPrefix = this.normalizePath(this.config.globalPrefix ?? "", false);
|
|
750
794
|
this.hooks = hooks;
|
|
@@ -815,6 +859,7 @@ var ExpressServer = class {
|
|
|
815
859
|
this.setupBodyParsingMiddleware();
|
|
816
860
|
this.setupCookieParsingMiddleware();
|
|
817
861
|
await this.setupOpenApiMiddleware();
|
|
862
|
+
this.setupResponseHook();
|
|
818
863
|
}
|
|
819
864
|
/**
|
|
820
865
|
* Set up basic middleware (trust proxy, request ID, context).
|
|
@@ -869,10 +914,15 @@ var ExpressServer = class {
|
|
|
869
914
|
* Set up global headers middleware.
|
|
870
915
|
*/
|
|
871
916
|
setupGlobalHeaders() {
|
|
917
|
+
const hasCustomHeaders = Boolean(this.config.globalHeaders && Object.keys(this.config.globalHeaders).length > 0);
|
|
918
|
+
const isMicroservice = Boolean(this.config.isMicroservice);
|
|
919
|
+
const hasServiceVersion = Boolean(this.config.serviceVersion?.enable);
|
|
920
|
+
if (!hasCustomHeaders && !isMicroservice && !hasServiceVersion) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
872
923
|
this.app.use((_req, res, next) => {
|
|
873
924
|
if (this.config.globalHeaders) {
|
|
874
|
-
for (const key
|
|
875
|
-
const value = this.config.globalHeaders[key];
|
|
925
|
+
for (const [key, value] of Object.entries(this.config.globalHeaders)) {
|
|
876
926
|
res.setHeader(key, typeof value === "function" ? value() : value);
|
|
877
927
|
}
|
|
878
928
|
}
|
|
@@ -1067,6 +1117,11 @@ var ExpressServer = class {
|
|
|
1067
1117
|
}, "Failed to mount OpenAPI docs");
|
|
1068
1118
|
}
|
|
1069
1119
|
}
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Set up response preprocessing hook (applies global prefix if set).
|
|
1123
|
+
*/
|
|
1124
|
+
setupResponseHook() {
|
|
1070
1125
|
if (this.hooks.onResponse) {
|
|
1071
1126
|
this.app.use(this.globalPrefix, this.hooks.onResponse);
|
|
1072
1127
|
}
|
|
@@ -1081,12 +1136,7 @@ var ExpressServer = class {
|
|
|
1081
1136
|
* 4. Error handler
|
|
1082
1137
|
*/
|
|
1083
1138
|
async setupRoutes() {
|
|
1084
|
-
|
|
1085
|
-
this.app.get(healthCheckPath, async (_req, res) => {
|
|
1086
|
-
return this.handleHealthCheckRequest(res);
|
|
1087
|
-
});
|
|
1088
|
-
const routerToUse = this.externalRouter || this.rootRouter;
|
|
1089
|
-
this.app.use(this.globalPrefix, routerToUse);
|
|
1139
|
+
this.app.use(this.globalPrefix, this.rootRouter);
|
|
1090
1140
|
await this.runHook("afterRoutes", this.app);
|
|
1091
1141
|
this.app.use((req, res) => {
|
|
1092
1142
|
const status = HttpStatusCodes.NOT_FOUND;
|
|
@@ -1109,60 +1159,25 @@ var ExpressServer = class {
|
|
|
1109
1159
|
});
|
|
1110
1160
|
}
|
|
1111
1161
|
/**
|
|
1112
|
-
*
|
|
1162
|
+
* Whether the Healthz probe server is enabled.
|
|
1113
1163
|
*/
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
return res.status(HttpStatusCodes.OK).json(new SuccessResponse("OK"));
|
|
1118
|
-
}
|
|
1119
|
-
const results = await this.executeHealthChecks();
|
|
1120
|
-
const allOk = results.every((r) => r.status);
|
|
1121
|
-
const status = allOk ? HttpStatusCodes.OK : HttpStatusCodes.SERVICE_UNAVAILABLE;
|
|
1122
|
-
const response = new SuccessResponse(allOk ? "OK" : "Service unavailable");
|
|
1123
|
-
if (!allOk) response.error = true;
|
|
1124
|
-
if (this.config.healthCheck?.detailed) response.data = {
|
|
1125
|
-
checks: results
|
|
1126
|
-
};
|
|
1127
|
-
return res.status(status).json(response);
|
|
1128
|
-
} catch {
|
|
1129
|
-
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;
|
|
1130
1167
|
}
|
|
1168
|
+
return this.config.healthzServer?.enable === true;
|
|
1131
1169
|
}
|
|
1132
1170
|
/**
|
|
1133
|
-
*
|
|
1171
|
+
* Get the graceful shutdown delay in milliseconds configured for HealthzServer.
|
|
1134
1172
|
*/
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
try {
|
|
1138
|
-
const status = await Promise.resolve(check());
|
|
1139
|
-
return {
|
|
1140
|
-
name,
|
|
1141
|
-
status,
|
|
1142
|
-
error: null
|
|
1143
|
-
};
|
|
1144
|
-
} catch (error) {
|
|
1145
|
-
return {
|
|
1146
|
-
name,
|
|
1147
|
-
status: false,
|
|
1148
|
-
error: error.message
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
}));
|
|
1152
|
-
return checkResults.map((result) => {
|
|
1153
|
-
if (result.status === "fulfilled") return result.value;
|
|
1154
|
-
return {
|
|
1155
|
-
name: "unknown",
|
|
1156
|
-
status: false,
|
|
1157
|
-
error: result.reason
|
|
1158
|
-
};
|
|
1159
|
-
});
|
|
1173
|
+
getHealthzShutdownDelay() {
|
|
1174
|
+
return typeof this.config.healthzServer === "object" ? this.config.healthzServer.shutdownDelayMs ?? 0 : 0;
|
|
1160
1175
|
}
|
|
1161
1176
|
/**
|
|
1162
|
-
* 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.
|
|
1163
1178
|
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1179
|
+
* By default, external dependencies (DB, Redis, etc.) are registered as `readiness` checks.
|
|
1180
|
+
* Can also be registered as `liveness` or `both`.
|
|
1166
1181
|
*
|
|
1167
1182
|
* Examples:
|
|
1168
1183
|
* - Database connectivity
|
|
@@ -1171,33 +1186,61 @@ var ExpressServer = class {
|
|
|
1171
1186
|
* - Memory/CPU usage checks
|
|
1172
1187
|
*
|
|
1173
1188
|
* @param name Unique identifier for the check (used in detailed responses)
|
|
1174
|
-
* @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
|
|
1175
1191
|
* @returns This instance for method chaining
|
|
1176
1192
|
*/
|
|
1177
|
-
registerHealthCheck(name, check) {
|
|
1178
|
-
|
|
1193
|
+
registerHealthCheck(name, check, options) {
|
|
1194
|
+
const probeType = typeof options === "string" ? options : options?.type ?? "readiness";
|
|
1195
|
+
const namedCheck = {
|
|
1179
1196
|
name,
|
|
1180
1197
|
check
|
|
1181
|
-
}
|
|
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);
|
|
1182
1206
|
return this;
|
|
1183
1207
|
}
|
|
1184
1208
|
/**
|
|
1185
|
-
*
|
|
1186
|
-
* Useful for readiness probes in deployment tooling.
|
|
1209
|
+
* Mark the service as ready / not-ready for traffic on the Healthz probe server.
|
|
1187
1210
|
*
|
|
1188
|
-
* @
|
|
1211
|
+
* @param ready Whether the service is ready to receive traffic
|
|
1212
|
+
* @returns This instance for method chaining
|
|
1189
1213
|
*/
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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();
|
|
1201
1244
|
}
|
|
1202
1245
|
/**
|
|
1203
1246
|
* Get the underlying Express application instance.
|
|
@@ -1221,11 +1264,15 @@ var ExpressServer = class {
|
|
|
1221
1264
|
* Start the HTTP server and begin listening for requests.
|
|
1222
1265
|
*
|
|
1223
1266
|
* This method:
|
|
1267
|
+
* - Protects against concurrent start() invocations
|
|
1268
|
+
* - Awaits server initialization (middleware + routes)
|
|
1224
1269
|
* - Executes beforeStart hooks
|
|
1270
|
+
* - Creates the HTTP/HTTPS server instance
|
|
1271
|
+
* - Sets up error handling and connection tracking BEFORE listening
|
|
1272
|
+
* - Executes onServerCreated hook BEFORE listening
|
|
1225
1273
|
* - Binds to the configured host/port
|
|
1226
|
-
* -
|
|
1227
|
-
* -
|
|
1228
|
-
* - Logs startup information
|
|
1274
|
+
* - Executes afterStart hooks on successful listen
|
|
1275
|
+
* - Cleans up server reference and listeners on startup failure
|
|
1229
1276
|
*
|
|
1230
1277
|
* @returns Promise resolving to the running HTTP server instance
|
|
1231
1278
|
* @throws Error if server fails to start or port is already in use
|
|
@@ -1235,33 +1282,112 @@ var ExpressServer = class {
|
|
|
1235
1282
|
getLogger().warn("Server is already running, returning existing instance");
|
|
1236
1283
|
return this.server;
|
|
1237
1284
|
}
|
|
1285
|
+
if (this.startPromise) {
|
|
1286
|
+
return this.startPromise;
|
|
1287
|
+
}
|
|
1288
|
+
this.startPromise = this.doStart();
|
|
1289
|
+
try {
|
|
1290
|
+
return await this.startPromise;
|
|
1291
|
+
} finally {
|
|
1292
|
+
this.startPromise = void 0;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Internal implementation of server startup.
|
|
1297
|
+
*/
|
|
1298
|
+
async doStart() {
|
|
1238
1299
|
await this.initPromise;
|
|
1239
1300
|
await this.runHook("beforeStart", this.app);
|
|
1301
|
+
const server = this.createServerInstance();
|
|
1302
|
+
this.server = server;
|
|
1240
1303
|
return new Promise((resolve, reject) => {
|
|
1241
|
-
|
|
1304
|
+
let isListening = false;
|
|
1305
|
+
server.on("error", (err) => {
|
|
1306
|
+
if (!isListening) {
|
|
1307
|
+
getLogger().error({
|
|
1308
|
+
err
|
|
1309
|
+
}, "Server failed to start");
|
|
1310
|
+
try {
|
|
1311
|
+
server.removeAllListeners();
|
|
1312
|
+
server.close();
|
|
1313
|
+
if (HealthzServer.isStarted()) {
|
|
1314
|
+
HealthzServer.stop().catch(() => {
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
} catch {
|
|
1318
|
+
}
|
|
1319
|
+
this.server = null;
|
|
1320
|
+
this.connections.clear();
|
|
1321
|
+
reject(err);
|
|
1322
|
+
} else {
|
|
1323
|
+
getLogger().error({
|
|
1324
|
+
err
|
|
1325
|
+
}, "Server runtime error");
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
this.setupConnectionTracking();
|
|
1329
|
+
Promise.resolve(this.runHook("onServerCreated", server)).then(() => {
|
|
1242
1330
|
const onListening = /* @__PURE__ */ __name(async () => {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
+
}
|
|
1246
1375
|
}, "onListening");
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1376
|
+
const listenArgs = [
|
|
1377
|
+
this.config.port,
|
|
1378
|
+
this.config.host,
|
|
1379
|
+
onListening
|
|
1380
|
+
];
|
|
1381
|
+
server.listen(...listenArgs);
|
|
1382
|
+
}).catch((err) => {
|
|
1383
|
+
server.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
1384
|
+
});
|
|
1254
1385
|
});
|
|
1255
1386
|
}
|
|
1256
1387
|
/**
|
|
1257
|
-
* Create HTTP or HTTPS server instance.
|
|
1388
|
+
* Create HTTP or HTTPS server instance (without listening).
|
|
1258
1389
|
*/
|
|
1259
|
-
createServerInstance(
|
|
1260
|
-
const listenArgs = [
|
|
1261
|
-
this.config.port,
|
|
1262
|
-
this.config.host,
|
|
1263
|
-
onListening
|
|
1264
|
-
];
|
|
1390
|
+
createServerInstance() {
|
|
1265
1391
|
if (this.config.https) {
|
|
1266
1392
|
const httpsOptions = {
|
|
1267
1393
|
...this.config.https,
|
|
@@ -1274,9 +1400,9 @@ var ExpressServer = class {
|
|
|
1274
1400
|
if (this.config.https.passphrase) {
|
|
1275
1401
|
httpsOptions.passphrase = this.config.https.passphrase;
|
|
1276
1402
|
}
|
|
1277
|
-
return https.createServer(httpsOptions, this.app)
|
|
1403
|
+
return https.createServer(httpsOptions, this.app);
|
|
1278
1404
|
}
|
|
1279
|
-
return this.app
|
|
1405
|
+
return http.createServer(this.app);
|
|
1280
1406
|
}
|
|
1281
1407
|
/**
|
|
1282
1408
|
* Set up connection tracking for graceful shutdown.
|
|
@@ -1288,17 +1414,6 @@ var ExpressServer = class {
|
|
|
1288
1414
|
});
|
|
1289
1415
|
}
|
|
1290
1416
|
/**
|
|
1291
|
-
* Set up error handling for server startup.
|
|
1292
|
-
*/
|
|
1293
|
-
setupServerErrorHandling(reject) {
|
|
1294
|
-
this.server.on("error", (err) => {
|
|
1295
|
-
getLogger().error({
|
|
1296
|
-
err
|
|
1297
|
-
}, "Server failed to start");
|
|
1298
|
-
reject(err);
|
|
1299
|
-
});
|
|
1300
|
-
}
|
|
1301
|
-
/**
|
|
1302
1417
|
* Log server startup information.
|
|
1303
1418
|
*/
|
|
1304
1419
|
logServerStartInfo() {
|
|
@@ -1307,8 +1422,8 @@ var ExpressServer = class {
|
|
|
1307
1422
|
const host = this.formatHostForUrl(this.config.host || "localhost");
|
|
1308
1423
|
const url = `${protocol}://${host}:${port}`;
|
|
1309
1424
|
getLogger().info(`Server running on ${url}`);
|
|
1310
|
-
if (this.
|
|
1311
|
-
getLogger().info(`
|
|
1425
|
+
if (this.healthzAddress) {
|
|
1426
|
+
getLogger().info(`Healthz probe server running on http://${this.healthzAddress.address}:${this.healthzAddress.port}`);
|
|
1312
1427
|
}
|
|
1313
1428
|
if (this.config.openApi?.enable) {
|
|
1314
1429
|
getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
|
|
@@ -1331,7 +1446,7 @@ var ExpressServer = class {
|
|
|
1331
1446
|
* - Monitoring systems are notified
|
|
1332
1447
|
*/
|
|
1333
1448
|
async stop(force = false) {
|
|
1334
|
-
if (!this.server) {
|
|
1449
|
+
if (!this.server && !HealthzServer.isStarted()) {
|
|
1335
1450
|
getLogger().warn("Stop called but server is not running");
|
|
1336
1451
|
return;
|
|
1337
1452
|
}
|
|
@@ -1340,10 +1455,26 @@ var ExpressServer = class {
|
|
|
1340
1455
|
return;
|
|
1341
1456
|
}
|
|
1342
1457
|
this.isShuttingDown = true;
|
|
1343
|
-
|
|
1458
|
+
if (HealthzServer.isStarted()) {
|
|
1459
|
+
HealthzServer.setReady(false);
|
|
1460
|
+
}
|
|
1461
|
+
if (this.server) {
|
|
1462
|
+
await this.runHook("beforeStop", this.server);
|
|
1463
|
+
}
|
|
1344
1464
|
try {
|
|
1345
|
-
|
|
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
|
+
}
|
|
1346
1473
|
} finally {
|
|
1474
|
+
if (HealthzServer.isStarted()) {
|
|
1475
|
+
await HealthzServer.stop();
|
|
1476
|
+
this.healthzAddress = null;
|
|
1477
|
+
}
|
|
1347
1478
|
this.isShuttingDown = false;
|
|
1348
1479
|
}
|
|
1349
1480
|
}
|
|
@@ -1360,6 +1491,7 @@ var ExpressServer = class {
|
|
|
1360
1491
|
afterStopCalled = true;
|
|
1361
1492
|
await this.runHook("afterStop");
|
|
1362
1493
|
}, "runAfterStop");
|
|
1494
|
+
server.closeIdleConnections?.();
|
|
1363
1495
|
const serverClosePromise = new Promise((resolve, reject) => {
|
|
1364
1496
|
server.close(async (err) => {
|
|
1365
1497
|
if (timer) clearTimeout(timer);
|
|
@@ -1422,7 +1554,7 @@ var ExpressServer = class {
|
|
|
1422
1554
|
}
|
|
1423
1555
|
let signalHandled = false;
|
|
1424
1556
|
signals.forEach((signal) => {
|
|
1425
|
-
|
|
1557
|
+
const handler = /* @__PURE__ */ __name(async () => {
|
|
1426
1558
|
if (signalHandled) {
|
|
1427
1559
|
getLogger().warn(`Ignoring duplicate ${signal}`);
|
|
1428
1560
|
return;
|
|
@@ -1430,7 +1562,8 @@ var ExpressServer = class {
|
|
|
1430
1562
|
signalHandled = true;
|
|
1431
1563
|
getLogger().info(`Received ${signal}, initiating graceful shutdown...`);
|
|
1432
1564
|
try {
|
|
1433
|
-
|
|
1565
|
+
this.disableGracefulShutdown();
|
|
1566
|
+
await this.stop(false);
|
|
1434
1567
|
process.exit(0);
|
|
1435
1568
|
} catch (err) {
|
|
1436
1569
|
getLogger().fatal({
|
|
@@ -1438,12 +1571,29 @@ var ExpressServer = class {
|
|
|
1438
1571
|
}, "Shutdown failed");
|
|
1439
1572
|
process.exit(1);
|
|
1440
1573
|
}
|
|
1441
|
-
});
|
|
1574
|
+
}, "handler");
|
|
1575
|
+
this.signalListeners.set(signal, handler);
|
|
1576
|
+
process.on(signal, handler);
|
|
1442
1577
|
});
|
|
1443
1578
|
this.gracefulShutdownRegistered = true;
|
|
1444
1579
|
return this;
|
|
1445
1580
|
}
|
|
1446
1581
|
/**
|
|
1582
|
+
* Unregister graceful shutdown signal listeners.
|
|
1583
|
+
* Useful for testing and dynamic server lifecycles to prevent memory and listener leaks.
|
|
1584
|
+
*/
|
|
1585
|
+
disableGracefulShutdown() {
|
|
1586
|
+
if (!this.gracefulShutdownRegistered) {
|
|
1587
|
+
return this;
|
|
1588
|
+
}
|
|
1589
|
+
for (const [signal, handler] of this.signalListeners.entries()) {
|
|
1590
|
+
process.removeListener(signal, handler);
|
|
1591
|
+
}
|
|
1592
|
+
this.signalListeners.clear();
|
|
1593
|
+
this.gracefulShutdownRegistered = false;
|
|
1594
|
+
return this;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1447
1597
|
* Destroy all active connections (gracefully if possible).
|
|
1448
1598
|
* If a connection does not close cleanly, it will be force-destroyed.
|
|
1449
1599
|
*/
|
|
@@ -1456,32 +1606,56 @@ var ExpressServer = class {
|
|
|
1456
1606
|
];
|
|
1457
1607
|
await Promise.allSettled(sockets.map((socket) => new Promise((resolve) => {
|
|
1458
1608
|
socket.end();
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
clearTimeout(timer);
|
|
1609
|
+
let timer;
|
|
1610
|
+
const cleanup = /* @__PURE__ */ __name(() => {
|
|
1611
|
+
if (timer) clearTimeout(timer);
|
|
1612
|
+
socket.removeListener("close", onClose);
|
|
1613
|
+
socket.removeListener("error", onError);
|
|
1465
1614
|
resolve();
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
|
|
1615
|
+
}, "cleanup");
|
|
1616
|
+
const onClose = /* @__PURE__ */ __name(() => cleanup(), "onClose");
|
|
1617
|
+
const onError = /* @__PURE__ */ __name(() => {
|
|
1469
1618
|
socket.destroy();
|
|
1470
|
-
|
|
1471
|
-
});
|
|
1619
|
+
cleanup();
|
|
1620
|
+
}, "onError");
|
|
1621
|
+
timer = setTimeout(() => {
|
|
1622
|
+
socket.destroy();
|
|
1623
|
+
cleanup();
|
|
1624
|
+
}, 1e3);
|
|
1625
|
+
socket.once("close", onClose);
|
|
1626
|
+
socket.once("error", onError);
|
|
1472
1627
|
})));
|
|
1473
1628
|
this.connections.clear();
|
|
1474
1629
|
getLogger().info(`Closed ${sockets.length} active connection(s)`);
|
|
1475
1630
|
}
|
|
1476
1631
|
/**
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
1632
|
+
* Mount a base router onto the server's root router.
|
|
1633
|
+
*
|
|
1634
|
+
* Note: This attaches the supplied router to the root router pipeline.
|
|
1635
|
+
* Duplicate mounting of the same router instance is ignored.
|
|
1636
|
+
*
|
|
1637
|
+
* @param router The Express router instance to mount
|
|
1638
|
+
* @returns This instance for method chaining
|
|
1479
1639
|
*/
|
|
1480
|
-
|
|
1481
|
-
this.
|
|
1640
|
+
addBaseRouter(router) {
|
|
1641
|
+
if (this.mountedRouters.has(router)) {
|
|
1642
|
+
return this;
|
|
1643
|
+
}
|
|
1644
|
+
this.mountedRouters.add(router);
|
|
1645
|
+
this.rootRouter.use(router);
|
|
1482
1646
|
return this;
|
|
1483
1647
|
}
|
|
1484
1648
|
/**
|
|
1649
|
+
* Alias for `addBaseRouter` (maintained for backward compatibility).
|
|
1650
|
+
* Mounts the supplied router onto the server's root router.
|
|
1651
|
+
*
|
|
1652
|
+
* @param router The Express router instance to mount
|
|
1653
|
+
* @returns This instance for method chaining
|
|
1654
|
+
*/
|
|
1655
|
+
setBaseRouter(router) {
|
|
1656
|
+
return this.addBaseRouter(router);
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1485
1659
|
* Create and register a new router (only used if not injecting one externally - use `setBaseRouter` instead).
|
|
1486
1660
|
*/
|
|
1487
1661
|
createRouter(prefix = "") {
|
|
@@ -1501,27 +1675,73 @@ var ExpressServer = class {
|
|
|
1501
1675
|
*/
|
|
1502
1676
|
registerRoute(methods, path, ...handlers) {
|
|
1503
1677
|
const fullPath = this.normalizePath(path, true);
|
|
1504
|
-
const routerToUse = this.
|
|
1505
|
-
const methodMap = {
|
|
1506
|
-
get: routerToUse.get.bind(routerToUse),
|
|
1507
|
-
post: routerToUse.post.bind(routerToUse),
|
|
1508
|
-
put: routerToUse.put.bind(routerToUse),
|
|
1509
|
-
delete: routerToUse.delete.bind(routerToUse),
|
|
1510
|
-
patch: routerToUse.patch.bind(routerToUse),
|
|
1511
|
-
options: routerToUse.options.bind(routerToUse),
|
|
1512
|
-
head: routerToUse.head.bind(routerToUse)
|
|
1513
|
-
};
|
|
1678
|
+
const routerToUse = this.rootRouter;
|
|
1514
1679
|
methods.forEach((m) => {
|
|
1515
|
-
const
|
|
1516
|
-
if (
|
|
1517
|
-
fn(fullPath, ...handlers);
|
|
1518
|
-
} else {
|
|
1680
|
+
const method = m.toLowerCase();
|
|
1681
|
+
if (!SUPPORTED_HTTP_METHODS.has(method) || typeof routerToUse[method] !== "function") {
|
|
1519
1682
|
throw new Error(`Unsupported HTTP method: ${m}`);
|
|
1520
1683
|
}
|
|
1684
|
+
routerToUse[method](fullPath, ...handlers);
|
|
1521
1685
|
});
|
|
1522
1686
|
return this;
|
|
1523
1687
|
}
|
|
1524
1688
|
/**
|
|
1689
|
+
* Register a GET route handler.
|
|
1690
|
+
*/
|
|
1691
|
+
get(path, ...handlers) {
|
|
1692
|
+
return this.registerRoute([
|
|
1693
|
+
"get"
|
|
1694
|
+
], path, ...handlers);
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Register a POST route handler.
|
|
1698
|
+
*/
|
|
1699
|
+
post(path, ...handlers) {
|
|
1700
|
+
return this.registerRoute([
|
|
1701
|
+
"post"
|
|
1702
|
+
], path, ...handlers);
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Register a PUT route handler.
|
|
1706
|
+
*/
|
|
1707
|
+
put(path, ...handlers) {
|
|
1708
|
+
return this.registerRoute([
|
|
1709
|
+
"put"
|
|
1710
|
+
], path, ...handlers);
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Register a DELETE route handler.
|
|
1714
|
+
*/
|
|
1715
|
+
delete(path, ...handlers) {
|
|
1716
|
+
return this.registerRoute([
|
|
1717
|
+
"delete"
|
|
1718
|
+
], path, ...handlers);
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Register a PATCH route handler.
|
|
1722
|
+
*/
|
|
1723
|
+
patch(path, ...handlers) {
|
|
1724
|
+
return this.registerRoute([
|
|
1725
|
+
"patch"
|
|
1726
|
+
], path, ...handlers);
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Register an OPTIONS route handler.
|
|
1730
|
+
*/
|
|
1731
|
+
options(path, ...handlers) {
|
|
1732
|
+
return this.registerRoute([
|
|
1733
|
+
"options"
|
|
1734
|
+
], path, ...handlers);
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Register a HEAD route handler.
|
|
1738
|
+
*/
|
|
1739
|
+
head(path, ...handlers) {
|
|
1740
|
+
return this.registerRoute([
|
|
1741
|
+
"head"
|
|
1742
|
+
], path, ...handlers);
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1525
1745
|
* Register custom middleware with optional path restriction.
|
|
1526
1746
|
*
|
|
1527
1747
|
* Use this for:
|
|
@@ -1535,7 +1755,7 @@ var ExpressServer = class {
|
|
|
1535
1755
|
* @returns This instance for method chaining
|
|
1536
1756
|
*/
|
|
1537
1757
|
registerMiddleware(path, middleware) {
|
|
1538
|
-
const routerToUse = this.
|
|
1758
|
+
const routerToUse = this.rootRouter;
|
|
1539
1759
|
if (typeof path === "string") {
|
|
1540
1760
|
const normalizedPath = this.normalizePath(path);
|
|
1541
1761
|
if (normalizedPath) {
|
|
@@ -1558,7 +1778,7 @@ var ExpressServer = class {
|
|
|
1558
1778
|
*/
|
|
1559
1779
|
useMiddleware(...middlewares) {
|
|
1560
1780
|
middlewares.forEach((middleware) => {
|
|
1561
|
-
|
|
1781
|
+
this.rootRouter.use(middleware);
|
|
1562
1782
|
});
|
|
1563
1783
|
return this;
|
|
1564
1784
|
}
|