@catbee/utils 2.0.0-next.1 → 2.0.1

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.
Files changed (87) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +55 -38
  3. package/array/index.cjs +37 -5
  4. package/array/index.d.ts +59 -8
  5. package/array/index.mjs +32 -4
  6. package/async/index.cjs +24 -3
  7. package/async/index.d.ts +18 -2
  8. package/async/index.mjs +24 -4
  9. package/cache/index.cjs +1 -1
  10. package/cache/index.d.ts +1 -1
  11. package/cache/index.mjs +1 -1
  12. package/config/index.cjs +8 -8
  13. package/config/index.d.ts +1 -1
  14. package/config/index.mjs +4 -4
  15. package/context-store/index.cjs +2 -3
  16. package/context-store/index.d.ts +1 -1
  17. package/context-store/index.mjs +2 -3
  18. package/crypto/index.cjs +55 -5
  19. package/crypto/index.d.ts +63 -2
  20. package/crypto/index.mjs +52 -7
  21. package/date/index.cjs +630 -1
  22. package/date/index.d.ts +487 -2
  23. package/date/index.mjs +621 -2
  24. package/{decorators → decorator}/index.cjs +425 -304
  25. package/{decorators → decorator}/index.d.ts +3 -2
  26. package/{decorators → decorator}/index.mjs +425 -304
  27. package/{dir → directory}/index.cjs +1 -1
  28. package/{dir → directory}/index.d.ts +1 -1
  29. package/{dir → directory}/index.mjs +1 -1
  30. package/env/index.cjs +90 -42
  31. package/env/index.d.ts +12 -1
  32. package/env/index.mjs +90 -42
  33. package/exception/index.cjs +1 -1
  34. package/exception/index.d.ts +1 -1
  35. package/exception/index.mjs +1 -1
  36. package/fs/index.cjs +1 -1
  37. package/fs/index.d.ts +1 -1
  38. package/fs/index.mjs +1 -1
  39. package/http-status-codes/index.cjs +1 -1
  40. package/http-status-codes/index.d.ts +1 -1
  41. package/http-status-codes/index.mjs +1 -1
  42. package/id/index.cjs +1 -1
  43. package/id/index.d.ts +1 -1
  44. package/id/index.mjs +1 -1
  45. package/index.cjs +10 -10
  46. package/index.d.ts +4 -4
  47. package/index.mjs +4 -4
  48. package/logger/index.cjs +2 -4
  49. package/logger/index.d.ts +2 -2
  50. package/logger/index.mjs +2 -4
  51. package/middleware/index.cjs +1 -1
  52. package/middleware/index.d.ts +1 -1
  53. package/middleware/index.mjs +1 -1
  54. package/{obj → object}/index.cjs +182 -108
  55. package/{obj → object}/index.d.ts +38 -2
  56. package/{obj → object}/index.mjs +180 -109
  57. package/package.json +31 -13
  58. package/performance/index.cjs +2 -2
  59. package/performance/index.d.ts +1 -1
  60. package/performance/index.mjs +2 -2
  61. package/request/index.cjs +36 -24
  62. package/request/index.d.ts +1 -1
  63. package/request/index.mjs +36 -24
  64. package/response/index.cjs +1 -1
  65. package/response/index.d.ts +1 -1
  66. package/response/index.mjs +1 -1
  67. package/server/index.cjs +233 -131
  68. package/server/index.d.ts +81 -1
  69. package/server/index.mjs +229 -127
  70. package/stream/index.cjs +1 -1
  71. package/stream/index.d.ts +1 -1
  72. package/stream/index.mjs +1 -1
  73. package/string/index.cjs +34 -1
  74. package/string/index.d.ts +45 -2
  75. package/string/index.mjs +31 -2
  76. package/type/index.cjs +18 -1
  77. package/type/index.d.ts +38 -2
  78. package/type/index.mjs +16 -2
  79. package/types/index.cjs +1 -1
  80. package/types/index.d.ts +10 -3
  81. package/types/index.mjs +1 -1
  82. package/url/index.cjs +61 -1
  83. package/url/index.d.ts +59 -2
  84. package/url/index.mjs +57 -2
  85. package/validation/index.cjs +3 -3
  86. package/validation/index.d.ts +1 -1
  87. package/validation/index.mjs +3 -3
