@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/healthz-server/index.cjs +270 -58
- package/healthz-server/index.d.ts +118 -14
- package/healthz-server/index.mjs +270 -58
- package/package.json +1 -1
- package/server/index.cjs +131 -17
- package/server/index.d.ts +56 -3
- package/server/index.mjs +131 -17
- package/string/index.cjs +44 -2
- package/string/index.d.ts +36 -1
- package/string/index.mjs +42 -3
- package/types/index.d.ts +43 -15
- package/url/index.cjs +5 -4
- package/url/index.mjs +5 -4
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
165
|
-
* > `
|
|
166
|
-
* >
|
|
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
|
|
178
|
-
* > `
|
|
179
|
-
* >
|
|
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
|
-
/**
|
|
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
|
|
85
|
-
return
|
|
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
|
-
|
|
96
|
-
parsedUrl.pathname =
|
|
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
|
|
83
|
-
return segment
|
|
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
|
-
|
|
94
|
-
parsedUrl.pathname =
|
|
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 {
|