@catbee/utils 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/string/index.cjs CHANGED
@@ -44,7 +44,8 @@ function toCamelCase(str) {
44
44
  }
45
45
  __name(toCamelCase, "toCamelCase");
46
46
  function slugify(str) {
47
- return str.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
47
+ const slug = str.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-");
48
+ return trimChars(slug, "-");
48
49
  }
49
50
  __name(slugify, "slugify");
50
51
  function truncate(str, len) {
@@ -69,7 +70,13 @@ function mask(str, visibleStart = 0, visibleEnd = 0, maskChar = "*") {
69
70
  }
70
71
  __name(mask, "mask");
71
72
  function stripHtml(str) {
72
- return str.replace(/<[^>]*>/g, "");
73
+ let prev;
74
+ let result = str;
75
+ do {
76
+ prev = result;
77
+ result = result.replace(/<[^<>]*>/g, "");
78
+ } while (result !== prev);
79
+ return result;
73
80
  }
74
81
  __name(stripHtml, "stripHtml");
75
82
  function equalsIgnoreCase(a, b) {
@@ -122,6 +129,38 @@ function ellipsis(str, maxLength, suffix = "...") {
122
129
  return (lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated) + suffix;
123
130
  }
124
131
  __name(ellipsis, "ellipsis");
132
+ function trimChars(str, chars = " ") {
133
+ if (!str) return "";
134
+ let start = 0;
135
+ let end = str.length;
136
+ while (start < end && chars.includes(str[start])) {
137
+ start++;
138
+ }
139
+ while (end > start && chars.includes(str[end - 1])) {
140
+ end--;
141
+ }
142
+ return str.slice(start, end);
143
+ }
144
+ __name(trimChars, "trimChars");
145
+ function trimLeadingChars(str, chars = " ") {
146
+ if (!str) return "";
147
+ let start = 0;
148
+ const end = str.length;
149
+ while (start < end && chars.includes(str[start])) {
150
+ start++;
151
+ }
152
+ return str.slice(start, end);
153
+ }
154
+ __name(trimLeadingChars, "trimLeadingChars");
155
+ function trimTrailingChars(str, chars = " ") {
156
+ if (!str) return "";
157
+ let end = str.length;
158
+ while (end > 0 && chars.includes(str[end - 1])) {
159
+ end--;
160
+ }
161
+ return str.slice(0, end);
162
+ }
163
+ __name(trimTrailingChars, "trimTrailingChars");
125
164
 
126
165
  exports.capitalize = capitalize;
127
166
  exports.countOccurrences = countOccurrences;
@@ -138,5 +177,8 @@ exports.toKebabCase = toKebabCase;
138
177
  exports.toPascalCase = toPascalCase;
139
178
  exports.toSnakeCase = toSnakeCase;
140
179
  exports.toTitleCase = toTitleCase;
180
+ exports.trimChars = trimChars;
181
+ exports.trimLeadingChars = trimLeadingChars;
182
+ exports.trimTrailingChars = trimTrailingChars;
141
183
  exports.truncate = truncate;
142
184
  exports.unescapeHtml = unescapeHtml;
package/string/index.d.ts CHANGED
@@ -165,5 +165,40 @@ declare function isBlank(str: string): boolean;
165
165
  * ellipsis('The quick brown fox', 10); // 'The quick...'
166
166
  */
167
167
  declare function ellipsis(str: string, maxLength: number, suffix?: string): string;
168
+ /**
169
+ * Trims leading and trailing occurrences of specified characters in linear O(n) time
170
+ * without using regular expressions (immune to polynomial ReDoS).
171
+ *
172
+ * @param {string} str - The input string.
173
+ * @param {string} [chars=' '] - Character or set of characters to trim.
174
+ * @returns {string} The trimmed string.
175
+ *
176
+ * @example
177
+ * trimChars('///api/users///', '/'); // 'api/users'
178
+ * trimChars('---hello-world---', '-'); // 'hello-world'
179
+ */
180
+ declare function trimChars(str: string, chars?: string): string;
181
+ /**
182
+ * Trims leading occurrences of specified characters in linear O(n) time.
183
+ *
184
+ * @param {string} str - The input string.
185
+ * @param {string} [chars=' '] - Character or set of characters to trim from start.
186
+ * @returns {string} The trimmed string.
187
+ *
188
+ * @example
189
+ * trimLeadingChars('///api/users', '/'); // 'api/users'
190
+ */
191
+ declare function trimLeadingChars(str: string, chars?: string): string;
192
+ /**
193
+ * Trims trailing occurrences of specified characters in linear O(n) time.
194
+ *
195
+ * @param {string} str - The input string.
196
+ * @param {string} [chars=' '] - Character or set of characters to trim from end.
197
+ * @returns {string} The trimmed string.
198
+ *
199
+ * @example
200
+ * trimTrailingChars('https://example.com///', '/'); // 'https://example.com'
201
+ */
202
+ declare function trimTrailingChars(str: string, chars?: string): string;
168
203
 
169
- export { capitalize, countOccurrences, ellipsis, equalsIgnoreCase, escapeRegex, isBlank, mask, reverse, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toTitleCase, truncate, unescapeHtml };
204
+ export { capitalize, countOccurrences, ellipsis, equalsIgnoreCase, escapeRegex, isBlank, mask, reverse, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toTitleCase, trimChars, trimLeadingChars, trimTrailingChars, truncate, unescapeHtml };
package/string/index.mjs CHANGED
@@ -42,7 +42,8 @@ function toCamelCase(str) {
42
42
  }
43
43
  __name(toCamelCase, "toCamelCase");
44
44
  function slugify(str) {
45
- return str.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
45
+ const slug = str.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-");
46
+ return trimChars(slug, "-");
46
47
  }
47
48
  __name(slugify, "slugify");
48
49
  function truncate(str, len) {
@@ -67,7 +68,13 @@ function mask(str, visibleStart = 0, visibleEnd = 0, maskChar = "*") {
67
68
  }
68
69
  __name(mask, "mask");
69
70
  function stripHtml(str) {
70
- return str.replace(/<[^>]*>/g, "");
71
+ let prev;
72
+ let result = str;
73
+ do {
74
+ prev = result;
75
+ result = result.replace(/<[^<>]*>/g, "");
76
+ } while (result !== prev);
77
+ return result;
71
78
  }
72
79
  __name(stripHtml, "stripHtml");
73
80
  function equalsIgnoreCase(a, b) {
@@ -120,5 +127,37 @@ function ellipsis(str, maxLength, suffix = "...") {
120
127
  return (lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated) + suffix;
121
128
  }
122
129
  __name(ellipsis, "ellipsis");
130
+ function trimChars(str, chars = " ") {
131
+ if (!str) return "";
132
+ let start = 0;
133
+ let end = str.length;
134
+ while (start < end && chars.includes(str[start])) {
135
+ start++;
136
+ }
137
+ while (end > start && chars.includes(str[end - 1])) {
138
+ end--;
139
+ }
140
+ return str.slice(start, end);
141
+ }
142
+ __name(trimChars, "trimChars");
143
+ function trimLeadingChars(str, chars = " ") {
144
+ if (!str) return "";
145
+ let start = 0;
146
+ const end = str.length;
147
+ while (start < end && chars.includes(str[start])) {
148
+ start++;
149
+ }
150
+ return str.slice(start, end);
151
+ }
152
+ __name(trimLeadingChars, "trimLeadingChars");
153
+ function trimTrailingChars(str, chars = " ") {
154
+ if (!str) return "";
155
+ let end = str.length;
156
+ while (end > 0 && chars.includes(str[end - 1])) {
157
+ end--;
158
+ }
159
+ return str.slice(0, end);
160
+ }
161
+ __name(trimTrailingChars, "trimTrailingChars");
123
162
 
124
- export { capitalize, countOccurrences, ellipsis, equalsIgnoreCase, escapeRegex, isBlank, mask, reverse, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toTitleCase, truncate, unescapeHtml };
163
+ export { capitalize, countOccurrences, ellipsis, equalsIgnoreCase, escapeRegex, isBlank, mask, reverse, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toTitleCase, trimChars, trimLeadingChars, trimTrailingChars, truncate, unescapeHtml };
package/types/index.d.ts CHANGED
@@ -161,28 +161,42 @@ type Without<T, U> = {
161
161
  * A health check function that can be synchronous or asynchronous.
162
162
  * Receives an AbortSignal that is triggered when `checkTimeoutMs` is exceeded.
163
163
  *
164
- * > **Cancellation Note**: Cancellation is cooperative. The check function must listen to
165
- * > `signal.aborted` or pass `signal` to underlying asynchronous APIs (e.g. database drivers, `fetch`).
166
- * > Synchronous CPU-bound loops or operations that ignore `signal` cannot be forcibly stopped by JavaScript.
164
+ * > **Cancellation & Timeout Semantics**:
165
+ * > When `checkTimeoutMs` expires, the HTTP probe response immediately fast-fails with 503
166
+ * > and aborts the supplied `AbortSignal`.
167
+ * >
168
+ * > However, runtime cancellation in JavaScript is **strictly cooperative**. The probe server cannot
169
+ * > preemptively interrupt executing JavaScript or force-cancel asynchronous work that does not listen
170
+ * > to the signal. If a check executes asynchronous tasks without passing `signal`
171
+ * > (e.g., `await db.query(...)` without `{ signal }`), that task will continue running in the background.
172
+ * > To prevent background resource leaks, always pass `signal` to underlying drivers/clients
173
+ * > (e.g. `fetch(url, { signal })`, database clients, HTTP clients) or check `signal?.aborted`.
167
174
  *
168
- * - Return `true` (or resolve to true) to signal healthy.
175
+ * - Return `true` (or resolve to true, or resolve void without error) to signal healthy.
169
176
  * - Return `false` (or resolve to false) to signal unhealthy.
170
177
  * - Throw an error (or reject) to signal unhealthy with an error message.
171
178
  */
172
- type HealthCheckFn = (signal?: AbortSignal) => boolean | Promise<boolean>;
179
+ type HealthCheckFn = (signal?: AbortSignal) => boolean | void | Promise<boolean | void>;
173
180
  /**
174
181
  * A readiness check function.
175
182
  * Receives an AbortSignal that is triggered when `checkTimeoutMs` is exceeded.
176
183
  *
177
- * > **Cancellation Note**: Cancellation is cooperative. The check function must listen to
178
- * > `signal.aborted` or pass `signal` to underlying asynchronous APIs (e.g. database drivers, `fetch`).
179
- * > Synchronous CPU-bound loops or operations that ignore `signal` cannot be forcibly stopped by JavaScript.
184
+ * > **Cancellation & Timeout Semantics**:
185
+ * > When `checkTimeoutMs` expires, the HTTP probe response immediately fast-fails with 503
186
+ * > and aborts the supplied `AbortSignal`.
187
+ * >
188
+ * > However, runtime cancellation in JavaScript is **strictly cooperative**. The probe server cannot
189
+ * > preemptively interrupt executing JavaScript or force-cancel asynchronous work that does not listen
190
+ * > to the signal. If a check executes asynchronous tasks without passing `signal`
191
+ * > (e.g., `await db.query(...)` without `{ signal }`), that task will continue running in the background.
192
+ * > To prevent background resource leaks, always pass `signal` to underlying drivers/clients
193
+ * > (e.g. `fetch(url, { signal })`, database clients, HTTP clients) or check `signal?.aborted`.
180
194
  *
181
- * - Return `true` (or resolve to true) to signal ready.
195
+ * - Return `true` (or resolve to true, or resolve void without error) to signal ready.
182
196
  * - Return `false` (or resolve to false) to signal not ready.
183
197
  * - Throw an error (or reject) to signal not ready with an error.
184
198
  */
185
- type ReadinessCheckFn = (signal?: AbortSignal) => boolean | Promise<boolean>;
199
+ type ReadinessCheckFn = (signal?: AbortSignal) => boolean | void | Promise<boolean | void>;
186
200
  /**
187
201
  * A named health check with an associated check function.
188
202
  */
@@ -191,10 +205,11 @@ interface NamedCheck {
191
205
  name: string;
192
206
  /**
193
207
  * Check function — return false or throw to indicate failure.
208
+ * Resolving void without throwing is considered healthy/ready.
194
209
  * Receives an AbortSignal that is triggered when the check times out.
195
- * Cancellation via the signal is cooperative.
210
+ * Cancellation via the signal is cooperative; pass `signal` to underlying operations.
196
211
  */
197
- check: (signal?: AbortSignal) => boolean | Promise<boolean>;
212
+ check: (signal?: AbortSignal) => boolean | void | Promise<boolean | void>;
198
213
  }
199
214
  /**
200
215
  * Configuration for the standalone healthz HTTP server.
@@ -255,14 +270,27 @@ interface CatbeeHealthzServerConfig {
255
270
  /**
256
271
  * Custom liveness check function. Runs *in addition to* `checks`.
257
272
  * Return `false` or throw to indicate unhealthy.
273
+ * Can also be provided via `onLivenessCheck`.
258
274
  */
259
275
  onHealthCheck?: HealthCheckFn;
276
+ /**
277
+ * Symmetrical alias for `onHealthCheck` — custom liveness check function.
278
+ * Runs *in addition to* `checks` on `/healthz`.
279
+ * Return `false` or throw to indicate unhealthy.
280
+ */
281
+ onLivenessCheck?: HealthCheckFn;
260
282
  /**
261
283
  * Custom readiness check function. Runs *in addition to* `readinessChecks`.
262
284
  * Return `false` or throw to indicate not ready.
263
285
  */
264
286
  onReadinessCheck?: ReadinessCheckFn;
265
- /** Timeout (ms) per individual check before it's considered failed
287
+ /**
288
+ * Timeout (ms) per individual check before it's considered failed.
289
+ *
290
+ * When exceeded, the probe immediately fast-fails with status 503 and triggers
291
+ * `abort()` on the check's `AbortSignal`. Note that cancellation is cooperative;
292
+ * checks should forward `signal` to downstream clients (e.g. database drivers, fetch)
293
+ * to avoid orphaned background execution.
266
294
  * - **default**: `5000`
267
295
  * - **env**: `HEALTHZ_CHECK_TIMEOUT_MS`
268
296
  */
@@ -657,9 +685,9 @@ interface CatbeeServerHooks {
657
685
  /** Called before server starts listening */
658
686
  beforeStart?: (app: Express) => Promise<void> | void;
659
687
  /** Called after server is ready */
660
- afterStart?: (server: http.Server) => Promise<void> | void;
688
+ afterStart?: (server: http.Server | https.Server) => Promise<void> | void;
661
689
  /** Called before graceful shutdown */
662
- beforeStop?: (server: http.Server) => Promise<void> | void;
690
+ beforeStop?: (server: http.Server | https.Server) => Promise<void> | void;
663
691
  /** Called after server stops */
664
692
  afterStop?: () => Promise<void> | void;
665
693
  /** Custom error handler (overrides Catbee default if provided) */
package/url/index.cjs CHANGED
@@ -25,6 +25,7 @@
25
25
  'use strict';
26
26
 
27
27
  var url = require('url');
28
+ var string = require('@catbee/utils/string');
28
29
 
29
30
  var __defProp = Object.defineProperty;
30
31
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
@@ -81,8 +82,8 @@ function getDomain(url$1, removeSubdomains = false) {
81
82
  __name(getDomain, "getDomain");
82
83
  function joinPaths(...segments) {
83
84
  let result = segments.filter((s) => s !== void 0 && s !== null).map((segment, index) => {
84
- if (index === 0) return segment.replace(/\/+$/, "");
85
- return segment.replace(/^\/+|\/+$/g, "");
85
+ if (index === 0) return string.trimTrailingChars(segment, "/");
86
+ return string.trimChars(segment, "/");
86
87
  }).filter(Boolean).join("/");
87
88
  if (segments[0] === "" && !result.startsWith("/")) result = "/" + result;
88
89
  return result;
@@ -92,8 +93,8 @@ function normalizeUrl(url$1, base) {
92
93
  try {
93
94
  let normalized = url$1.startsWith("//") ? `https:${url$1}` : url$1;
94
95
  const parsedUrl = base ? new url.URL(normalized, base) : new url.URL(normalized);
95
- parsedUrl.pathname = parsedUrl.pathname.replace(/\/+/g, "/");
96
- parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, "");
96
+ const collapsed = parsedUrl.pathname.replace(/\/+/g, "/");
97
+ parsedUrl.pathname = string.trimTrailingChars(collapsed, "/");
97
98
  parsedUrl.hostname = parsedUrl.hostname.toLowerCase();
98
99
  return parsedUrl.toString();
99
100
  } catch {
package/url/index.mjs CHANGED
@@ -23,6 +23,7 @@
23
23
  */
24
24
 
25
25
  import { URL, URLSearchParams } from 'url';
26
+ import { trimTrailingChars, trimChars } from '@catbee/utils/string';
26
27
 
27
28
  var __defProp = Object.defineProperty;
28
29
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
@@ -79,8 +80,8 @@ function getDomain(url, removeSubdomains = false) {
79
80
  __name(getDomain, "getDomain");
80
81
  function joinPaths(...segments) {
81
82
  let result = segments.filter((s) => s !== void 0 && s !== null).map((segment, index) => {
82
- if (index === 0) return segment.replace(/\/+$/, "");
83
- return segment.replace(/^\/+|\/+$/g, "");
83
+ if (index === 0) return trimTrailingChars(segment, "/");
84
+ return trimChars(segment, "/");
84
85
  }).filter(Boolean).join("/");
85
86
  if (segments[0] === "" && !result.startsWith("/")) result = "/" + result;
86
87
  return result;
@@ -90,8 +91,8 @@ function normalizeUrl(url, base) {
90
91
  try {
91
92
  let normalized = url.startsWith("//") ? `https:${url}` : url;
92
93
  const parsedUrl = base ? new URL(normalized, base) : new URL(normalized);
93
- parsedUrl.pathname = parsedUrl.pathname.replace(/\/+/g, "/");
94
- parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, "");
94
+ const collapsed = parsedUrl.pathname.replace(/\/+/g, "/");
95
+ parsedUrl.pathname = trimTrailingChars(collapsed, "/");
95
96
  parsedUrl.hostname = parsedUrl.hostname.toLowerCase();
96
97
  return parsedUrl.toString();
97
98
  } catch {