package/server/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * The MIT License
3
3
  *
4
- * Copyright (c) 2025 Catbee Technologies. https://catbee.in/license
4
+ * Copyright (c) 2026 Catbee Technologies. https://catbee.in/license
5
5
  *
6
6
  * Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  * of this software and associated documentation files (the "Software"), to deal
@@ -25,13 +25,13 @@
25
25
  import express from 'express';
26
26
  import https from 'https';
27
27
  import { HttpStatusCodes } from '@catbee/utils/http-status-codes';
28
- import { SuccessResponse, createFinalErrorResponse } from '@catbee/utils/response';
28
+ import { createFinalErrorResponse, SuccessResponse } from '@catbee/utils/response';
29
29
  import { requestId, setupRequestContext, timeout, responseTime, errorHandler } from '@catbee/utils/middleware';
30
30
  import { Env } from '@catbee/utils/env';
31
31
  import { getLogger } from '@catbee/utils/logger';
32
32
  import { ServiceUnavailableException, InternalServerErrorException, NotFoundException } from '@catbee/utils/exception';
33
33
  import { getCatbeeServerGlobalConfig } from '@catbee/utils/config';
34
- import { deepObjMerge, deepClone } from '@catbee/utils/obj';
34
+ import { deepObjMerge, deepClone } from '@catbee/utils/object';
35
35
  import { fileExists, readFile, readFileSync } from '@catbee/utils/fs';
36
36
  import { isPort } from '@catbee/utils/validation';
37
37
  import { optionalRequire } from '@catbee/utils/async';
