@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.mjs
CHANGED
|
@@ -298,42 +298,6 @@ var ServerConfigBuilder = class {
|
|
|
298
298
|
return this.setEnabled("requestLogging", false);
|
|
299
299
|
}
|
|
300
300
|
/**
|
|
301
|
-
* Configures server metrics collection and endpoints.
|
|
302
|
-
*
|
|
303
|
-
* @param opts - Metrics configuration options
|
|
304
|
-
* @returns The builder instance for chaining
|
|
305
|
-
* @default - { enable: false, path: '/metrics', withGlobalPrefix: false }
|
|
306
|
-
*
|
|
307
|
-
* @example
|
|
308
|
-
* ```typescript
|
|
309
|
-
* builder.withMetrics({
|
|
310
|
-
* enable: true,
|
|
311
|
-
* path: '/metrics'
|
|
312
|
-
* })
|
|
313
|
-
* ```
|
|
314
|
-
*/
|
|
315
|
-
withMetrics(opts) {
|
|
316
|
-
this.mergeConfig("metrics", opts);
|
|
317
|
-
return this;
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Enables Prometheus metrics collection and endpoint
|
|
321
|
-
*
|
|
322
|
-
* @param opts - Optional metrics configuration
|
|
323
|
-
* @returns The builder instance for chaining
|
|
324
|
-
*/
|
|
325
|
-
enableMetrics(opts = {}) {
|
|
326
|
-
return this.setEnabled("metrics", true, opts);
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Disables Prometheus metrics
|
|
330
|
-
*
|
|
331
|
-
* @returns The builder instance for chaining
|
|
332
|
-
*/
|
|
333
|
-
disableMetrics() {
|
|
334
|
-
return this.setEnabled("metrics", false);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
301
|
* Configures server health check endpoint.
|
|
338
302
|
*
|
|
339
303
|
* @param opts - Health check configuration options
|
|
@@ -353,6 +317,19 @@ var ServerConfigBuilder = class {
|
|
|
353
317
|
return this;
|
|
354
318
|
}
|
|
355
319
|
/**
|
|
320
|
+
* Enables health check endpoint with default or custom settings
|
|
321
|
+
* @param opts - Optional health check configuration
|
|
322
|
+
* @returns The builder instance for chaining
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```typescript
|
|
326
|
+
* builder.disableHealthCheck()
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
disableHealthCheck() {
|
|
330
|
+
return this.setEnabled("healthCheck", false);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
356
333
|
* Configures OpenAPI/Swagger documentation for the API.
|
|
357
334
|
*
|
|
358
335
|
* @param opts - OpenAPI configuration options
|
|
@@ -363,7 +340,7 @@ var ServerConfigBuilder = class {
|
|
|
363
340
|
* ```typescript
|
|
364
341
|
* builder.withOpenApi({
|
|
365
342
|
* enable: true,
|
|
366
|
-
*
|
|
343
|
+
* mountPath: '/api-docs',
|
|
367
344
|
* filePath: './openapi.yaml'
|
|
368
345
|
* })
|
|
369
346
|
* ```
|
|
@@ -703,15 +680,12 @@ var DependencyErrors = {
|
|
|
703
680
|
compression: getDependencyErrorMessage("compression"),
|
|
704
681
|
"express-rate-limit": getDependencyErrorMessage("express-rate-limit"),
|
|
705
682
|
"cookie-parser": getDependencyErrorMessage("cookie-parser"),
|
|
706
|
-
"@scalar/express-api-reference": getDependencyErrorMessage("@scalar/express-api-reference")
|
|
707
|
-
"prom-client": getDependencyErrorMessage("prom-client")
|
|
683
|
+
"@scalar/express-api-reference": getDependencyErrorMessage("@scalar/express-api-reference")
|
|
708
684
|
};
|
|
709
685
|
var ExpressServer = class {
|
|
710
686
|
static {
|
|
711
687
|
__name(this, "ExpressServer");
|
|
712
688
|
}
|
|
713
|
-
/** Prometheus client registry for metrics collection */
|
|
714
|
-
register = null;
|
|
715
689
|
/** HTTP server instance (null when not running) */
|
|
716
690
|
server = null;
|
|
717
691
|
/** Merged configuration with defaults applied */
|
|
@@ -730,16 +704,13 @@ var ExpressServer = class {
|
|
|
730
704
|
connections = /* @__PURE__ */ new Set();
|
|
731
705
|
/** Flag indicating if the server is shutting down */
|
|
732
706
|
isShuttingDown = false;
|
|
707
|
+
/** Flag indicating if graceful shutdown handlers are registered */
|
|
708
|
+
gracefulShutdownRegistered = false;
|
|
733
709
|
/**
|
|
734
710
|
* Collection of registered health check functions.
|
|
735
711
|
* These are executed when the health check endpoint is accessed.
|
|
736
712
|
*/
|
|
737
713
|
healthChecks = [];
|
|
738
|
-
/** Prometheus metrics for monitoring */
|
|
739
|
-
requestCounter;
|
|
740
|
-
routeTimings;
|
|
741
|
-
requestSizes;
|
|
742
|
-
clientIPs;
|
|
743
714
|
/** Promise that resolves when initialization (middleware + routes) is complete */
|
|
744
715
|
initPromise;
|
|
745
716
|
/**
|
|
@@ -755,7 +726,6 @@ var ExpressServer = class {
|
|
|
755
726
|
*
|
|
756
727
|
* Default Monitoring:
|
|
757
728
|
* - Request/Response logging
|
|
758
|
-
* - Prometheus metrics
|
|
759
729
|
* - Health checks
|
|
760
730
|
* - Request tracing
|
|
761
731
|
*/
|
|
@@ -773,82 +743,6 @@ var ExpressServer = class {
|
|
|
773
743
|
getLogger().error(msg);
|
|
774
744
|
throw new Error(msg);
|
|
775
745
|
}
|
|
776
|
-
const safeAppName = (this.config.appName || "express_app").toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
777
|
-
if (this.config.metrics?.enable) {
|
|
778
|
-
const client = optionalRequire("prom-client");
|
|
779
|
-
if (!client) {
|
|
780
|
-
this.throwDependancyError("prom-client");
|
|
781
|
-
}
|
|
782
|
-
this.register = new client.Registry();
|
|
783
|
-
this.requestCounter = new client.Counter({
|
|
784
|
-
name: `${safeAppName}_http_requests_total`,
|
|
785
|
-
help: "Total HTTP requests",
|
|
786
|
-
labelNames: [
|
|
787
|
-
"method",
|
|
788
|
-
"route",
|
|
789
|
-
"status"
|
|
790
|
-
],
|
|
791
|
-
registers: [
|
|
792
|
-
this.register
|
|
793
|
-
]
|
|
794
|
-
});
|
|
795
|
-
this.routeTimings = new client.Histogram({
|
|
796
|
-
name: `${safeAppName}_http_request_duration_seconds`,
|
|
797
|
-
help: "Duration of HTTP requests by route",
|
|
798
|
-
labelNames: [
|
|
799
|
-
"method",
|
|
800
|
-
"route",
|
|
801
|
-
"status"
|
|
802
|
-
],
|
|
803
|
-
buckets: [
|
|
804
|
-
0.1,
|
|
805
|
-
0.3,
|
|
806
|
-
0.5,
|
|
807
|
-
0.7,
|
|
808
|
-
1,
|
|
809
|
-
3,
|
|
810
|
-
5,
|
|
811
|
-
7,
|
|
812
|
-
10
|
|
813
|
-
],
|
|
814
|
-
registers: [
|
|
815
|
-
this.register
|
|
816
|
-
]
|
|
817
|
-
});
|
|
818
|
-
this.requestSizes = new client.Histogram({
|
|
819
|
-
name: `${safeAppName}_http_request_size_bytes`,
|
|
820
|
-
help: "Size of HTTP request bodies",
|
|
821
|
-
labelNames: [
|
|
822
|
-
"method",
|
|
823
|
-
"route"
|
|
824
|
-
],
|
|
825
|
-
buckets: [
|
|
826
|
-
100,
|
|
827
|
-
1e3,
|
|
828
|
-
1e4,
|
|
829
|
-
1e5,
|
|
830
|
-
1e6
|
|
831
|
-
],
|
|
832
|
-
registers: [
|
|
833
|
-
this.register
|
|
834
|
-
]
|
|
835
|
-
});
|
|
836
|
-
this.clientIPs = new client.Counter({
|
|
837
|
-
name: `${safeAppName}_http_client_ip_total`,
|
|
838
|
-
help: "Client IP request counter",
|
|
839
|
-
labelNames: [
|
|
840
|
-
"ip",
|
|
841
|
-
"method"
|
|
842
|
-
],
|
|
843
|
-
registers: [
|
|
844
|
-
this.register
|
|
845
|
-
]
|
|
846
|
-
});
|
|
847
|
-
client.collectDefaultMetrics({
|
|
848
|
-
register: this.register,
|
|
849
|
-
prefix: `${safeAppName}_`
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
746
|
if (config?.healthCheck?.checks) {
|
|
853
747
|
this.healthChecks.push(...config.healthCheck.checks);
|
|
854
748
|
}
|
|
@@ -921,7 +815,6 @@ var ExpressServer = class {
|
|
|
921
815
|
this.setupBodyParsingMiddleware();
|
|
922
816
|
this.setupCookieParsingMiddleware();
|
|
923
817
|
await this.setupOpenApiMiddleware();
|
|
924
|
-
this.setupMetricsMiddleware();
|
|
925
818
|
}
|
|
926
819
|
/**
|
|
927
820
|
* Set up basic middleware (trust proxy, request ID, context).
|
|
@@ -944,6 +837,7 @@ var ExpressServer = class {
|
|
|
944
837
|
if (this.isShuttingDown) {
|
|
945
838
|
res.setHeader("Connection", "close");
|
|
946
839
|
res.status(HttpStatusCodes.SERVICE_UNAVAILABLE).json(new ServiceUnavailableException("Server is shutting down"));
|
|
840
|
+
return;
|
|
947
841
|
}
|
|
948
842
|
next();
|
|
949
843
|
});
|
|
@@ -955,7 +849,7 @@ var ExpressServer = class {
|
|
|
955
849
|
if (this.config.helmet) {
|
|
956
850
|
const helmet = optionalRequire("helmet");
|
|
957
851
|
if (!helmet) {
|
|
958
|
-
this.
|
|
852
|
+
this.throwDependencyError("helmet");
|
|
959
853
|
}
|
|
960
854
|
if (typeof this.config.helmet === "object") {
|
|
961
855
|
this.app.use(helmet(this.config.helmet));
|
|
@@ -966,7 +860,7 @@ var ExpressServer = class {
|
|
|
966
860
|
if (this.config.cors) {
|
|
967
861
|
const cors = optionalRequire("cors");
|
|
968
862
|
if (!cors) {
|
|
969
|
-
this.
|
|
863
|
+
this.throwDependencyError("cors");
|
|
970
864
|
}
|
|
971
865
|
this.app.use(cors(this.config.cors === true ? {} : this.config.cors));
|
|
972
866
|
}
|
|
@@ -1018,7 +912,7 @@ var ExpressServer = class {
|
|
|
1018
912
|
if (this.config.rateLimit?.enable) {
|
|
1019
913
|
const rateLimit = optionalRequire("express-rate-limit");
|
|
1020
914
|
if (!rateLimit) {
|
|
1021
|
-
this.
|
|
915
|
+
this.throwDependencyError("express-rate-limit");
|
|
1022
916
|
}
|
|
1023
917
|
this.app.use(rateLimit({
|
|
1024
918
|
windowMs: this.config.rateLimit.windowMs ?? 15 * 60 * 1e3,
|
|
@@ -1067,7 +961,7 @@ var ExpressServer = class {
|
|
|
1067
961
|
if (this.config.compression) {
|
|
1068
962
|
const compression = optionalRequire("compression");
|
|
1069
963
|
if (!compression) {
|
|
1070
|
-
this.
|
|
964
|
+
this.throwDependencyError("compression");
|
|
1071
965
|
}
|
|
1072
966
|
if (typeof this.config.compression === "object") {
|
|
1073
967
|
this.app.use(compression(this.config.compression));
|
|
@@ -1123,7 +1017,7 @@ var ExpressServer = class {
|
|
|
1123
1017
|
if (this.config.cookieParser) {
|
|
1124
1018
|
const cookieParser = optionalRequire("cookie-parser");
|
|
1125
1019
|
if (!cookieParser) {
|
|
1126
|
-
this.
|
|
1020
|
+
this.throwDependencyError("cookie-parser");
|
|
1127
1021
|
}
|
|
1128
1022
|
if (typeof this.config.cookieParser === "object") {
|
|
1129
1023
|
this.app.use(cookieParser(void 0, this.config.cookieParser));
|
|
@@ -1157,7 +1051,7 @@ var ExpressServer = class {
|
|
|
1157
1051
|
}
|
|
1158
1052
|
const apiReference = optionalRequire("@scalar/express-api-reference")?.apiReference;
|
|
1159
1053
|
if (!apiReference) {
|
|
1160
|
-
this.
|
|
1054
|
+
this.throwDependencyError("@scalar/express-api-reference", getDependencyErrorMessage("@scalar/express-api-reference", "OpenAPI docs"));
|
|
1161
1055
|
}
|
|
1162
1056
|
this.app.use(openApiMountPath, apiReference({
|
|
1163
1057
|
spec: {
|
|
@@ -1178,50 +1072,10 @@ var ExpressServer = class {
|
|
|
1178
1072
|
}
|
|
1179
1073
|
}
|
|
1180
1074
|
/**
|
|
1181
|
-
* Set up metrics tracking middleware.
|
|
1182
|
-
*/
|
|
1183
|
-
setupMetricsMiddleware() {
|
|
1184
|
-
if (this.config.metrics?.enable) {
|
|
1185
|
-
this.app.use((req, res, next) => {
|
|
1186
|
-
const start = process.hrtime();
|
|
1187
|
-
this.clientIPs?.inc({
|
|
1188
|
-
ip: req.ip,
|
|
1189
|
-
method: req.method
|
|
1190
|
-
});
|
|
1191
|
-
const cl = req.headers["content-length"];
|
|
1192
|
-
if (cl) {
|
|
1193
|
-
const size = Number(cl);
|
|
1194
|
-
if (!Number.isNaN(size) && size >= 0) {
|
|
1195
|
-
const route = this.normalizeRouteForMetrics(req, res);
|
|
1196
|
-
this.requestSizes?.observe({
|
|
1197
|
-
method: req.method,
|
|
1198
|
-
route
|
|
1199
|
-
}, size);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
res.once("finish", () => {
|
|
1203
|
-
const [seconds, nanoseconds] = process.hrtime(start);
|
|
1204
|
-
const finalRoute = this.normalizeRouteForMetrics(req, res);
|
|
1205
|
-
this.requestCounter?.inc({
|
|
1206
|
-
method: req.method,
|
|
1207
|
-
route: finalRoute,
|
|
1208
|
-
status: res.statusCode.toString()
|
|
1209
|
-
});
|
|
1210
|
-
this.routeTimings?.observe({
|
|
1211
|
-
method: req.method,
|
|
1212
|
-
route: finalRoute,
|
|
1213
|
-
status: res.statusCode.toString()
|
|
1214
|
-
}, seconds + nanoseconds / 1e9);
|
|
1215
|
-
});
|
|
1216
|
-
next();
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
/**
|
|
1221
1075
|
* Configure server routes and error handling.
|
|
1222
1076
|
* Sets up in following order:
|
|
1223
1077
|
*
|
|
1224
|
-
* 1. Built-in routes (health
|
|
1078
|
+
* 1. Built-in routes (health)
|
|
1225
1079
|
* 2. Application routes
|
|
1226
1080
|
* 3. 404 handler
|
|
1227
1081
|
* 4. Error handler
|
|
@@ -1231,13 +1085,6 @@ var ExpressServer = class {
|
|
|
1231
1085
|
this.app.get(healthCheckPath, async (_req, res) => {
|
|
1232
1086
|
return this.handleHealthCheckRequest(res);
|
|
1233
1087
|
});
|
|
1234
|
-
if (this.config.metrics?.enable) {
|
|
1235
|
-
const metricsPath = this.normalizePath(this.config.metrics.path ?? "/metrics", this.config.metrics?.withGlobalPrefix);
|
|
1236
|
-
this.app.get(metricsPath, async (_req, res) => {
|
|
1237
|
-
res.set("Content-Type", this.register.contentType);
|
|
1238
|
-
res.end(await this.register.metrics());
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1241
1088
|
const routerToUse = this.externalRouter || this.rootRouter;
|
|
1242
1089
|
this.app.use(this.globalPrefix, routerToUse);
|
|
1243
1090
|
await this.runHook("afterRoutes", this.app);
|
|
@@ -1384,6 +1231,10 @@ var ExpressServer = class {
|
|
|
1384
1231
|
* @throws Error if server fails to start or port is already in use
|
|
1385
1232
|
*/
|
|
1386
1233
|
async start() {
|
|
1234
|
+
if (this.server) {
|
|
1235
|
+
getLogger().warn("Server is already running, returning existing instance");
|
|
1236
|
+
return this.server;
|
|
1237
|
+
}
|
|
1387
1238
|
await this.initPromise;
|
|
1388
1239
|
await this.runHook("beforeStart", this.app);
|
|
1389
1240
|
return new Promise((resolve, reject) => {
|
|
@@ -1456,12 +1307,9 @@ var ExpressServer = class {
|
|
|
1456
1307
|
const host = this.formatHostForUrl(this.config.host || "localhost");
|
|
1457
1308
|
const url = `${protocol}://${host}:${port}`;
|
|
1458
1309
|
getLogger().info(`Server running on ${url}`);
|
|
1459
|
-
if (this.config.healthCheck?.path) {
|
|
1310
|
+
if (this.config.healthCheck && this.config.healthCheck?.path) {
|
|
1460
1311
|
getLogger().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`);
|
|
1461
1312
|
}
|
|
1462
|
-
if (this.config.metrics?.enable && this.config.metrics.path) {
|
|
1463
|
-
getLogger().info(`Metrics available at ${url}${this.normalizePath(this.config.metrics.path, this.config.metrics.withGlobalPrefix)}`);
|
|
1464
|
-
}
|
|
1465
1313
|
if (this.config.openApi?.enable) {
|
|
1466
1314
|
getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
|
|
1467
1315
|
}
|
|
@@ -1488,50 +1336,70 @@ var ExpressServer = class {
|
|
|
1488
1336
|
return;
|
|
1489
1337
|
}
|
|
1490
1338
|
if (this.isShuttingDown) {
|
|
1491
|
-
getLogger().warn("
|
|
1339
|
+
getLogger().warn("Shutdown already in progress");
|
|
1492
1340
|
return;
|
|
1493
1341
|
}
|
|
1494
1342
|
this.isShuttingDown = true;
|
|
1495
1343
|
await this.runHook("beforeStop", this.server);
|
|
1496
1344
|
try {
|
|
1497
|
-
await this.gracefulShutdown();
|
|
1498
|
-
} catch (err) {
|
|
1499
|
-
getLogger().error({
|
|
1500
|
-
err
|
|
1501
|
-
}, "Graceful shutdown timed out");
|
|
1502
|
-
if (force) {
|
|
1503
|
-
getLogger().warn("Forcing connection destroy due to shutdown timeout");
|
|
1504
|
-
}
|
|
1345
|
+
await this.gracefulShutdown(force);
|
|
1505
1346
|
} finally {
|
|
1506
|
-
|
|
1347
|
+
this.isShuttingDown = false;
|
|
1507
1348
|
}
|
|
1508
1349
|
}
|
|
1509
1350
|
/**
|
|
1510
1351
|
* Perform graceful server shutdown with timeout.
|
|
1511
1352
|
*/
|
|
1512
|
-
async gracefulShutdown() {
|
|
1353
|
+
async gracefulShutdown(force = false) {
|
|
1513
1354
|
const shutdownTimeout = 1e4;
|
|
1355
|
+
const server = this.server;
|
|
1356
|
+
let timer;
|
|
1357
|
+
let afterStopCalled = false;
|
|
1358
|
+
const runAfterStop = /* @__PURE__ */ __name(async () => {
|
|
1359
|
+
if (afterStopCalled) return;
|
|
1360
|
+
afterStopCalled = true;
|
|
1361
|
+
await this.runHook("afterStop");
|
|
1362
|
+
}, "runAfterStop");
|
|
1514
1363
|
const serverClosePromise = new Promise((resolve, reject) => {
|
|
1515
|
-
|
|
1364
|
+
server.close(async (err) => {
|
|
1365
|
+
if (timer) clearTimeout(timer);
|
|
1516
1366
|
if (err) {
|
|
1517
1367
|
getLogger().error({
|
|
1518
1368
|
err
|
|
1519
|
-
}, "Error
|
|
1369
|
+
}, "Error during server shutdown");
|
|
1520
1370
|
reject(err);
|
|
1521
1371
|
return;
|
|
1522
1372
|
}
|
|
1523
1373
|
this.server = null;
|
|
1524
|
-
this.isShuttingDown = false;
|
|
1525
1374
|
getLogger().info("Server stopped gracefully");
|
|
1526
|
-
await
|
|
1375
|
+
await runAfterStop();
|
|
1527
1376
|
resolve();
|
|
1528
1377
|
});
|
|
1529
1378
|
});
|
|
1530
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1379
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1380
|
+
timer = setTimeout(() => reject(new Error("Shutdown timeout")), shutdownTimeout);
|
|
1381
|
+
});
|
|
1382
|
+
try {
|
|
1383
|
+
await Promise.race([
|
|
1384
|
+
serverClosePromise,
|
|
1385
|
+
timeoutPromise
|
|
1386
|
+
]);
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
if (timer) clearTimeout(timer);
|
|
1389
|
+
getLogger().warn({
|
|
1390
|
+
err
|
|
1391
|
+
}, "Graceful shutdown timed out");
|
|
1392
|
+
if (!force) {
|
|
1393
|
+
throw err;
|
|
1394
|
+
}
|
|
1395
|
+
getLogger().warn("Escalating to forced shutdown");
|
|
1396
|
+
server.closeIdleConnections?.();
|
|
1397
|
+
server.closeAllConnections?.();
|
|
1398
|
+
await this.destroyConnections();
|
|
1399
|
+
this.server = null;
|
|
1400
|
+
getLogger().info("Forced shutdown completed");
|
|
1401
|
+
await runAfterStop();
|
|
1402
|
+
}
|
|
1535
1403
|
}
|
|
1536
1404
|
/**
|
|
1537
1405
|
* Enable graceful shutdown on OS signals for production deployment.
|
|
@@ -1548,31 +1416,64 @@ var ExpressServer = class {
|
|
|
1548
1416
|
"SIGINT",
|
|
1549
1417
|
"SIGTERM"
|
|
1550
1418
|
]) {
|
|
1419
|
+
if (this.gracefulShutdownRegistered) {
|
|
1420
|
+
getLogger().warn("Graceful shutdown handlers are already registered");
|
|
1421
|
+
return this;
|
|
1422
|
+
}
|
|
1423
|
+
let signalHandled = false;
|
|
1551
1424
|
signals.forEach((signal) => {
|
|
1552
1425
|
process.on(signal, async () => {
|
|
1426
|
+
if (signalHandled) {
|
|
1427
|
+
getLogger().warn(`Ignoring duplicate ${signal}`);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
signalHandled = true;
|
|
1553
1431
|
getLogger().info(`Received ${signal}, initiating graceful shutdown...`);
|
|
1554
1432
|
try {
|
|
1555
|
-
await this.stop();
|
|
1433
|
+
await this.stop(true);
|
|
1556
1434
|
process.exit(0);
|
|
1557
1435
|
} catch (err) {
|
|
1558
|
-
getLogger().
|
|
1436
|
+
getLogger().fatal({
|
|
1559
1437
|
err
|
|
1560
|
-
}, "
|
|
1561
|
-
|
|
1562
|
-
await this.stop(true);
|
|
1563
|
-
process.exit(1);
|
|
1564
|
-
} catch (forceError) {
|
|
1565
|
-
getLogger().fatal({
|
|
1566
|
-
forceError
|
|
1567
|
-
}, "Forced shutdown failed, exiting hard");
|
|
1568
|
-
process.exit(1);
|
|
1569
|
-
}
|
|
1438
|
+
}, "Shutdown failed");
|
|
1439
|
+
process.exit(1);
|
|
1570
1440
|
}
|
|
1571
1441
|
});
|
|
1572
1442
|
});
|
|
1443
|
+
this.gracefulShutdownRegistered = true;
|
|
1573
1444
|
return this;
|
|
1574
1445
|
}
|
|
1575
1446
|
/**
|
|
1447
|
+
* Destroy all active connections (gracefully if possible).
|
|
1448
|
+
* If a connection does not close cleanly, it will be force-destroyed.
|
|
1449
|
+
*/
|
|
1450
|
+
async destroyConnections() {
|
|
1451
|
+
if (!this.connections.size) {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const sockets = [
|
|
1455
|
+
...this.connections
|
|
1456
|
+
];
|
|
1457
|
+
await Promise.allSettled(sockets.map((socket) => new Promise((resolve) => {
|
|
1458
|
+
socket.end();
|
|
1459
|
+
const timer = setTimeout(() => {
|
|
1460
|
+
socket.destroy();
|
|
1461
|
+
resolve();
|
|
1462
|
+
}, 1e3);
|
|
1463
|
+
socket.once("close", () => {
|
|
1464
|
+
clearTimeout(timer);
|
|
1465
|
+
resolve();
|
|
1466
|
+
});
|
|
1467
|
+
socket.once("error", () => {
|
|
1468
|
+
clearTimeout(timer);
|
|
1469
|
+
socket.destroy();
|
|
1470
|
+
resolve();
|
|
1471
|
+
});
|
|
1472
|
+
})));
|
|
1473
|
+
this.connections.clear();
|
|
1474
|
+
getLogger().info(`Closed ${sockets.length} active connection(s)`);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1576
1477
|
* Set an externally created base router.
|
|
1577
1478
|
* This will override the internal rootRouter.
|
|
1578
1479
|
*/
|
|
@@ -1600,14 +1501,15 @@ var ExpressServer = class {
|
|
|
1600
1501
|
*/
|
|
1601
1502
|
registerRoute(methods, path, ...handlers) {
|
|
1602
1503
|
const fullPath = this.normalizePath(path, true);
|
|
1504
|
+
const routerToUse = this.externalRouter || this.rootRouter;
|
|
1603
1505
|
const methodMap = {
|
|
1604
|
-
get:
|
|
1605
|
-
post:
|
|
1606
|
-
put:
|
|
1607
|
-
delete:
|
|
1608
|
-
patch:
|
|
1609
|
-
options:
|
|
1610
|
-
head:
|
|
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)
|
|
1611
1513
|
};
|
|
1612
1514
|
methods.forEach((m) => {
|
|
1613
1515
|
const fn = methodMap[m];
|
|
@@ -1633,15 +1535,16 @@ var ExpressServer = class {
|
|
|
1633
1535
|
* @returns This instance for method chaining
|
|
1634
1536
|
*/
|
|
1635
1537
|
registerMiddleware(path, middleware) {
|
|
1538
|
+
const routerToUse = this.externalRouter || this.rootRouter;
|
|
1636
1539
|
if (typeof path === "string") {
|
|
1637
1540
|
const normalizedPath = this.normalizePath(path);
|
|
1638
1541
|
if (normalizedPath) {
|
|
1639
|
-
|
|
1542
|
+
routerToUse.use(normalizedPath, middleware);
|
|
1640
1543
|
} else {
|
|
1641
|
-
|
|
1544
|
+
routerToUse.use(middleware);
|
|
1642
1545
|
}
|
|
1643
1546
|
} else {
|
|
1644
|
-
|
|
1547
|
+
routerToUse.use(path);
|
|
1645
1548
|
}
|
|
1646
1549
|
return this;
|
|
1647
1550
|
}
|
|
@@ -1655,22 +1558,11 @@ var ExpressServer = class {
|
|
|
1655
1558
|
*/
|
|
1656
1559
|
useMiddleware(...middlewares) {
|
|
1657
1560
|
middlewares.forEach((middleware) => {
|
|
1658
|
-
this.
|
|
1561
|
+
(this.externalRouter || this.rootRouter).use(middleware);
|
|
1659
1562
|
});
|
|
1660
1563
|
return this;
|
|
1661
1564
|
}
|
|
1662
1565
|
/**
|
|
1663
|
-
* Get Prometheus registry (to add custom counters/histograms)
|
|
1664
|
-
*
|
|
1665
|
-
* @return {*} {client.Registry}
|
|
1666
|
-
*/
|
|
1667
|
-
getMetricsRegistry() {
|
|
1668
|
-
if (!this.config.metrics?.enable) {
|
|
1669
|
-
getLogger().warn("Metrics are not enabled in the server configuration \nPlease enable metrics to use this feature.");
|
|
1670
|
-
}
|
|
1671
|
-
return this.register;
|
|
1672
|
-
}
|
|
1673
|
-
/**
|
|
1674
1566
|
* Get server configuration
|
|
1675
1567
|
*
|
|
1676
1568
|
* @return {*} {CatbeeServerConfig}
|
|
@@ -1819,41 +1711,6 @@ var ExpressServer = class {
|
|
|
1819
1711
|
}
|
|
1820
1712
|
return sanitize(prefix + "/" + path);
|
|
1821
1713
|
}
|
|
1822
|
-
normalizeRouteForMetrics(req, res) {
|
|
1823
|
-
if (req?.route?.path) {
|
|
1824
|
-
return req.route.path;
|
|
1825
|
-
}
|
|
1826
|
-
if (res.statusCode === 404) return "/404";
|
|
1827
|
-
const path = (req.path || "unknown").split("?")[0];
|
|
1828
|
-
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");
|
|
1829
|
-
}
|
|
1830
|
-
/**
|
|
1831
|
-
* Destroy all active connections (gracefully if possible).
|
|
1832
|
-
* If a connection does not close cleanly, it will be force-destroyed.
|
|
1833
|
-
*/
|
|
1834
|
-
async destroyConnections() {
|
|
1835
|
-
const total = this.connections.size;
|
|
1836
|
-
if (total === 0) {
|
|
1837
|
-
getLogger().debug("No active connections to close");
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
|
-
const timeoutMs = 5e3;
|
|
1841
|
-
await Promise.race([
|
|
1842
|
-
Promise.all(Array.from(this.connections).map((conn) => new Promise((resolve) => {
|
|
1843
|
-
conn.end(() => {
|
|
1844
|
-
if (!conn.destroyed) conn.destroy();
|
|
1845
|
-
resolve();
|
|
1846
|
-
});
|
|
1847
|
-
conn.on("error", () => {
|
|
1848
|
-
conn.destroy();
|
|
1849
|
-
resolve();
|
|
1850
|
-
});
|
|
1851
|
-
}))),
|
|
1852
|
-
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
1853
|
-
]);
|
|
1854
|
-
this.connections.clear();
|
|
1855
|
-
getLogger().info(`Closed ${total} active connections`);
|
|
1856
|
-
}
|
|
1857
1714
|
async validateHttpsFiles() {
|
|
1858
1715
|
if (!await fileExists(this.config.https.key)) {
|
|
1859
1716
|
const msg = `HTTPS key file not found: ${this.config.https.key}`;
|
|
@@ -1877,7 +1734,7 @@ var ExpressServer = class {
|
|
|
1877
1734
|
}
|
|
1878
1735
|
return false;
|
|
1879
1736
|
}
|
|
1880
|
-
|
|
1737
|
+
throwDependencyError(packageName, msg) {
|
|
1881
1738
|
getLogger().error({
|
|
1882
1739
|
command: `npm install ${packageName}`
|
|
1883
1740
|
}, msg || DependencyErrors[packageName]);
|
package/types/index.d.ts
CHANGED
|
@@ -484,28 +484,6 @@ interface CatbeeServerConfig {
|
|
|
484
484
|
*/
|
|
485
485
|
withGlobalPrefix?: boolean;
|
|
486
486
|
};
|
|
487
|
-
/** Prometheus metrics config
|
|
488
|
-
* - **enable**: `false` - **env**: `SERVER_METRICS_ENABLE`
|
|
489
|
-
* - **path**: `'/metrics'` - **env**: `SERVER_METRICS_PATH`
|
|
490
|
-
* - **withGlobalPrefix**: `false` - **env**: `SERVER_METRICS_WITH_GLOBAL_PREFIX`
|
|
491
|
-
*/
|
|
492
|
-
metrics?: {
|
|
493
|
-
/** Enable metrics endpoint
|
|
494
|
-
* - **default**: `false`
|
|
495
|
-
* - **env**: `SERVER_METRICS_ENABLE`
|
|
496
|
-
*/
|
|
497
|
-
enable: boolean;
|
|
498
|
-
/** Metrics endpoint path
|
|
499
|
-
* - **default**: `'/metrics'`
|
|
500
|
-
* - **env**: `SERVER_METRICS_PATH`
|
|
501
|
-
*/
|
|
502
|
-
path?: string;
|
|
503
|
-
/** Apply global prefix
|
|
504
|
-
* - **default**: `false`
|
|
505
|
-
* - **env**: `SERVER_METRICS_WITH_GLOBAL_PREFIX`
|
|
506
|
-
*/
|
|
507
|
-
withGlobalPrefix?: boolean;
|
|
508
|
-
};
|
|
509
487
|
/** Service version header config
|
|
510
488
|
* - **enable**: `false` - **env**: `SERVER_SERVICE_VERSION_ENABLE`
|
|
511
489
|
* - **headerName**: `'x-service-version'` - **env**: `SERVER_SERVICE_VERSION_HEADER_NAME`
|