@catbee/utils 2.0.0-next.1 → 2.0.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.
Files changed (87) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +50 -36
  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 +230 -131
  68. package/server/index.d.ts +81 -1
  69. package/server/index.mjs +226 -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 +3 -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';
@@ -879,6 +879,24 @@ var ExpressServer = class {
879
879
  if (this.config.https) {
880
880
  await this.validateHttpsFiles();
881
881
  }
882
+ this.setupBasicMiddleware();
883
+ this.setupSecurityMiddleware();
884
+ this.setupGlobalHeaders();
885
+ this.setupTimeoutMiddleware();
886
+ this.setupResponseTimeMiddleware();
887
+ this.setupRateLimitingMiddleware();
888
+ this.setupRequestLoggingMiddleware();
889
+ this.setupCompressionMiddleware();
890
+ this.setupStaticFilesMiddleware();
891
+ this.setupBodyParsingMiddleware();
892
+ this.setupCookieParsingMiddleware();
893
+ await this.setupOpenApiMiddleware();
894
+ this.setupMetricsMiddleware();
895
+ }
896
+ /**
897
+ * Set up basic middleware (trust proxy, request ID, context).
898
+ */
899
+ setupBasicMiddleware() {
882
900
  this.app.disable("x-powered-by");
883
901
  if (this.config.trustProxy) {
884
902
  this.app.set("trust proxy", true);
@@ -895,11 +913,15 @@ var ExpressServer = class {
895
913
  this.app.use((_req, res, next) => {
896
914
  if (this.isShuttingDown) {
897
915
  res.setHeader("Connection", "close");
898
- return res.status(HttpStatusCodes.SERVICE_UNAVAILABLE).json(new ServiceUnavailableException("Server is shutting down"));
916
+ res.status(HttpStatusCodes.SERVICE_UNAVAILABLE).json(new ServiceUnavailableException("Server is shutting down"));
899
917
  }
900
918
  next();
901
- return;
902
919
  });
920
+ }
921
+ /**
922
+ * Set up security middleware (Helmet, CORS).
923
+ */
924
+ setupSecurityMiddleware() {
903
925
  if (this.config.helmet) {
904
926
  const helmet = optionalRequire("helmet");
905
927
  if (!helmet) {
@@ -918,6 +940,11 @@ var ExpressServer = class {
918
940
  }
919
941
  this.app.use(cors(this.config.cors === true ? {} : this.config.cors));
920
942
  }
943
+ }
944
+ /**
945
+ * Set up global headers middleware.
946
+ */
947
+ setupGlobalHeaders() {
921
948
  this.app.use((_req, res, next) => {
922
949
  if (this.config.globalHeaders) {
923
950
  for (const key in this.config.globalHeaders) {
@@ -934,15 +961,30 @@ var ExpressServer = class {
934
961
  }
935
962
  next();
936
963
  });
964
+ }
965
+ /**
966
+ * Set up request timeout middleware.
967
+ */
968
+ setupTimeoutMiddleware() {
937
969
  if (this.config.requestTimeout) {
938
970
  this.app.use(timeout(this.config.requestTimeout));
939
971
  }
972
+ }
973
+ /**
974
+ * Set up response time tracking middleware.
975
+ */
976
+ setupResponseTimeMiddleware() {
940
977
  if (this.config.responseTime?.enable) {
941
978
  this.app.use(responseTime({
942
979
  addHeader: this.config.responseTime.addHeader,
943
980
  logOnComplete: this.config.responseTime.logOnComplete
944
981
  }));
945
982
  }
983
+ }
984
+ /**
985
+ * Set up rate limiting middleware.
986
+ */
987
+ setupRateLimitingMiddleware() {
946
988
  if (this.config.rateLimit?.enable) {
947
989
  const rateLimit = optionalRequire("express-rate-limit");
948
990
  if (!rateLimit) {
@@ -960,6 +1002,11 @@ var ExpressServer = class {
960
1002
  legacyHeaders: this.config.rateLimit.legacyHeaders ?? false
961
1003
  }));
962
1004
  }
1005
+ }
1006
+ /**
1007
+ * Set up request logging middleware.
1008
+ */
1009
+ setupRequestLoggingMiddleware() {
963
1010
  if (this.config.requestLogging?.enable) {
964
1011
  this.app.use((req, res, next) => {
965
1012
  if (typeof this.config.requestLogging?.ignorePaths === "function") {
@@ -983,6 +1030,11 @@ var ExpressServer = class {
983
1030
  if (this.hooks.onRequest) {
984
1031
  this.app.use(this.hooks.onRequest);
985
1032
  }
1033
+ }
1034
+ /**
1035
+ * Set up response compression middleware.
1036
+ */
1037
+ setupCompressionMiddleware() {
986
1038
  if (this.config.compression) {
987
1039
  const compression = optionalRequire("compression");
988
1040
  if (!compression) {
@@ -994,6 +1046,11 @@ var ExpressServer = class {
994
1046
  this.app.use(compression());
995
1047
  }
996
1048
  }
1049
+ }
1050
+ /**
1051
+ * Set up static file serving middleware.
1052
+ */
1053
+ setupStaticFilesMiddleware() {
997
1054
  if (this.config.staticFolders) {
998
1055
  this.config.staticFolders.forEach((folder) => {
999
1056
  this.app.use(this.normalizePath(folder.path ?? "/"), express.static(folder.directory, {
@@ -1006,6 +1063,11 @@ var ExpressServer = class {
1006
1063
  getLogger().info(`Serving static folder: ${folder.directory} at path ${folder.path || "/"}`);
1007
1064
  });
1008
1065
  }
1066
+ }
1067
+ /**
1068
+ * Set up body parsing middleware.
1069
+ */
1070
+ setupBodyParsingMiddleware() {
1009
1071
  if (this.config.bodyParser) {
1010
1072
  if (this.config.bodyParser.json) {
1011
1073
  this.app.use(express.json(this.config.bodyParser.json));
@@ -1014,6 +1076,11 @@ var ExpressServer = class {
1014
1076
  this.app.use(express.urlencoded(this.config.bodyParser.urlencoded));
1015
1077
  }
1016
1078
  }
1079
+ }
1080
+ /**
1081
+ * Set up cookie parsing middleware.
1082
+ */
1083
+ setupCookieParsingMiddleware() {
1017
1084
  if (this.config.cookieParser) {
1018
1085
  const cookieParser = optionalRequire("cookie-parser");
1019
1086
  if (!cookieParser) {
@@ -1025,6 +1092,11 @@ var ExpressServer = class {
1025
1092
  this.app.use(cookieParser());
1026
1093
  }
1027
1094
  }
1095
+ }
1096
+ /**
1097
+ * Set up OpenAPI documentation middleware.
1098
+ */
1099
+ async setupOpenApiMiddleware() {
1028
1100
  if (this.config.openApi?.enable) {
1029
1101
  try {
1030
1102
  const openApiMountPath = this.normalizePath(this.config.openApi.mountPath ?? "/docs", this.config.openApi.withGlobalPrefix);
@@ -1065,6 +1137,11 @@ var ExpressServer = class {
1065
1137
  if (this.hooks.onResponse) {
1066
1138
  this.app.use(this.globalPrefix, this.hooks.onResponse);
1067
1139
  }
1140
+ }
1141
+ /**
1142
+ * Set up metrics tracking middleware.
1143
+ */
1144
+ setupMetricsMiddleware() {
1068
1145
  if (this.config.metrics?.enable) {
1069
1146
  this.app.use((req, res, next) => {
1070
1147
  const start = process.hrtime();
@@ -1113,45 +1190,7 @@ var ExpressServer = class {
1113
1190
  async setupRoutes() {
1114
1191
  const healthCheckPath = this.normalizePath(this.config.healthCheck?.path || "/healthz", this.config.healthCheck?.withGlobalPrefix);
1115
1192
  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
- }
1193
+ return this.handleHealthCheckRequest(res);
1155
1194
  });
1156
1195
  if (this.config.metrics?.enable) {
1157
1196
  const metricsPath = this.normalizePath(this.config.metrics.path ?? "/metrics", this.config.metrics?.withGlobalPrefix);
@@ -1183,6 +1222,56 @@ var ExpressServer = class {
1183
1222
  });
1184
1223
  }
1185
1224
  /**
1225
+ * Execute health check and return response.
1226
+ */
1227
+ async handleHealthCheckRequest(res) {
1228
+ try {
1229
+ if (!this.healthChecks.length || getCatbeeServerGlobalConfig().skipHealthzChecksValidation) {
1230
+ return res.status(HttpStatusCodes.OK).json(new SuccessResponse("OK"));
1231
+ }
1232
+ const results = await this.executeHealthChecks();
1233
+ const allOk = results.every((r) => r.status);
1234
+ const status = allOk ? HttpStatusCodes.OK : HttpStatusCodes.SERVICE_UNAVAILABLE;
1235
+ const response = new SuccessResponse(allOk ? "OK" : "Service unavailable");
1236
+ if (!allOk) response.error = true;
1237
+ if (this.config.healthCheck?.detailed) response.data = {
1238
+ checks: results
1239
+ };
1240
+ return res.status(status).json(response);
1241
+ } catch {
1242
+ return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json(new InternalServerErrorException("Health check failed"));
1243
+ }
1244
+ }
1245
+ /**
1246
+ * Execute all registered health checks and return results.
1247
+ */
1248
+ async executeHealthChecks() {
1249
+ const checkResults = await Promise.allSettled(this.healthChecks.map(async ({ name, check }) => {
1250
+ try {
1251
+ const status = await Promise.resolve(check());
1252
+ return {
1253
+ name,
1254
+ status,
1255
+ error: null
1256
+ };
1257
+ } catch (error) {
1258
+ return {
1259
+ name,
1260
+ status: false,
1261
+ error: error.message
1262
+ };
1263
+ }
1264
+ }));
1265
+ return checkResults.map((result) => {
1266
+ if (result.status === "fulfilled") return result.value;
1267
+ return {
1268
+ name: "unknown",
1269
+ status: false,
1270
+ error: result.reason
1271
+ };
1272
+ });
1273
+ }
1274
+ /**
1186
1275
  * Register a new health check function for monitoring service dependencies.
1187
1276
  *
1188
1277
  * Health checks are executed when the health endpoint is accessed and
@@ -1213,28 +1302,8 @@ var ExpressServer = class {
1213
1302
  */
1214
1303
  async ready() {
1215
1304
  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
- });
1305
+ if (!this.healthChecks.length || getCatbeeServerGlobalConfig().skipHealthzChecksValidation) return true;
1306
+ const results = await this.executeHealthChecks();
1238
1307
  return results.every((r) => r.status === true);
1239
1308
  } catch (err) {
1240
1309
  getLogger().error({
@@ -1279,58 +1348,82 @@ var ExpressServer = class {
1279
1348
  await this.runHook("beforeStart", this.app);
1280
1349
  return new Promise((resolve, reject) => {
1281
1350
  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
- });
1351
+ const onListening = /* @__PURE__ */ __name(async () => {
1352
+ this.logServerStartInfo();
1353
+ if (this.server) await this.runHook("afterStart", this.server);
1354
+ resolve(this.server);
1355
+ }, "onListening");
1356
+ this.server = this.createServerInstance(onListening);
1357
+ this.setupConnectionTracking();
1358
+ this.setupServerErrorHandling(reject);
1328
1359
  } catch (error) {
1329
1360
  reject(error);
1330
1361
  }
1331
1362
  });
1332
1363
  }
1333
1364
  /**
1365
+ * Create HTTP or HTTPS server instance.
1366
+ */
1367
+ createServerInstance(onListening) {
1368
+ const listenArgs = [
1369
+ this.config.port,
1370
+ this.config.host,
1371
+ onListening
1372
+ ];
1373
+ if (this.config.https) {
1374
+ const httpsOptions = {
1375
+ ...this.config.https,
1376
+ key: readFileSync(this.config.https.key),
1377
+ cert: readFileSync(this.config.https.cert)
1378
+ };
1379
+ if (this.config.https.ca) {
1380
+ httpsOptions.ca = readFileSync(this.config.https.ca);
1381
+ }
1382
+ if (this.config.https.passphrase) {
1383
+ httpsOptions.passphrase = this.config.https.passphrase;
1384
+ }
1385
+ return https.createServer(httpsOptions, this.app).listen(...listenArgs);
1386
+ }
1387
+ return this.app.listen(...listenArgs);
1388
+ }
1389
+ /**
1390
+ * Set up connection tracking for graceful shutdown.
1391
+ */
1392
+ setupConnectionTracking() {
1393
+ this.server.on("connection", (conn) => {
1394
+ this.connections.add(conn);
1395
+ conn.on("close", () => this.connections.delete(conn));
1396
+ });
1397
+ }
1398
+ /**
1399
+ * Set up error handling for server startup.
1400
+ */
1401
+ setupServerErrorHandling(reject) {
1402
+ this.server.on("error", (err) => {
1403
+ getLogger().error({
1404
+ err
1405
+ }, "Server failed to start");
1406
+ reject(err);
1407
+ });
1408
+ }
1409
+ /**
1410
+ * Log server startup information.
1411
+ */
1412
+ logServerStartInfo() {
1413
+ const protocol = this.config.https ? "https" : "http";
1414
+ const url = `${protocol}://${this.config.host}:${this.config.port}`;
1415
+ getLogger().info(`Server running on ${url}`);
1416
+ if (this.config.healthCheck?.path) {
1417
+ getLogger().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`);
1418
+ }
1419
+ if (this.config.metrics?.enable && this.config.metrics.path) {
1420
+ getLogger().info(`Metrics available at ${url}${this.normalizePath(this.config.metrics.path, this.config.metrics.withGlobalPrefix)}`);
1421
+ }
1422
+ if (this.config.openApi?.enable) {
1423
+ getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
1424
+ }
1425
+ }
1426
+ /**
1334
1427
  * Stop the HTTP server gracefully.
1335
1428
  *
1336
1429
  * This method:
@@ -1357,6 +1450,23 @@ var ExpressServer = class {
1357
1450
  }
1358
1451
  this.isShuttingDown = true;
1359
1452
  await this.runHook("beforeStop", this.server);
1453
+ try {
1454
+ await this.gracefulShutdown();
1455
+ } catch (err) {
1456
+ getLogger().error({
1457
+ err
1458
+ }, "Graceful shutdown timed out");
1459
+ if (force) {
1460
+ getLogger().warn("Forcing connection destroy due to shutdown timeout");
1461
+ }
1462
+ } finally {
1463
+ await this.destroyConnections();
1464
+ }
1465
+ }
1466
+ /**
1467
+ * Perform graceful server shutdown with timeout.
1468
+ */
1469
+ async gracefulShutdown() {
1360
1470
  const shutdownTimeout = 1e4;
1361
1471
  const serverClosePromise = new Promise((resolve, reject) => {
1362
1472
  this.server.close(async (err) => {
@@ -1375,21 +1485,10 @@ var ExpressServer = class {
1375
1485
  });
1376
1486
  });
1377
1487
  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
- }
1488
+ await Promise.race([
1489
+ serverClosePromise,
1490
+ timeoutPromise
1491
+ ]);
1393
1492
  }
1394
1493
  /**
1395
1494
  * 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 };
package/string/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
@@ -91,5 +91,34 @@ function toTitleCase(str) {
91
91
  return str.split(/(\s+)/).map((part) => part.trim().length === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
92
92
  }
93
93
  __name(toTitleCase, "toTitleCase");
94
+ function escapeRegex(str) {
95
+ if (typeof str !== "string") throw new TypeError("Expected a string");
96
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
97
+ }
98
+ __name(escapeRegex, "escapeRegex");
99
+ function unescapeHtml(str) {
100
+ if (typeof str !== "string") return "";
101
+ const entities = {
102
+ "&amp;": "&",
103
+ "&lt;": "<",
104
+ "&gt;": ">",
105
+ "&quot;": '"',
106
+ "&#39;": "'",
107
+ "&#x27;": "'"
108
+ };
109
+ return str.replace(/&(?:amp|lt|gt|quot|#39|#x27);/g, (match) => entities[match] || match);
110
+ }
111
+ __name(unescapeHtml, "unescapeHtml");
112
+ function isBlank(str) {
113
+ return typeof str === "string" && str.trim().length === 0;
114
+ }
115
+ __name(isBlank, "isBlank");
116
+ function ellipsis(str, maxLength, suffix = "...") {
117
+ if (typeof str !== "string" || str.length <= maxLength) return str;
118
+ const truncated = str.slice(0, maxLength - suffix.length);
119
+ const lastSpace = truncated.lastIndexOf(" ");
120
+ return (lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated) + suffix;
121
+ }
122
+ __name(ellipsis, "ellipsis");
94
123
 
95
- export { capitalize, countOccurrences, equalsIgnoreCase, mask, reverse, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toTitleCase, truncate };
124
+ export { capitalize, countOccurrences, ellipsis, equalsIgnoreCase, escapeRegex, isBlank, mask, reverse, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toTitleCase, truncate, unescapeHtml };