@@ -853,6 +853,7 @@ var ExpressServer = class {
853
853
  async initialize() {
854
854
  await this.runHook("beforeInit", this);
855
855
  await this.setupMiddleware();
856
+ await this.runHook("beforeRoutes", this.app);
856
857
  await this.setupRoutes();
857
858
  await this.runHook("afterInit", this);
858
859
  }
@@ -879,6 +880,24 @@ var ExpressServer = class {
879
880
  if (this.config.https) {
880
881
  await this.validateHttpsFiles();
881
882
  }
883
+ this.setupBasicMiddleware();
884
+ this.setupSecurityMiddleware();
885
+ this.setupGlobalHeaders();
886
+ this.setupTimeoutMiddleware();
887
+ this.setupResponseTimeMiddleware();
888
+ this.setupRateLimitingMiddleware();
889
+ this.setupRequestLoggingMiddleware();
890
+ this.setupCompressionMiddleware();
891
+ this.setupStaticFilesMiddleware();
892
+ this.setupBodyParsingMiddleware();
893
+ this.setupCookieParsingMiddleware();
894
+ await this.setupOpenApiMiddleware();
895
+ this.setupMetricsMiddleware();
896
+ }
897
+ /**
898
+ * Set up basic middleware (trust proxy, request ID, context).
899
+ */
900
+ setupBasicMiddleware() {
882
901
  this.app.disable("x-powered-by");
883
902
  if (this.config.trustProxy) {
884
903
  this.app.set("trust proxy", true);
@@ -895,11 +914,15 @@ var ExpressServer = class {
895
914
  this.app.use((_req, res, next) => {
896
915
  if (this.isShuttingDown) {
897
916
  res.setHeader("Connection", "close");
898
- return res.status(HttpStatusCodes.SERVICE_UNAVAILABLE).json(new ServiceUnavailableException("Server is shutting down"));
917
+ res.status(HttpStatusCodes.SERVICE_UNAVAILABLE).json(new ServiceUnavailableException("Server is shutting down"));
899
918
  }
900
919
  next();
901
- return;
902
920
  });
921
+ }
922
+ /**
923
+ * Set up security middleware (Helmet, CORS).
924
+ */
925
+ setupSecurityMiddleware() {
903
926
  if (this.config.helmet) {
904
927
  const helmet = optionalRequire("helmet");
905
928
  if (!helmet) {
@@ -918,6 +941,11 @@ var ExpressServer = class {
918
941
  }
919
942
  this.app.use(cors(this.config.cors === true ? {} : this.config.cors));
920
943
  }
944
+ }
945
+ /**
946
+ * Set up global headers middleware.
947
+ */
948
+ setupGlobalHeaders() {
921
949
  this.app.use((_req, res, next) => {
922
950
  if (this.config.globalHeaders) {
923
951
  for (const key in this.config.globalHeaders) {
@@ -934,15 +962,30 @@ var ExpressServer = class {
934
962
  }
935
963
  next();
936
964
  });
965
+ }
966
+ /**
967
+ * Set up request timeout middleware.
968
+ */
969
+ setupTimeoutMiddleware() {
937
970
  if (this.config.requestTimeout) {
938
971
  this.app.use(timeout(this.config.requestTimeout));
939
972
  }
973
+ }
974
+ /**
975
+ * Set up response time tracking middleware.
976
+ */
977
+ setupResponseTimeMiddleware() {
940
978
  if (this.config.responseTime?.enable) {
941
979
  this.app.use(responseTime({
942
980
  addHeader: this.config.responseTime.addHeader,
943
981
  logOnComplete: this.config.responseTime.logOnComplete
944
982
  }));
945
983
  }
984
+ }
985
+ /**
986
+ * Set up rate limiting middleware.
987
+ */
988
+ setupRateLimitingMiddleware() {
946
989
  if (this.config.rateLimit?.enable) {
947
990
  const rateLimit = optionalRequire("express-rate-limit");
948
991
  if (!rateLimit) {
@@ -960,6 +1003,11 @@ var ExpressServer = class {
960
1003
  legacyHeaders: this.config.rateLimit.legacyHeaders ?? false
961
1004
  }));
962
1005
  }
1006
+ }
1007
+ /**
1008
+ * Set up request logging middleware.
1009
+ */
1010
+ setupRequestLoggingMiddleware() {
963
1011
  if (this.config.requestLogging?.enable) {
964
1012
  this.app.use((req, res, next) => {
965
1013
  if (typeof this.config.requestLogging?.ignorePaths === "function") {
@@ -983,6 +1031,11 @@ var ExpressServer = class {
983
1031
  if (this.hooks.onRequest) {
984
1032
  this.app.use(this.hooks.onRequest);
985
1033
  }
1034
+ }
1035
+ /**
1036
+ * Set up response compression middleware.
1037
+ */
1038
+ setupCompressionMiddleware() {
986
1039
  if (this.config.compression) {
987
1040
  const compression = optionalRequire("compression");
988
1041
  if (!compression) {
@@ -994,6 +1047,11 @@ var ExpressServer = class {
994
1047
  this.app.use(compression());
995
1048
  }
996
1049
  }
1050
+ }
1051
+ /**
1052
+ * Set up static file serving middleware.
1053
+ */
1054
+ setupStaticFilesMiddleware() {
997
1055
  if (this.config.staticFolders) {
998
1056
  this.config.staticFolders.forEach((folder) => {
999
1057
  this.app.use(this.normalizePath(folder.path ?? "/"), express.static(folder.directory, {
@@ -1006,6 +1064,11 @@ var ExpressServer = class {
1006
1064
  getLogger().info(`Serving static folder: ${folder.directory} at path ${folder.path || "/"}`);
1007
1065
  });
1008
1066
  }
1067
+ }
1068
+ /**
1069
+ * Set up body parsing middleware.
1070
+ */
1071
+ setupBodyParsingMiddleware() {
1009
1072
  if (this.config.bodyParser) {
1010
1073
  if (this.config.bodyParser.json) {
1011
1074
  this.app.use(express.json(this.config.bodyParser.json));
@@ -1014,6 +1077,11 @@ var ExpressServer = class {
1014
1077
  this.app.use(express.urlencoded(this.config.bodyParser.urlencoded));
1015
1078
  }
1016
1079
  }
1080
+ }
1081
+ /**
1082
+ * Set up cookie parsing middleware.
1083
+ */
1084
+ setupCookieParsingMiddleware() {
1017
1085
  if (this.config.cookieParser) {
1018
1086
  const cookieParser = optionalRequire("cookie-parser");
1019
1087
  if (!cookieParser) {
@@ -1025,6 +1093,11 @@ var ExpressServer = class {
1025
1093
  this.app.use(cookieParser());
1026
1094
  }
1027
1095
  }
1096
+ }
1097
+ /**
1098
+ * Set up OpenAPI documentation middleware.
1099
+ */
1100
+ async setupOpenApiMiddleware() {
1028
1101
  if (this.config.openApi?.enable) {
1029
1102
  try {
1030
1103
  const openApiMountPath = this.normalizePath(this.config.openApi.mountPath ?? "/docs", this.config.openApi.withGlobalPrefix);
@@ -1065,6 +1138,11 @@ var ExpressServer = class {
1065
1138
  if (this.hooks.onResponse) {
1066
1139
  this.app.use(this.globalPrefix, this.hooks.onResponse);
1067
1140
  }
1141
+ }
1142
+ /**
1143
+ * Set up metrics tracking middleware.
1144
+ */
1145
+ setupMetricsMiddleware() {
1068
1146
  if (this.config.metrics?.enable) {
1069
1147
  this.app.use((req, res, next) => {
1070
1148
  const start = process.hrtime();
@@ -1113,45 +1191,7 @@ var ExpressServer = class {
1113
1191
  async setupRoutes() {
1114
1192
  const healthCheckPath = this.normalizePath(this.config.healthCheck?.path || "/healthz", this.config.healthCheck?.withGlobalPrefix);
1115
1193
  this.app.get(healthCheckPath, async (_req, res) => {
1116
- try {
1117
- if (!this.healthChecks.length || getCatbeeServerGlobalConfig().skipHealthz) {
1118
- return res.status(HttpStatusCodes.OK).json(new SuccessResponse("OK"));
1119
- }
1120
- const checkResults = await Promise.allSettled(this.healthChecks.map(async ({ name, check }) => {
1121
- try {
1122
- const status2 = await Promise.resolve(check());
1123
- return {
1124
- name,
1125
- status: status2,
1126
- error: null
1127
- };
1128
- } catch (error) {
1129
- return {
1130
- name,
1131
- status: false,
1132
- error: error.message
1133
- };
1134
- }
1135
- }));
1136
- const results = checkResults.map((result) => {
1137
- if (result.status === "fulfilled") return result.value;
1138
- return {
1139
- name: "unknown",
1140
- status: false,
1141
- error: result.reason
1142
- };
1143
- });
1144
- const allOk = results.every((r) => r.status);
1145
- const status = allOk ? HttpStatusCodes.OK : HttpStatusCodes.SERVICE_UNAVAILABLE;
1146
- const response = new SuccessResponse(allOk ? "OK" : "Service unavailable");
1147
- if (!allOk) response.error = true;
1148
- if (this.config.healthCheck?.detailed) response.data = {
1149
- checks: results
1150
- };
1151
- return res.status(status).json(response);
1152
- } catch {
1153
- return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json(new InternalServerErrorException("Health check failed"));
1154
- }
1194
+ return this.handleHealthCheckRequest(res);
1155
1195
  });
1156
1196
  if (this.config.metrics?.enable) {
1157
1197
  const metricsPath = this.normalizePath(this.config.metrics.path ?? "/metrics", this.config.metrics?.withGlobalPrefix);
@@ -1162,6 +1202,7 @@ var ExpressServer = class {
1162
1202
  }
1163
1203
  const routerToUse = this.externalRouter || this.rootRouter;
1164
1204
  this.app.use(this.globalPrefix, routerToUse);
1205
+ await this.runHook("afterRoutes", this.app);
1165
1206
  this.app.use((req, res) => {
1166
1207
  const status = HttpStatusCodes.NOT_FOUND;
1167
1208
  const response = createFinalErrorResponse(req, status, `Route ${req.method.toUpperCase()} ${req.path} not found`);
@@ -1183,6 +1224,56 @@ var ExpressServer = class {
1183
1224
  });
1184
1225
  }
1185
1226
  /**
1227
+ * Execute health check and return response.
1228
+ */
1229
+ async handleHealthCheckRequest(res) {
1230
+ try {
1231
+ if (!this.healthChecks.length || getCatbeeServerGlobalConfig().skipHealthzChecksValidation) {
1232
+ return res.status(HttpStatusCodes.OK).json(new SuccessResponse("OK"));
1233
+ }
1234
+ const results = await this.executeHealthChecks();
1235
+ const allOk = results.every((r) => r.status);
1236
+ const status = allOk ? HttpStatusCodes.OK : HttpStatusCodes.SERVICE_UNAVAILABLE;
1237
+ const response = new SuccessResponse(allOk ? "OK" : "Service unavailable");
1238
+ if (!allOk) response.error = true;
1239
+ if (this.config.healthCheck?.detailed) response.data = {
1240
+ checks: results
1241
+ };
1242
+ return res.status(status).json(response);
1243
+ } catch {
1244
+ return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json(new InternalServerErrorException("Health check failed"));
1245
+ }
1246
+ }
1247
+ /**
1248
+ * Execute all registered health checks and return results.
1249
+ */
1250
+ async executeHealthChecks() {
1251
+ const checkResults = await Promise.allSettled(this.healthChecks.map(async ({ name, check }) => {
1252
+ try {
1253
+ const status = await Promise.resolve(check());
1254
+ return {
1255
+ name,
1256
+ status,
1257
+ error: null
1258
+ };
1259
+ } catch (error) {
1260
+ return {
1261
+ name,
1262
+ status: false,
1263
+ error: error.message
1264
+ };
1265
+ }
1266
+ }));
1267
+ return checkResults.map((result) => {
1268
+ if (result.status === "fulfilled") return result.value;
1269
+ return {
1270
+ name: "unknown",
1271
+ status: false,
1272
+ error: result.reason
1273
+ };
1274
+ });
1275
+ }
1276
+ /**
1186
1277
  * Register a new health check function for monitoring service dependencies.
1187
1278
  *
1188
1279
  * Health checks are executed when the health endpoint is accessed and
@@ -1213,28 +1304,8 @@ var ExpressServer = class {
1213
1304
  */
1214
1305
  async ready() {
1215
1306
  try {
1216
- if (!this.healthChecks.length || getCatbeeServerGlobalConfig().skipHealthz) return true;
1217
- const checkResults = await Promise.allSettled(this.healthChecks.map(async ({ name, check }) => {
1218
- try {
1219
- const status = await Promise.resolve(check());
1220
- return {
1221
- name,
1222
- status,
1223
- error: null
1224
- };
1225
- } catch (error) {
1226
- return {
1227
- name,
1228
- status: false,
1229
- error: error.message
1230
- };
1231
- }
1232
- }));
1233
- const results = checkResults.map((result) => result.status === "fulfilled" ? result.value : {
1234
- name: "unknown",
1235
- status: false,
1236
- error: result.reason
1237
- });
1307
+ if (!this.healthChecks.length || getCatbeeServerGlobalConfig().skipHealthzChecksValidation) return true;
1308
+ const results = await this.executeHealthChecks();
1238
1309
  return results.every((r) => r.status === true);
1239
1310
  } catch (err) {
1240
1311
  getLogger().error({
@@ -1279,58 +1350,83 @@ var ExpressServer = class {
1279
1350
  await this.runHook("beforeStart", this.app);
1280
1351
  return new Promise((resolve, reject) => {
1281
1352
  try {
1282
- const listenArgs = [
1283
- this.config.port,
1284
- this.config.host,
1285
- async () => {
1286
- const protocol = this.config.https ? "https" : "http";
1287
- const url = `${protocol}://${this.config.host}:${this.config.port}`;
1288
- getLogger().info(`Server running on ${url}`);
1289
- if (this.config.healthCheck?.path) {
1290
- getLogger().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`);
1291
- }
1292
- if (this.config.metrics?.enable && this.config.metrics.path) {
1293
- getLogger().info(`Metrics available at ${url}${this.normalizePath(this.config.metrics.path, this.config.metrics.withGlobalPrefix)}`);
1294
- }
1295
- if (this.config.openApi?.enable) {
1296
- getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
1297
- }
1298
- if (this.server) await this.runHook("afterStart", this.server);
1299
- resolve(this.server);
1300
- }
1301
- ];
1302
- if (this.config.https) {
1303
- const httpsOptions = {
1304
- ...this.config.https,
1305
- key: readFileSync(this.config.https.key),
1306
- cert: readFileSync(this.config.https.cert)
1307
- };
1308
- if (this.config.https.ca) {
1309
- httpsOptions.ca = readFileSync(this.config.https.ca);
1310
- }
1311
- if (this.config.https.passphrase) {
1312
- httpsOptions.passphrase = this.config.https.passphrase;
1313
- }
1314
- this.server = https.createServer(httpsOptions, this.app).listen(...listenArgs);
1315
- } else {
1316
- this.server = this.app.listen(...listenArgs);
1317
- }
1318
- this.server.on("connection", (conn) => {
1319
- this.connections.add(conn);
1320
- conn.on("close", () => this.connections.delete(conn));
1321
- });
1322
- this.server.on("error", (err) => {
1323
- getLogger().error({
1324
- err
1325
- }, "Server failed to start");
1326
- reject(err);
1327
- });
1353
+ const onListening = /* @__PURE__ */ __name(async () => {
1354
+ this.logServerStartInfo();
1355
+ if (this.server) await this.runHook("afterStart", this.server);
1356
+ resolve(this.server);
1357
+ }, "onListening");
1358
+ this.server = this.createServerInstance(onListening);
1359
+ this.runHook("onServerCreated", this.server);
1360
+ this.setupConnectionTracking();
1361
+ this.setupServerErrorHandling(reject);
1328
1362
  } catch (error) {
1329
1363
  reject(error);
1330
1364
  }
1331
1365
  });
1332
1366
  }
1333
1367
  /**
1368
+ * Create HTTP or HTTPS server instance.
1369
+ */
1370
+ createServerInstance(onListening) {
1371
+ const listenArgs = [
1372
+ this.config.port,
1373
+ this.config.host,
1374
+ onListening
1375
+ ];
1376
+ if (this.config.https) {
1377
+ const httpsOptions = {
1378
+ ...this.config.https,
1379
+ key: readFileSync(this.config.https.key),
1380
+ cert: readFileSync(this.config.https.cert)
1381
+ };
1382
+ if (this.config.https.ca) {
1383
+ httpsOptions.ca = readFileSync(this.config.https.ca);
1384
+ }
1385
+ if (this.config.https.passphrase) {
1386
+ httpsOptions.passphrase = this.config.https.passphrase;
1387
+ }
1388
+ return https.createServer(httpsOptions, this.app).listen(...listenArgs);
1389
+ }
1390
+ return this.app.listen(...listenArgs);
1391
+ }
1392
+ /**
1393
+ * Set up connection tracking for graceful shutdown.
1394
+ */
1395
+ setupConnectionTracking() {
1396
+ this.server.on("connection", (conn) => {
1397
+ this.connections.add(conn);
1398
+ conn.on("close", () => this.connections.delete(conn));
1399
+ });
1400
+ }
1401
+ /**
1402
+ * Set up error handling for server startup.
1403
+ */
1404
+ setupServerErrorHandling(reject) {
1405
+ this.server.on("error", (err) => {
1406
+ getLogger().error({
1407
+ err
1408
+ }, "Server failed to start");
1409
+ reject(err);
1410
+ });
1411
+ }
1412
+ /**
1413
+ * Log server startup information.
1414
+ */
1415
+ logServerStartInfo() {
1416
+ const protocol = this.config.https ? "https" : "http";
1417
+ const url = `${protocol}://${this.config.host}:${this.config.port}`;
1418
+ getLogger().info(`Server running on ${url}`);
1419
+ if (this.config.healthCheck?.path) {
1420
+ getLogger().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`);
1421
+ }
1422
+ if (this.config.metrics?.enable && this.config.metrics.path) {
1423
+ getLogger().info(`Metrics available at ${url}${this.normalizePath(this.config.metrics.path, this.config.metrics.withGlobalPrefix)}`);
1424
+ }
1425
+ if (this.config.openApi?.enable) {
1426
+ getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
1427
+ }
1428
+ }
1429
+ /**
1334
1430
  * Stop the HTTP server gracefully.
1335
1431
  *
1336
1432
  * This method:
@@ -1357,6 +1453,23 @@ var ExpressServer = class {
1357
1453
  }
1358
1454
  this.isShuttingDown = true;
1359
1455
  await this.runHook("beforeStop", this.server);
1456
+ try {
1457
+ await this.gracefulShutdown();
1458
+ } catch (err) {
1459
+ getLogger().error({
1460
+ err
1461
+ }, "Graceful shutdown timed out");
1462
+ if (force) {
1463
+ getLogger().warn("Forcing connection destroy due to shutdown timeout");
1464
+ }
1465
+ } finally {
1466
+ await this.destroyConnections();
1467
+ }
1468
+ }
1469
+ /**
1470
+ * Perform graceful server shutdown with timeout.
1471
+ */
1472
+ async gracefulShutdown() {
1360
1473
  const shutdownTimeout = 1e4;
1361
1474
  const serverClosePromise = new Promise((resolve, reject) => {
1362
1475
  this.server.close(async (err) => {
@@ -1375,21 +1488,10 @@ var ExpressServer = class {
1375
1488
  });
1376
1489
  });
1377
1490
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Shutdown timeout")), shutdownTimeout));
1378
- try {
1379
- await Promise.race([
1380
- serverClosePromise,
1381
- timeoutPromise
1382
- ]);
1383
- } catch (err) {
1384
- getLogger().error({
1385
- err
1386
- }, "Graceful shutdown timed out");
1387
- if (force) {
1388
- getLogger().warn("Forcing connection destroy due to shutdown timeout");
1389
- }
1390
- } finally {
1391
- await this.destroyConnections();
1392
- }
1491
+ await Promise.race([
1492
+ serverClosePromise,
1493
+ timeoutPromise
1494
+ ]);
1393
1495
  }
1394
1496
  /**
1395
1497
  * Enable graceful shutdown on OS signals for production deployment.
package/stream/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * The MIT License
3
3
  *
4
- * Copyright (c) 2025 Catbee Technologies. https://catbee.in/license
4
+ * Copyright (c) 2026 Catbee Technologies. https://catbee.in/license
5
5
  *
6
6
  * Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  * of this software and associated documentation files (the "Software"), to deal
package/stream/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * The MIT License
3
3
  *
4
- * Copyright (c) 2025 Catbee Technologies. https://catbee.in/license
4
+ * Copyright (c) 2026 Catbee Technologies. https://catbee.in/license
5
5
  *
6
6
  * Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  * of this software and associated documentation files (the "Software"), to deal
package/stream/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * The MIT License
3
3
  *
4
- * Copyright (c) 2025 Catbee Technologies. https://catbee.in/license
4
+ * Copyright (c) 2026 Catbee Technologies. https://catbee.in/license
5
5
  *
6
6
  * Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  * of this software and associated documentation files (the "Software"), to deal
package/string/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * The MIT License
3
3
  *
4
- * Copyright (c) 2025 Catbee Technologies. https://catbee.in/license
4
+ * Copyright (c) 2026 Catbee Technologies. https://catbee.in/license
5
5
  *
6
6
  * Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  * of this software and associated documentation files (the "Software"), to deal
@@ -93,10 +93,42 @@ function toTitleCase(str) {
93
93
  return str.split(/(\s+)/).map((part) => part.trim().length === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
94
94
  }
95
95
  __name(toTitleCase, "toTitleCase");
96
+ function escapeRegex(str) {
97
+ if (typeof str !== "string") throw new TypeError("Expected a string");
98
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
99
+ }
100
+ __name(escapeRegex, "escapeRegex");
101
+ function unescapeHtml(str) {
102
+ if (typeof str !== "string") return "";
103
+ const entities = {
104
+ "&": "&",
105
+ "&lt;": "<",
106
+ "&gt;": ">",
107
+ "&quot;": '"',
108
+ "&#39;": "'",
109
+ "&#x27;": "'"
110
+ };
111
+ return str.replace(/&(?:amp|lt|gt|quot|#39|#x27);/g, (match) => entities[match] || match);
112
+ }
113
+ __name(unescapeHtml, "unescapeHtml");
114
+ function isBlank(str) {
115
+ return typeof str === "string" && str.trim().length === 0;
116
+ }
117
+ __name(isBlank, "isBlank");
118
+ function ellipsis(str, maxLength, suffix = "...") {
119
+ if (typeof str !== "string" || str.length <= maxLength) return str;
120
+ const truncated = str.slice(0, maxLength - suffix.length);
121
+ const lastSpace = truncated.lastIndexOf(" ");
122
+ return (lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated) + suffix;
123
+ }
124
+ __name(ellipsis, "ellipsis");
96
125
 
97
126
  exports.capitalize = capitalize;
98
127
  exports.countOccurrences = countOccurrences;
128
+ exports.ellipsis = ellipsis;
99
129
  exports.equalsIgnoreCase = equalsIgnoreCase;
130
+ exports.escapeRegex = escapeRegex;
131
+ exports.isBlank = isBlank;
100
132
  exports.mask = mask;
101
133
  exports.reverse = reverse;
102
134
  exports.slugify = slugify;
@@ -107,3 +139,4 @@ exports.toPascalCase = toPascalCase;
107
139
  exports.toSnakeCase = toSnakeCase;
108
140
  exports.toTitleCase = toTitleCase;
109
141
  exports.truncate = truncate;
142
+ exports.unescapeHtml = unescapeHtml;
package/string/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * The MIT License
3
3
  *
4
- * Copyright (c) 2025 Catbee Technologies. https://catbee.in/license
4
+ * Copyright (c) 2026 Catbee Technologies. https://catbee.in/license
5
5
  *
6
6
  * Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  * of this software and associated documentation files (the "Software"), to deal
@@ -122,5 +122,48 @@ declare function countOccurrences(str: string, substring: string, caseSensitive?
122
122
  * @returns Title-cased string
123
123
  */
124
124
  declare function toTitleCase(str: string): string;
125
+ /**
126
+ * Escapes special regex characters in a string.
127
+ *
128
+ * @param {string} str - The input string.
129
+ * @returns {string} String with regex characters escaped.
130
+ *
131
+ * @example
132
+ * escapeRegex('Hello (world)'); // 'Hello \\(world\\)'
133
+ */
134
+ declare function escapeRegex(str: string): string;
135
+ /**
136
+ * Unescapes HTML entities in a string.
137
+ *
138
+ * @param {string} str - The HTML string.
139
+ * @returns {string} String with HTML entities unescaped.
140
+ *
141
+ * @example
142
+ * unescapeHtml('&lt;div&gt;Hello&lt;/div&gt;'); // '<div>Hello</div>'
143
+ */
144
+ declare function unescapeHtml(str: string): string;
145
+ /**
146
+ * Checks if a string is blank (empty or only whitespace).
147
+ *
148
+ * @param {string} str - The input string.
149
+ * @returns {boolean} True if blank.
150
+ *
151
+ * @example
152
+ * isBlank(' '); // true
153
+ * isBlank('hello'); // false
154
+ */
155
+ declare function isBlank(str: string): boolean;
156
+ /**
157
+ * Truncates a string with an ellipsis, ensuring word boundaries.
158
+ *
159
+ * @param {string} str - The input string.
160
+ * @param {number} maxLength - Maximum length.
161
+ * @param {string} [suffix='...'] - Suffix to append.
162
+ * @returns {string} Truncated string.
163
+ *
164
+ * @example
165
+ * ellipsis('The quick brown fox', 10); // 'The quick...'
166
+ */
167
+ declare function ellipsis(str: string, maxLength: number, suffix?: string): string;
125
168
 
126
- export { capitalize, countOccurrences, equalsIgnoreCase, mask, reverse, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toTitleCase, truncate };
169
+ export { capitalize, countOccurrences, ellipsis, equalsIgnoreCase, escapeRegex, isBlank, mask, reverse, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toTitleCase, truncate, unescapeHtml };