@catbee/utils 2.0.3 → 2.0.4
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/config/index.cjs +0 -5
- package/config/index.mjs +0 -5
- package/crypto/index.cjs +9 -1
- package/crypto/index.mjs +9 -1
- package/date/index.cjs +276 -0
- package/date/index.d.ts +44 -1
- package/date/index.mjs +273 -1
- package/error/index.cjs +58 -0
- package/error/index.d.ts +62 -0
- package/error/index.mjs +54 -0
- package/index.cjs +7 -0
- package/index.d.ts +1 -0
- package/index.mjs +1 -0
- package/logger/index.cjs +13 -12
- package/logger/index.d.ts +5 -5
- package/logger/index.mjs +13 -12
- package/package.json +6 -2
- package/server/index.cjs +129 -272
- package/server/index.d.ts +22 -59
- package/server/index.mjs +129 -272
- package/types/index.d.ts +0 -22
package/server/index.cjs
CHANGED
|
@@ -305,42 +305,6 @@ var ServerConfigBuilder = class {
|
|
|
305
305
|
return this.setEnabled("requestLogging", false);
|
|
306
306
|
}
|
|
307
307
|
/**
|
|
308
|
-
* Configures server metrics collection and endpoints.
|
|
309
|
-
*
|
|
310
|
-
* @param opts - Metrics configuration options
|
|
311
|
-
* @returns The builder instance for chaining
|
|
312
|
-
* @default - { enable: false, path: '/metrics', withGlobalPrefix: false }
|
|
313
|
-
*
|
|
314
|
-
* @example
|
|
315
|
-
* ```typescript
|
|
316
|
-
* builder.withMetrics({
|
|
317
|
-
* enable: true,
|
|
318
|
-
* path: '/metrics'
|
|
319
|
-
* })
|
|
320
|
-
* ```
|
|
321
|
-
*/
|
|
322
|
-
withMetrics(opts) {
|
|
323
|
-
this.mergeConfig("metrics", opts);
|
|
324
|
-
return this;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Enables Prometheus metrics collection and endpoint
|
|
328
|
-
*
|
|
329
|
-
* @param opts - Optional metrics configuration
|
|
330
|
-
* @returns The builder instance for chaining
|
|
331
|
-
*/
|
|
332
|
-
enableMetrics(opts = {}) {
|
|
333
|
-
return this.setEnabled("metrics", true, opts);
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Disables Prometheus metrics
|
|
337
|
-
*
|
|
338
|
-
* @returns The builder instance for chaining
|
|
339
|
-
*/
|
|
340
|
-
disableMetrics() {
|
|
341
|
-
return this.setEnabled("metrics", false);
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
308
|
* Configures server health check endpoint.
|
|
345
309
|
*
|
|
346
310
|
* @param opts - Health check configuration options
|
|
@@ -360,6 +324,19 @@ var ServerConfigBuilder = class {
|
|
|
360
324
|
return this;
|
|
361
325
|
}
|
|
362
326
|
/**
|
|
327
|
+
* Enables health check endpoint with default or custom settings
|
|
328
|
+
* @param opts - Optional health check configuration
|
|
329
|
+
* @returns The builder instance for chaining
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```typescript
|
|
333
|
+
* builder.disableHealthCheck()
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
disableHealthCheck() {
|
|
337
|
+
return this.setEnabled("healthCheck", false);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
363
340
|
* Configures OpenAPI/Swagger documentation for the API.
|
|
364
341
|
*
|
|
365
342
|
* @param opts - OpenAPI configuration options
|
|
@@ -370,7 +347,7 @@ var ServerConfigBuilder = class {
|
|
|
370
347
|
* ```typescript
|
|
371
348
|
* builder.withOpenApi({
|
|
372
349
|
* enable: true,
|
|
373
|
-
*
|
|
350
|
+
* mountPath: '/api-docs',
|
|
374
351
|
* filePath: './openapi.yaml'
|
|
375
352
|
* })
|
|
376
353
|
* ```
|
|
@@ -710,15 +687,12 @@ var DependencyErrors = {
|
|
|
710
687
|
compression: getDependencyErrorMessage("compression"),
|
|
711
688
|
"express-rate-limit": getDependencyErrorMessage("express-rate-limit"),
|
|
712
689
|
"cookie-parser": getDependencyErrorMessage("cookie-parser"),
|
|
713
|
-
"@scalar/express-api-reference": getDependencyErrorMessage("@scalar/express-api-reference")
|
|
714
|
-
"prom-client": getDependencyErrorMessage("prom-client")
|
|
690
|
+
"@scalar/express-api-reference": getDependencyErrorMessage("@scalar/express-api-reference")
|
|
715
691
|
};
|
|
716
692
|
var ExpressServer = class {
|
|
717
693
|
static {
|
|
718
694
|
__name(this, "ExpressServer");
|
|
719
695
|
}
|
|
720
|
-
/** Prometheus client registry for metrics collection */
|
|
721
|
-
register = null;
|
|
722
696
|
/** HTTP server instance (null when not running) */
|
|
723
697
|
server = null;
|
|
724
698
|
/** Merged configuration with defaults applied */
|
|
@@ -737,16 +711,13 @@ var ExpressServer = class {
|
|
|
737
711
|
connections = /* @__PURE__ */ new Set();
|
|
738
712
|
/** Flag indicating if the server is shutting down */
|
|
739
713
|
isShuttingDown = false;
|
|
714
|
+
/** Flag indicating if graceful shutdown handlers are registered */
|
|
715
|
+
gracefulShutdownRegistered = false;
|
|
740
716
|
/**
|
|
741
717
|
* Collection of registered health check functions.
|
|
742
718
|
* These are executed when the health check endpoint is accessed.
|
|
743
719
|
*/
|
|
744
720
|
healthChecks = [];
|
|
745
|
-
/** Prometheus metrics for monitoring */
|
|
746
|
-
requestCounter;
|
|
747
|
-
routeTimings;
|
|
748
|
-
requestSizes;
|
|
749
|
-
clientIPs;
|
|
750
721
|
/** Promise that resolves when initialization (middleware + routes) is complete */
|
|
751
722
|
initPromise;
|
|
752
723
|
/**
|
|
@@ -762,7 +733,6 @@ var ExpressServer = class {
|
|
|
762
733
|
*
|
|
763
734
|
* Default Monitoring:
|
|
764
735
|
* - Request/Response logging
|
|
765
|
-
* - Prometheus metrics
|
|
766
736
|
* - Health checks
|
|
767
737
|
* - Request tracing
|
|
768
738
|
*/
|
|
@@ -780,82 +750,6 @@ var ExpressServer = class {
|
|
|
780
750
|
logger.getLogger().error(msg);
|
|
781
751
|
throw new Error(msg);
|
|
782
752
|
}
|
|
783
|
-
const safeAppName = (this.config.appName || "express_app").toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
784
|
-
if (this.config.metrics?.enable) {
|
|
785
|
-
const client = async.optionalRequire("prom-client");
|
|
786
|
-
if (!client) {
|
|
787
|
-
this.throwDependancyError("prom-client");
|
|
788
|
-
}
|
|
789
|
-
this.register = new client.Registry();
|
|
790
|
-
this.requestCounter = new client.Counter({
|
|
791
|
-
name: `${safeAppName}_http_requests_total`,
|
|
792
|
-
help: "Total HTTP requests",
|
|
793
|
-
labelNames: [
|
|
794
|
-
"method",
|
|
795
|
-
"route",
|
|
796
|
-
"status"
|
|
797
|
-
],
|
|
798
|
-
registers: [
|
|
799
|
-
this.register
|
|
800
|
-
]
|
|
801
|
-
});
|
|
802
|
-
this.routeTimings = new client.Histogram({
|
|
803
|
-
name: `${safeAppName}_http_request_duration_seconds`,
|
|
804
|
-
help: "Duration of HTTP requests by route",
|
|
805
|
-
labelNames: [
|
|
806
|
-
"method",
|
|
807
|
-
"route",
|
|
808
|
-
"status"
|
|
809
|
-
],
|
|
810
|
-
buckets: [
|
|
811
|
-
0.1,
|
|
812
|
-
0.3,
|
|
813
|
-
0.5,
|
|
814
|
-
0.7,
|
|
815
|
-
1,
|
|
816
|
-
3,
|
|
817
|
-
5,
|
|
818
|
-
7,
|
|
819
|
-
10
|
|
820
|
-
],
|
|
821
|
-
registers: [
|
|
822
|
-
this.register
|
|
823
|
-
]
|
|
824
|
-
});
|
|
825
|
-
this.requestSizes = new client.Histogram({
|
|
826
|
-
name: `${safeAppName}_http_request_size_bytes`,
|
|
827
|
-
help: "Size of HTTP request bodies",
|
|
828
|
-
labelNames: [
|
|
829
|
-
"method",
|
|
830
|
-
"route"
|
|
831
|
-
],
|
|
832
|
-
buckets: [
|
|
833
|
-
100,
|
|
834
|
-
1e3,
|
|
835
|
-
1e4,
|
|
836
|
-
1e5,
|
|
837
|
-
1e6
|
|
838
|
-
],
|
|
839
|
-
registers: [
|
|
840
|
-
this.register
|
|
841
|
-
]
|
|
842
|
-
});
|
|
843
|
-
this.clientIPs = new client.Counter({
|
|
844
|
-
name: `${safeAppName}_http_client_ip_total`,
|
|
845
|
-
help: "Client IP request counter",
|
|
846
|
-
labelNames: [
|
|
847
|
-
"ip",
|
|
848
|
-
"method"
|
|
849
|
-
],
|
|
850
|
-
registers: [
|
|
851
|
-
this.register
|
|
852
|
-
]
|
|
853
|
-
});
|
|
854
|
-
client.collectDefaultMetrics({
|
|
855
|
-
register: this.register,
|
|
856
|
-
prefix: `${safeAppName}_`
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
753
|
if (config$1?.healthCheck?.checks) {
|
|
860
754
|
this.healthChecks.push(...config$1.healthCheck.checks);
|
|
861
755
|
}
|
|
@@ -928,7 +822,6 @@ var ExpressServer = class {
|
|
|
928
822
|
this.setupBodyParsingMiddleware();
|
|
929
823
|
this.setupCookieParsingMiddleware();
|
|
930
824
|
await this.setupOpenApiMiddleware();
|
|
931
|
-
this.setupMetricsMiddleware();
|
|
932
825
|
}
|
|
933
826
|
/**
|
|
934
827
|
* Set up basic middleware (trust proxy, request ID, context).
|
|
@@ -951,6 +844,7 @@ var ExpressServer = class {
|
|
|
951
844
|
if (this.isShuttingDown) {
|
|
952
845
|
res.setHeader("Connection", "close");
|
|
953
846
|
res.status(httpStatusCodes.HttpStatusCodes.SERVICE_UNAVAILABLE).json(new exception.ServiceUnavailableException("Server is shutting down"));
|
|
847
|
+
return;
|
|
954
848
|
}
|
|
955
849
|
next();
|
|
956
850
|
});
|
|
@@ -962,7 +856,7 @@ var ExpressServer = class {
|
|
|
962
856
|
if (this.config.helmet) {
|
|
963
857
|
const helmet = async.optionalRequire("helmet");
|
|
964
858
|
if (!helmet) {
|
|
965
|
-
this.
|
|
859
|
+
this.throwDependencyError("helmet");
|
|
966
860
|
}
|
|
967
861
|
if (typeof this.config.helmet === "object") {
|
|
968
862
|
this.app.use(helmet(this.config.helmet));
|
|
@@ -973,7 +867,7 @@ var ExpressServer = class {
|
|
|
973
867
|
if (this.config.cors) {
|
|
974
868
|
const cors = async.optionalRequire("cors");
|
|
975
869
|
if (!cors) {
|
|
976
|
-
this.
|
|
870
|
+
this.throwDependencyError("cors");
|
|
977
871
|
}
|
|
978
872
|
this.app.use(cors(this.config.cors === true ? {} : this.config.cors));
|
|
979
873
|
}
|
|
@@ -1025,7 +919,7 @@ var ExpressServer = class {
|
|
|
1025
919
|
if (this.config.rateLimit?.enable) {
|
|
1026
920
|
const rateLimit = async.optionalRequire("express-rate-limit");
|
|
1027
921
|
if (!rateLimit) {
|
|
1028
|
-
this.
|
|
922
|
+
this.throwDependencyError("express-rate-limit");
|
|
1029
923
|
}
|
|
1030
924
|
this.app.use(rateLimit({
|
|
1031
925
|
windowMs: this.config.rateLimit.windowMs ?? 15 * 60 * 1e3,
|
|
@@ -1074,7 +968,7 @@ var ExpressServer = class {
|
|
|
1074
968
|
if (this.config.compression) {
|
|
1075
969
|
const compression = async.optionalRequire("compression");
|
|
1076
970
|
if (!compression) {
|
|
1077
|
-
this.
|
|
971
|
+
this.throwDependencyError("compression");
|
|
1078
972
|
}
|
|
1079
973
|
if (typeof this.config.compression === "object") {
|
|
1080
974
|
this.app.use(compression(this.config.compression));
|
|
@@ -1130,7 +1024,7 @@ var ExpressServer = class {
|
|
|
1130
1024
|
if (this.config.cookieParser) {
|
|
1131
1025
|
const cookieParser = async.optionalRequire("cookie-parser");
|
|
1132
1026
|
if (!cookieParser) {
|
|
1133
|
-
this.
|
|
1027
|
+
this.throwDependencyError("cookie-parser");
|
|
1134
1028
|
}
|
|
1135
1029
|
if (typeof this.config.cookieParser === "object") {
|
|
1136
1030
|
this.app.use(cookieParser(void 0, this.config.cookieParser));
|
|
@@ -1164,7 +1058,7 @@ var ExpressServer = class {
|
|
|
1164
1058
|
}
|
|
1165
1059
|
const apiReference = async.optionalRequire("@scalar/express-api-reference")?.apiReference;
|
|
1166
1060
|
if (!apiReference) {
|
|
1167
|
-
this.
|
|
1061
|
+
this.throwDependencyError("@scalar/express-api-reference", getDependencyErrorMessage("@scalar/express-api-reference", "OpenAPI docs"));
|
|
1168
1062
|
}
|
|
1169
1063
|
this.app.use(openApiMountPath, apiReference({
|
|
1170
1064
|
spec: {
|
|
@@ -1185,50 +1079,10 @@ var ExpressServer = class {
|
|
|
1185
1079
|
}
|
|
1186
1080
|
}
|
|
1187
1081
|
/**
|
|
1188
|
-
* Set up metrics tracking middleware.
|
|
1189
|
-
*/
|
|
1190
|
-
setupMetricsMiddleware() {
|
|
1191
|
-
if (this.config.metrics?.enable) {
|
|
1192
|
-
this.app.use((req, res, next) => {
|
|
1193
|
-
const start = process.hrtime();
|
|
1194
|
-
this.clientIPs?.inc({
|
|
1195
|
-
ip: req.ip,
|
|
1196
|
-
method: req.method
|
|
1197
|
-
});
|
|
1198
|
-
const cl = req.headers["content-length"];
|
|
1199
|
-
if (cl) {
|
|
1200
|
-
const size = Number(cl);
|
|
1201
|
-
if (!Number.isNaN(size) && size >= 0) {
|
|
1202
|
-
const route = this.normalizeRouteForMetrics(req, res);
|
|
1203
|
-
this.requestSizes?.observe({
|
|
1204
|
-
method: req.method,
|
|
1205
|
-
route
|
|
1206
|
-
}, size);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
res.once("finish", () => {
|
|
1210
|
-
const [seconds, nanoseconds] = process.hrtime(start);
|
|
1211
|
-
const finalRoute = this.normalizeRouteForMetrics(req, res);
|
|
1212
|
-
this.requestCounter?.inc({
|
|
1213
|
-
method: req.method,
|
|
1214
|
-
route: finalRoute,
|
|
1215
|
-
status: res.statusCode.toString()
|
|
1216
|
-
});
|
|
1217
|
-
this.routeTimings?.observe({
|
|
1218
|
-
method: req.method,
|
|
1219
|
-
route: finalRoute,
|
|
1220
|
-
status: res.statusCode.toString()
|
|
1221
|
-
}, seconds + nanoseconds / 1e9);
|
|
1222
|
-
});
|
|
1223
|
-
next();
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
/**
|
|
1228
1082
|
* Configure server routes and error handling.
|
|
1229
1083
|
* Sets up in following order:
|
|
1230
1084
|
*
|
|
1231
|
-
* 1. Built-in routes (health
|
|
1085
|
+
* 1. Built-in routes (health)
|
|
1232
1086
|
* 2. Application routes
|
|
1233
1087
|
* 3. 404 handler
|
|
1234
1088
|
* 4. Error handler
|
|
@@ -1238,13 +1092,6 @@ var ExpressServer = class {
|
|
|
1238
1092
|
this.app.get(healthCheckPath, async (_req, res) => {
|
|
1239
1093
|
return this.handleHealthCheckRequest(res);
|
|
1240
1094
|
});
|
|
1241
|
-
if (this.config.metrics?.enable) {
|
|
1242
|
-
const metricsPath = this.normalizePath(this.config.metrics.path ?? "/metrics", this.config.metrics?.withGlobalPrefix);
|
|
1243
|
-
this.app.get(metricsPath, async (_req, res) => {
|
|
1244
|
-
res.set("Content-Type", this.register.contentType);
|
|
1245
|
-
res.end(await this.register.metrics());
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
1095
|
const routerToUse = this.externalRouter || this.rootRouter;
|
|
1249
1096
|
this.app.use(this.globalPrefix, routerToUse);
|
|
1250
1097
|
await this.runHook("afterRoutes", this.app);
|
|
@@ -1391,6 +1238,10 @@ var ExpressServer = class {
|
|
|
1391
1238
|
* @throws Error if server fails to start or port is already in use
|
|
1392
1239
|
*/
|
|
1393
1240
|
async start() {
|
|
1241
|
+
if (this.server) {
|
|
1242
|
+
logger.getLogger().warn("Server is already running, returning existing instance");
|
|
1243
|
+
return this.server;
|
|
1244
|
+
}
|
|
1394
1245
|
await this.initPromise;
|
|
1395
1246
|
await this.runHook("beforeStart", this.app);
|
|
1396
1247
|
return new Promise((resolve, reject) => {
|
|
@@ -1463,12 +1314,9 @@ var ExpressServer = class {
|
|
|
1463
1314
|
const host = this.formatHostForUrl(this.config.host || "localhost");
|
|
1464
1315
|
const url = `${protocol}://${host}:${port}`;
|
|
1465
1316
|
logger.getLogger().info(`Server running on ${url}`);
|
|
1466
|
-
if (this.config.healthCheck?.path) {
|
|
1317
|
+
if (this.config.healthCheck && this.config.healthCheck?.path) {
|
|
1467
1318
|
logger.getLogger().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`);
|
|
1468
1319
|
}
|
|
1469
|
-
if (this.config.metrics?.enable && this.config.metrics.path) {
|
|
1470
|
-
logger.getLogger().info(`Metrics available at ${url}${this.normalizePath(this.config.metrics.path, this.config.metrics.withGlobalPrefix)}`);
|
|
1471
|
-
}
|
|
1472
1320
|
if (this.config.openApi?.enable) {
|
|
1473
1321
|
logger.getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
|
|
1474
1322
|
}
|
|
@@ -1495,50 +1343,70 @@ var ExpressServer = class {
|
|
|
1495
1343
|
return;
|
|
1496
1344
|
}
|
|
1497
1345
|
if (this.isShuttingDown) {
|
|
1498
|
-
logger.getLogger().warn("
|
|
1346
|
+
logger.getLogger().warn("Shutdown already in progress");
|
|
1499
1347
|
return;
|
|
1500
1348
|
}
|
|
1501
1349
|
this.isShuttingDown = true;
|
|
1502
1350
|
await this.runHook("beforeStop", this.server);
|
|
1503
1351
|
try {
|
|
1504
|
-
await this.gracefulShutdown();
|
|
1505
|
-
} catch (err) {
|
|
1506
|
-
logger.getLogger().error({
|
|
1507
|
-
err
|
|
1508
|
-
}, "Graceful shutdown timed out");
|
|
1509
|
-
if (force) {
|
|
1510
|
-
logger.getLogger().warn("Forcing connection destroy due to shutdown timeout");
|
|
1511
|
-
}
|
|
1352
|
+
await this.gracefulShutdown(force);
|
|
1512
1353
|
} finally {
|
|
1513
|
-
|
|
1354
|
+
this.isShuttingDown = false;
|
|
1514
1355
|
}
|
|
1515
1356
|
}
|
|
1516
1357
|
/**
|
|
1517
1358
|
* Perform graceful server shutdown with timeout.
|
|
1518
1359
|
*/
|
|
1519
|
-
async gracefulShutdown() {
|
|
1360
|
+
async gracefulShutdown(force = false) {
|
|
1520
1361
|
const shutdownTimeout = 1e4;
|
|
1362
|
+
const server = this.server;
|
|
1363
|
+
let timer;
|
|
1364
|
+
let afterStopCalled = false;
|
|
1365
|
+
const runAfterStop = /* @__PURE__ */ __name(async () => {
|
|
1366
|
+
if (afterStopCalled) return;
|
|
1367
|
+
afterStopCalled = true;
|
|
1368
|
+
await this.runHook("afterStop");
|
|
1369
|
+
}, "runAfterStop");
|
|
1521
1370
|
const serverClosePromise = new Promise((resolve, reject) => {
|
|
1522
|
-
|
|
1371
|
+
server.close(async (err) => {
|
|
1372
|
+
if (timer) clearTimeout(timer);
|
|
1523
1373
|
if (err) {
|
|
1524
1374
|
logger.getLogger().error({
|
|
1525
1375
|
err
|
|
1526
|
-
}, "Error
|
|
1376
|
+
}, "Error during server shutdown");
|
|
1527
1377
|
reject(err);
|
|
1528
1378
|
return;
|
|
1529
1379
|
}
|
|
1530
1380
|
this.server = null;
|
|
1531
|
-
this.isShuttingDown = false;
|
|
1532
1381
|
logger.getLogger().info("Server stopped gracefully");
|
|
1533
|
-
await
|
|
1382
|
+
await runAfterStop();
|
|
1534
1383
|
resolve();
|
|
1535
1384
|
});
|
|
1536
1385
|
});
|
|
1537
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1386
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1387
|
+
timer = setTimeout(() => reject(new Error("Shutdown timeout")), shutdownTimeout);
|
|
1388
|
+
});
|
|
1389
|
+
try {
|
|
1390
|
+
await Promise.race([
|
|
1391
|
+
serverClosePromise,
|
|
1392
|
+
timeoutPromise
|
|
1393
|
+
]);
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
if (timer) clearTimeout(timer);
|
|
1396
|
+
logger.getLogger().warn({
|
|
1397
|
+
err
|
|
1398
|
+
}, "Graceful shutdown timed out");
|
|
1399
|
+
if (!force) {
|
|
1400
|
+
throw err;
|
|
1401
|
+
}
|
|
1402
|
+
logger.getLogger().warn("Escalating to forced shutdown");
|
|
1403
|
+
server.closeIdleConnections?.();
|
|
1404
|
+
server.closeAllConnections?.();
|
|
1405
|
+
await this.destroyConnections();
|
|
1406
|
+
this.server = null;
|
|
1407
|
+
logger.getLogger().info("Forced shutdown completed");
|
|
1408
|
+
await runAfterStop();
|
|
1409
|
+
}
|
|
1542
1410
|
}
|
|
1543
1411
|
/**
|
|
1544
1412
|
* Enable graceful shutdown on OS signals for production deployment.
|
|
@@ -1555,31 +1423,64 @@ var ExpressServer = class {
|
|
|
1555
1423
|
"SIGINT",
|
|
1556
1424
|
"SIGTERM"
|
|
1557
1425
|
]) {
|
|
1426
|
+
if (this.gracefulShutdownRegistered) {
|
|
1427
|
+
logger.getLogger().warn("Graceful shutdown handlers are already registered");
|
|
1428
|
+
return this;
|
|
1429
|
+
}
|
|
1430
|
+
let signalHandled = false;
|
|
1558
1431
|
signals.forEach((signal) => {
|
|
1559
1432
|
process.on(signal, async () => {
|
|
1433
|
+
if (signalHandled) {
|
|
1434
|
+
logger.getLogger().warn(`Ignoring duplicate ${signal}`);
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
signalHandled = true;
|
|
1560
1438
|
logger.getLogger().info(`Received ${signal}, initiating graceful shutdown...`);
|
|
1561
1439
|
try {
|
|
1562
|
-
await this.stop();
|
|
1440
|
+
await this.stop(true);
|
|
1563
1441
|
process.exit(0);
|
|
1564
1442
|
} catch (err) {
|
|
1565
|
-
logger.getLogger().
|
|
1443
|
+
logger.getLogger().fatal({
|
|
1566
1444
|
err
|
|
1567
|
-
}, "
|
|
1568
|
-
|
|
1569
|
-
await this.stop(true);
|
|
1570
|
-
process.exit(1);
|
|
1571
|
-
} catch (forceError) {
|
|
1572
|
-
logger.getLogger().fatal({
|
|
1573
|
-
forceError
|
|
1574
|
-
}, "Forced shutdown failed, exiting hard");
|
|
1575
|
-
process.exit(1);
|
|
1576
|
-
}
|
|
1445
|
+
}, "Shutdown failed");
|
|
1446
|
+
process.exit(1);
|
|
1577
1447
|
}
|
|
1578
1448
|
});
|
|
1579
1449
|
});
|
|
1450
|
+
this.gracefulShutdownRegistered = true;
|
|
1580
1451
|
return this;
|
|
1581
1452
|
}
|
|
1582
1453
|
/**
|
|
1454
|
+
* Destroy all active connections (gracefully if possible).
|
|
1455
|
+
* If a connection does not close cleanly, it will be force-destroyed.
|
|
1456
|
+
*/
|
|
1457
|
+
async destroyConnections() {
|
|
1458
|
+
if (!this.connections.size) {
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const sockets = [
|
|
1462
|
+
...this.connections
|
|
1463
|
+
];
|
|
1464
|
+
await Promise.allSettled(sockets.map((socket) => new Promise((resolve) => {
|
|
1465
|
+
socket.end();
|
|
1466
|
+
const timer = setTimeout(() => {
|
|
1467
|
+
socket.destroy();
|
|
1468
|
+
resolve();
|
|
1469
|
+
}, 1e3);
|
|
1470
|
+
socket.once("close", () => {
|
|
1471
|
+
clearTimeout(timer);
|
|
1472
|
+
resolve();
|
|
1473
|
+
});
|
|
1474
|
+
socket.once("error", () => {
|
|
1475
|
+
clearTimeout(timer);
|
|
1476
|
+
socket.destroy();
|
|
1477
|
+
resolve();
|
|
1478
|
+
});
|
|
1479
|
+
})));
|
|
1480
|
+
this.connections.clear();
|
|
1481
|
+
logger.getLogger().info(`Closed ${sockets.length} active connection(s)`);
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1583
1484
|
* Set an externally created base router.
|
|
1584
1485
|
* This will override the internal rootRouter.
|
|
1585
1486
|
*/
|
|
@@ -1607,14 +1508,15 @@ var ExpressServer = class {
|
|
|
1607
1508
|
*/
|
|
1608
1509
|
registerRoute(methods, path, ...handlers) {
|
|
1609
1510
|
const fullPath = this.normalizePath(path, true);
|
|
1511
|
+
const routerToUse = this.externalRouter || this.rootRouter;
|
|
1610
1512
|
const methodMap = {
|
|
1611
|
-
get:
|
|
1612
|
-
post:
|
|
1613
|
-
put:
|
|
1614
|
-
delete:
|
|
1615
|
-
patch:
|
|
1616
|
-
options:
|
|
1617
|
-
head:
|
|
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)
|
|
1618
1520
|
};
|
|
1619
1521
|
methods.forEach((m) => {
|
|
1620
1522
|
const fn = methodMap[m];
|
|
@@ -1640,15 +1542,16 @@ var ExpressServer = class {
|
|
|
1640
1542
|
* @returns This instance for method chaining
|
|
1641
1543
|
*/
|
|
1642
1544
|
registerMiddleware(path, middleware) {
|
|
1545
|
+
const routerToUse = this.externalRouter || this.rootRouter;
|
|
1643
1546
|
if (typeof path === "string") {
|
|
1644
1547
|
const normalizedPath = this.normalizePath(path);
|
|
1645
1548
|
if (normalizedPath) {
|
|
1646
|
-
|
|
1549
|
+
routerToUse.use(normalizedPath, middleware);
|
|
1647
1550
|
} else {
|
|
1648
|
-
|
|
1551
|
+
routerToUse.use(middleware);
|
|
1649
1552
|
}
|
|
1650
1553
|
} else {
|
|
1651
|
-
|
|
1554
|
+
routerToUse.use(path);
|
|
1652
1555
|
}
|
|
1653
1556
|
return this;
|
|
1654
1557
|
}
|
|
@@ -1662,22 +1565,11 @@ var ExpressServer = class {
|
|
|
1662
1565
|
*/
|
|
1663
1566
|
useMiddleware(...middlewares) {
|
|
1664
1567
|
middlewares.forEach((middleware) => {
|
|
1665
|
-
this.
|
|
1568
|
+
(this.externalRouter || this.rootRouter).use(middleware);
|
|
1666
1569
|
});
|
|
1667
1570
|
return this;
|
|
1668
1571
|
}
|
|
1669
1572
|
/**
|
|
1670
|
-
* Get Prometheus registry (to add custom counters/histograms)
|
|
1671
|
-
*
|
|
1672
|
-
* @return {*} {client.Registry}
|
|
1673
|
-
*/
|
|
1674
|
-
getMetricsRegistry() {
|
|
1675
|
-
if (!this.config.metrics?.enable) {
|
|
1676
|
-
logger.getLogger().warn("Metrics are not enabled in the server configuration \nPlease enable metrics to use this feature.");
|
|
1677
|
-
}
|
|
1678
|
-
return this.register;
|
|
1679
|
-
}
|
|
1680
|
-
/**
|
|
1681
1573
|
* Get server configuration
|
|
1682
1574
|
*
|
|
1683
1575
|
* @return {*} {CatbeeServerConfig}
|
|
@@ -1826,41 +1718,6 @@ var ExpressServer = class {
|
|
|
1826
1718
|
}
|
|
1827
1719
|
return sanitize(prefix + "/" + path);
|
|
1828
1720
|
}
|
|
1829
|
-
normalizeRouteForMetrics(req, res) {
|
|
1830
|
-
if (req?.route?.path) {
|
|
1831
|
-
return req.route.path;
|
|
1832
|
-
}
|
|
1833
|
-
if (res.statusCode === 404) return "/404";
|
|
1834
|
-
const path = (req.path || "unknown").split("?")[0];
|
|
1835
|
-
return path.replace(/\/[0-9]+/g, "/:id").replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, "/:uuid");
|
|
1836
|
-
}
|
|
1837
|
-
/**
|
|
1838
|
-
* Destroy all active connections (gracefully if possible).
|
|
1839
|
-
* If a connection does not close cleanly, it will be force-destroyed.
|
|
1840
|
-
*/
|
|
1841
|
-
async destroyConnections() {
|
|
1842
|
-
const total = this.connections.size;
|
|
1843
|
-
if (total === 0) {
|
|
1844
|
-
logger.getLogger().debug("No active connections to close");
|
|
1845
|
-
return;
|
|
1846
|
-
}
|
|
1847
|
-
const timeoutMs = 5e3;
|
|
1848
|
-
await Promise.race([
|
|
1849
|
-
Promise.all(Array.from(this.connections).map((conn) => new Promise((resolve) => {
|
|
1850
|
-
conn.end(() => {
|
|
1851
|
-
if (!conn.destroyed) conn.destroy();
|
|
1852
|
-
resolve();
|
|
1853
|
-
});
|
|
1854
|
-
conn.on("error", () => {
|
|
1855
|
-
conn.destroy();
|
|
1856
|
-
resolve();
|
|
1857
|
-
});
|
|
1858
|
-
}))),
|
|
1859
|
-
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
1860
|
-
]);
|
|
1861
|
-
this.connections.clear();
|
|
1862
|
-
logger.getLogger().info(`Closed ${total} active connections`);
|
|
1863
|
-
}
|
|
1864
1721
|
async validateHttpsFiles() {
|
|
1865
1722
|
if (!await fs.fileExists(this.config.https.key)) {
|
|
1866
1723
|
const msg = `HTTPS key file not found: ${this.config.https.key}`;
|
|
@@ -1884,7 +1741,7 @@ var ExpressServer = class {
|
|
|
1884
1741
|
}
|
|
1885
1742
|
return false;
|
|
1886
1743
|
}
|
|
1887
|
-
|
|
1744
|
+
throwDependencyError(packageName, msg) {
|
|
1888
1745
|
logger.getLogger().error({
|
|
1889
1746
|
command: `npm install ${packageName}`
|
|
1890
1747
|
}, msg || DependencyErrors[packageName]);
|