@catbee/utils 2.2.0 → 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 +287 -62
- package/healthz-server/index.d.ts +134 -22
- package/healthz-server/index.mjs +287 -62
- package/package.json +1 -1
- package/server/index.cjs +234 -92
- package/server/index.d.ts +66 -3
- package/server/index.mjs +234 -92
- 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/healthz-server/index.mjs
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { createServer } from 'http';
|
|
26
|
+
import { trimChars } from '@catbee/utils/string';
|
|
26
27
|
import { Env } from '@catbee/utils/env';
|
|
27
28
|
|
|
28
29
|
var __defProp = Object.defineProperty;
|
|
@@ -58,6 +59,24 @@ __name(resolveConfig, "resolveConfig");
|
|
|
58
59
|
// src/healthz-server/healthz-server.ts
|
|
59
60
|
var SINGLETON_KEY = Symbol.for("CatbeeHealthzServer");
|
|
60
61
|
var _global = globalThis;
|
|
62
|
+
function formatHostForUrl(host) {
|
|
63
|
+
if (host.includes(":")) {
|
|
64
|
+
return `[${host}]`;
|
|
65
|
+
}
|
|
66
|
+
return host;
|
|
67
|
+
}
|
|
68
|
+
__name(formatHostForUrl, "formatHostForUrl");
|
|
69
|
+
function getErrorMessage(err) {
|
|
70
|
+
if (err instanceof Error) return err.message;
|
|
71
|
+
if (typeof err === "string" && err.trim().length > 0) return err;
|
|
72
|
+
return "Unknown error";
|
|
73
|
+
}
|
|
74
|
+
__name(getErrorMessage, "getErrorMessage");
|
|
75
|
+
function normalizeProbePath(p) {
|
|
76
|
+
const trimmed = trimChars(p, "/");
|
|
77
|
+
return trimmed ? `/${trimmed}` : "/";
|
|
78
|
+
}
|
|
79
|
+
__name(normalizeProbePath, "normalizeProbePath");
|
|
61
80
|
var HealthzServer = class _HealthzServer {
|
|
62
81
|
static {
|
|
63
82
|
__name(this, "HealthzServer");
|
|
@@ -65,15 +84,30 @@ var HealthzServer = class _HealthzServer {
|
|
|
65
84
|
server;
|
|
66
85
|
config;
|
|
67
86
|
startedAt = Date.now();
|
|
68
|
-
/**
|
|
69
|
-
|
|
87
|
+
/** Running address info for the Healthz probe server */
|
|
88
|
+
addressInfo = null;
|
|
89
|
+
/** Whether the health HTTP server is currently running and listening on its port */
|
|
90
|
+
running = false;
|
|
91
|
+
/** Whether application startup has completed (for `/startupz`) */
|
|
92
|
+
startupComplete = false;
|
|
70
93
|
/** Whether the service is ready to receive traffic (for `/readyz`) */
|
|
71
94
|
ready = false;
|
|
72
95
|
shuttingDown = false;
|
|
96
|
+
livenessChecks;
|
|
97
|
+
readinessChecks;
|
|
98
|
+
stoppingPromise;
|
|
73
99
|
onSigterm = /* @__PURE__ */ __name(() => this.initiateShutdown(), "onSigterm");
|
|
74
100
|
onSigint = /* @__PURE__ */ __name(() => this.initiateShutdown(), "onSigint");
|
|
75
101
|
constructor(config) {
|
|
76
102
|
this.config = config;
|
|
103
|
+
this.livenessChecks = [
|
|
104
|
+
...config.checks
|
|
105
|
+
];
|
|
106
|
+
this.readinessChecks = config.readinessChecks !== void 0 ? [
|
|
107
|
+
...config.readinessChecks
|
|
108
|
+
] : [
|
|
109
|
+
...config.checks
|
|
110
|
+
];
|
|
77
111
|
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
78
112
|
if (this.config.handleSignals !== false) {
|
|
79
113
|
process.once("SIGTERM", this.onSigterm);
|
|
@@ -95,68 +129,181 @@ var HealthzServer = class _HealthzServer {
|
|
|
95
129
|
const config = resolveConfig(opts);
|
|
96
130
|
const instance = new _HealthzServer(config);
|
|
97
131
|
_global[SINGLETON_KEY] = instance;
|
|
132
|
+
const bindHost = config.host.startsWith("[") && config.host.endsWith("]") ? config.host.slice(1, -1) : config.host;
|
|
98
133
|
return new Promise((resolve, reject) => {
|
|
99
134
|
const onError = /* @__PURE__ */ __name((err) => {
|
|
100
135
|
instance.cleanup();
|
|
136
|
+
instance.running = false;
|
|
137
|
+
instance.shuttingDown = false;
|
|
138
|
+
instance.addressInfo = null;
|
|
101
139
|
delete _global[SINGLETON_KEY];
|
|
102
140
|
reject(err);
|
|
103
141
|
}, "onError");
|
|
104
142
|
instance.server.once("error", onError);
|
|
105
143
|
instance.server.listen({
|
|
106
|
-
host:
|
|
144
|
+
host: bindHost,
|
|
107
145
|
port: config.port
|
|
108
146
|
}, () => {
|
|
109
147
|
instance.server.off("error", onError);
|
|
110
|
-
instance.
|
|
148
|
+
instance.server.on("error", () => {
|
|
149
|
+
});
|
|
150
|
+
instance.running = true;
|
|
111
151
|
const addr = instance.server.address();
|
|
112
152
|
if (!addr || typeof addr === "string") {
|
|
113
153
|
instance.cleanup();
|
|
154
|
+
instance.running = false;
|
|
155
|
+
instance.shuttingDown = false;
|
|
156
|
+
instance.addressInfo = null;
|
|
114
157
|
delete _global[SINGLETON_KEY];
|
|
115
158
|
reject(new Error("Failed to resolve health server address"));
|
|
116
159
|
return;
|
|
117
160
|
}
|
|
118
|
-
|
|
161
|
+
instance.addressInfo = {
|
|
119
162
|
address: addr.address,
|
|
120
163
|
family: addr.family,
|
|
121
164
|
port: addr.port
|
|
165
|
+
};
|
|
166
|
+
resolve({
|
|
167
|
+
...instance.addressInfo
|
|
122
168
|
});
|
|
123
169
|
});
|
|
124
170
|
});
|
|
125
171
|
}
|
|
126
|
-
/** Whether the
|
|
127
|
-
|
|
128
|
-
return
|
|
172
|
+
/** Whether the Healthz HTTP probe server is currently running and listening on its port. */
|
|
173
|
+
isRunning() {
|
|
174
|
+
return this.running;
|
|
175
|
+
}
|
|
176
|
+
/** Whether the Healthz HTTP probe server is currently running and listening on its port. */
|
|
177
|
+
static isRunning() {
|
|
178
|
+
return _global[SINGLETON_KEY]?.running ?? false;
|
|
179
|
+
}
|
|
180
|
+
/** Mark application startup as completed (switches `/startupz` to 200). */
|
|
181
|
+
markStartupComplete() {
|
|
182
|
+
this.startupComplete = true;
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
/** Mark application startup as completed (switches `/startupz` to 200). */
|
|
186
|
+
static markStartupComplete() {
|
|
187
|
+
_global[SINGLETON_KEY]?.markStartupComplete();
|
|
188
|
+
}
|
|
189
|
+
/** Set application startup completion status. */
|
|
190
|
+
setStartupComplete(complete) {
|
|
191
|
+
this.startupComplete = complete;
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
/** Set application startup completion status. */
|
|
195
|
+
static setStartupComplete(complete) {
|
|
196
|
+
_global[SINGLETON_KEY]?.setStartupComplete(complete);
|
|
197
|
+
}
|
|
198
|
+
/** Whether application startup has completed (for `/startupz`). */
|
|
199
|
+
isStartupComplete() {
|
|
200
|
+
return this.startupComplete;
|
|
201
|
+
}
|
|
202
|
+
/** Whether application startup has completed (for `/startupz`). */
|
|
203
|
+
static isStartupComplete() {
|
|
204
|
+
return _global[SINGLETON_KEY]?.isStartupComplete() ?? false;
|
|
205
|
+
}
|
|
206
|
+
/** Mark the service as ready / not-ready for traffic. */
|
|
207
|
+
setReady(ready) {
|
|
208
|
+
this.ready = ready;
|
|
209
|
+
return this;
|
|
129
210
|
}
|
|
130
211
|
/** Mark the service as ready / not-ready for traffic. */
|
|
131
212
|
static setReady(ready) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
213
|
+
_global[SINGLETON_KEY]?.setReady(ready);
|
|
214
|
+
}
|
|
215
|
+
/** Whether the service is currently marked as ready. */
|
|
216
|
+
isReady() {
|
|
217
|
+
return this.ready;
|
|
135
218
|
}
|
|
136
219
|
/** Whether the service is currently marked as ready. */
|
|
137
220
|
static isReady() {
|
|
138
|
-
return _global[SINGLETON_KEY]?.
|
|
221
|
+
return _global[SINGLETON_KEY]?.isReady() ?? false;
|
|
222
|
+
}
|
|
223
|
+
/** Get address info of this health server instance. */
|
|
224
|
+
getAddress() {
|
|
225
|
+
return this.addressInfo;
|
|
226
|
+
}
|
|
227
|
+
/** Get address info of the active singleton health server. */
|
|
228
|
+
static getAddress() {
|
|
229
|
+
return _global[SINGLETON_KEY]?.getAddress() ?? null;
|
|
230
|
+
}
|
|
231
|
+
/** Get port the health server is listening on. */
|
|
232
|
+
getPort() {
|
|
233
|
+
return this.addressInfo?.port;
|
|
234
|
+
}
|
|
235
|
+
/** Get port the active singleton health server is listening on. */
|
|
236
|
+
static getPort() {
|
|
237
|
+
return _global[SINGLETON_KEY]?.getPort();
|
|
238
|
+
}
|
|
239
|
+
/** Get configured host. */
|
|
240
|
+
getHost() {
|
|
241
|
+
return this.config.host;
|
|
242
|
+
}
|
|
243
|
+
/** Get configured host of the active singleton health server. */
|
|
244
|
+
static getHost() {
|
|
245
|
+
return _global[SINGLETON_KEY]?.getHost();
|
|
246
|
+
}
|
|
247
|
+
/** Get full URL for this health server instance. */
|
|
248
|
+
getUrl() {
|
|
249
|
+
if (!this.addressInfo) return void 0;
|
|
250
|
+
const host = formatHostForUrl(this.addressInfo.address);
|
|
251
|
+
return `http://${host}:${this.addressInfo.port}`;
|
|
252
|
+
}
|
|
253
|
+
/** Get full URL for the active singleton health server. */
|
|
254
|
+
static getUrl() {
|
|
255
|
+
return _global[SINGLETON_KEY]?.getUrl();
|
|
256
|
+
}
|
|
257
|
+
/** Gracefully stop this health server instance. */
|
|
258
|
+
async stop() {
|
|
259
|
+
return _HealthzServer.stop();
|
|
139
260
|
}
|
|
140
261
|
/** Gracefully stop the health-check server. */
|
|
141
262
|
static async stop() {
|
|
142
263
|
const instance = _global[SINGLETON_KEY];
|
|
143
264
|
if (!instance) return;
|
|
265
|
+
if (instance.stoppingPromise) {
|
|
266
|
+
return instance.stoppingPromise;
|
|
267
|
+
}
|
|
268
|
+
instance.shuttingDown = true;
|
|
269
|
+
instance.ready = false;
|
|
144
270
|
instance.cleanup();
|
|
145
|
-
|
|
271
|
+
instance.stoppingPromise = new Promise((resolve, reject) => {
|
|
272
|
+
let forceTimer;
|
|
273
|
+
const onDone = /* @__PURE__ */ __name((err) => {
|
|
274
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
275
|
+
instance.running = false;
|
|
276
|
+
instance.shuttingDown = false;
|
|
277
|
+
instance.addressInfo = null;
|
|
278
|
+
delete _global[SINGLETON_KEY];
|
|
279
|
+
if (err && err.code !== "ERR_SERVER_NOT_RUNNING") {
|
|
280
|
+
reject(err);
|
|
281
|
+
} else {
|
|
282
|
+
resolve();
|
|
283
|
+
}
|
|
284
|
+
}, "onDone");
|
|
146
285
|
if (typeof instance.server.closeIdleConnections === "function") {
|
|
147
286
|
instance.server.closeIdleConnections();
|
|
148
287
|
}
|
|
149
|
-
instance.server.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
288
|
+
if (typeof instance.server.closeAllConnections === "function") {
|
|
289
|
+
instance.server.closeAllConnections();
|
|
290
|
+
}
|
|
291
|
+
forceTimer = setTimeout(() => {
|
|
292
|
+
if (typeof instance.server.closeAllConnections === "function") {
|
|
293
|
+
instance.server.closeAllConnections();
|
|
294
|
+
}
|
|
295
|
+
onDone();
|
|
296
|
+
}, 5e3);
|
|
297
|
+
instance.server.close((err) => onDone(err ?? void 0));
|
|
153
298
|
});
|
|
299
|
+
return instance.stoppingPromise;
|
|
154
300
|
}
|
|
155
301
|
static getInstance() {
|
|
156
302
|
return _global[SINGLETON_KEY];
|
|
157
303
|
}
|
|
158
304
|
/**
|
|
159
305
|
* Register a named check on this HealthzServer instance dynamically.
|
|
306
|
+
* If a check with the same name already exists in the target probe, it is updated in-place.
|
|
160
307
|
*
|
|
161
308
|
* @param check - The named check to register
|
|
162
309
|
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
@@ -164,18 +311,57 @@ var HealthzServer = class _HealthzServer {
|
|
|
164
311
|
*/
|
|
165
312
|
registerCheck(check, type = "readiness") {
|
|
166
313
|
if (type === "liveness" || type === "both") {
|
|
167
|
-
this.
|
|
314
|
+
const idx = this.livenessChecks.findIndex((c) => c.name === check.name);
|
|
315
|
+
if (idx !== -1) {
|
|
316
|
+
this.livenessChecks[idx] = check;
|
|
317
|
+
} else {
|
|
318
|
+
this.livenessChecks.push(check);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (type === "readiness" || type === "both") {
|
|
322
|
+
const idx = this.readinessChecks.findIndex((c) => c.name === check.name);
|
|
323
|
+
if (idx !== -1) {
|
|
324
|
+
this.readinessChecks[idx] = check;
|
|
325
|
+
} else {
|
|
326
|
+
this.readinessChecks.push(check);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return this;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Unregister a named check on this HealthzServer instance.
|
|
333
|
+
*
|
|
334
|
+
* @param name - The name of the check to remove
|
|
335
|
+
* @param type - Which probe to remove from ('liveness', 'readiness', or 'both')
|
|
336
|
+
* @returns This instance for chaining
|
|
337
|
+
*/
|
|
338
|
+
unregisterCheck(name, type = "both") {
|
|
339
|
+
if (type === "liveness" || type === "both") {
|
|
340
|
+
const idx = this.livenessChecks.findIndex((c) => c.name === name);
|
|
341
|
+
if (idx !== -1) this.livenessChecks.splice(idx, 1);
|
|
168
342
|
}
|
|
169
343
|
if (type === "readiness" || type === "both") {
|
|
170
|
-
this.
|
|
171
|
-
|
|
172
|
-
];
|
|
173
|
-
this.config.readinessChecks.push(check);
|
|
344
|
+
const idx = this.readinessChecks.findIndex((c) => c.name === name);
|
|
345
|
+
if (idx !== -1) this.readinessChecks.splice(idx, 1);
|
|
174
346
|
}
|
|
175
347
|
return this;
|
|
176
348
|
}
|
|
177
349
|
/**
|
|
350
|
+
* Get all registered checks on this instance.
|
|
351
|
+
*/
|
|
352
|
+
getChecks() {
|
|
353
|
+
return {
|
|
354
|
+
liveness: [
|
|
355
|
+
...this.livenessChecks
|
|
356
|
+
],
|
|
357
|
+
readiness: [
|
|
358
|
+
...this.readinessChecks
|
|
359
|
+
]
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
178
363
|
* Register a named check on the active singleton HealthzServer instance (if started).
|
|
364
|
+
* If a check with the same name already exists in the target probe, it is updated in-place.
|
|
179
365
|
*
|
|
180
366
|
* @param check - The named check to register
|
|
181
367
|
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
@@ -183,51 +369,73 @@ var HealthzServer = class _HealthzServer {
|
|
|
183
369
|
static registerCheck(check, type = "readiness") {
|
|
184
370
|
_global[SINGLETON_KEY]?.registerCheck(check, type);
|
|
185
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Unregister a named check on the active singleton HealthzServer instance (if started).
|
|
374
|
+
*
|
|
375
|
+
* @param name - Name of the check to remove
|
|
376
|
+
* @param type - Which probe to remove from ('liveness', 'readiness', or 'both')
|
|
377
|
+
*/
|
|
378
|
+
static unregisterCheck(name, type = "both") {
|
|
379
|
+
_global[SINGLETON_KEY]?.unregisterCheck(name, type);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Get all registered checks on the active singleton HealthzServer instance.
|
|
383
|
+
*/
|
|
384
|
+
static getChecks() {
|
|
385
|
+
return _global[SINGLETON_KEY]?.getChecks() ?? {
|
|
386
|
+
liveness: [],
|
|
387
|
+
readiness: []
|
|
388
|
+
};
|
|
389
|
+
}
|
|
186
390
|
async handleRequest(req, res) {
|
|
187
391
|
const isHead = req.method === "HEAD";
|
|
188
392
|
if (req.method !== "GET" && !isHead) {
|
|
393
|
+
res.setHeader("Allow", "GET, HEAD");
|
|
189
394
|
this.sendJson(res, 405, {
|
|
190
395
|
error: "Method Not Allowed"
|
|
191
396
|
}, isHead);
|
|
192
397
|
return;
|
|
193
398
|
}
|
|
194
|
-
const
|
|
399
|
+
const rawUrl = req.url ?? "/";
|
|
400
|
+
const pathname = normalizeProbePath(rawUrl.split("?")[0]);
|
|
401
|
+
const matchPath = /* @__PURE__ */ __name((configured) => pathname === normalizeProbePath(configured), "matchPath");
|
|
195
402
|
try {
|
|
196
|
-
if (
|
|
403
|
+
if (matchPath(this.config.healthzPath)) {
|
|
197
404
|
await this.handleLiveness(res, isHead);
|
|
198
|
-
} else if (
|
|
405
|
+
} else if (matchPath(this.config.readyzPath)) {
|
|
199
406
|
await this.handleReadiness(res, isHead);
|
|
200
|
-
} else if (
|
|
407
|
+
} else if (matchPath(this.config.startupzPath)) {
|
|
201
408
|
this.handleStartup(res, isHead);
|
|
202
409
|
} else {
|
|
203
410
|
this.sendJson(res, 404, {
|
|
204
411
|
error: "Not Found"
|
|
205
412
|
}, isHead);
|
|
206
413
|
}
|
|
207
|
-
} catch (
|
|
208
|
-
const message = err instanceof Error ? err.message : "Internal Server Error";
|
|
414
|
+
} catch (_err) {
|
|
209
415
|
this.sendJson(res, 500, {
|
|
210
|
-
error:
|
|
416
|
+
error: "Internal Server Error"
|
|
211
417
|
}, isHead);
|
|
212
418
|
}
|
|
213
419
|
}
|
|
214
420
|
/** `/healthz` — Liveness probe */
|
|
215
421
|
async handleLiveness(res, isHead = false) {
|
|
216
|
-
const results = await this.runChecks(this.
|
|
217
|
-
|
|
422
|
+
const results = await this.runChecks(this.livenessChecks);
|
|
423
|
+
const livenessCheck = this.config.onLivenessCheck ?? this.config.onHealthCheck;
|
|
424
|
+
if (livenessCheck) {
|
|
218
425
|
const start = Date.now();
|
|
219
426
|
try {
|
|
220
|
-
const
|
|
427
|
+
const result = await this.executeCheck(livenessCheck, this.config.checkTimeoutMs);
|
|
428
|
+
const ok = result === void 0 ? true : !!result;
|
|
221
429
|
results.push({
|
|
222
430
|
name: "custom",
|
|
223
|
-
ok
|
|
431
|
+
ok,
|
|
224
432
|
durationMs: Date.now() - start,
|
|
225
433
|
...!ok ? {
|
|
226
434
|
error: "Health check returned false"
|
|
227
435
|
} : {}
|
|
228
436
|
});
|
|
229
437
|
} catch (err) {
|
|
230
|
-
const message = err
|
|
438
|
+
const message = getErrorMessage(err);
|
|
231
439
|
results.push({
|
|
232
440
|
name: "custom",
|
|
233
441
|
ok: false,
|
|
@@ -255,22 +463,22 @@ var HealthzServer = class _HealthzServer {
|
|
|
255
463
|
], isHead);
|
|
256
464
|
return;
|
|
257
465
|
}
|
|
258
|
-
const
|
|
259
|
-
const results = await this.runChecks(checksToRun);
|
|
466
|
+
const results = await this.runChecks(this.readinessChecks);
|
|
260
467
|
if (this.config.onReadinessCheck) {
|
|
261
468
|
const start = Date.now();
|
|
262
469
|
try {
|
|
263
|
-
const
|
|
470
|
+
const result = await this.executeCheck(this.config.onReadinessCheck, this.config.checkTimeoutMs);
|
|
471
|
+
const ready = result === void 0 ? true : !!result;
|
|
264
472
|
results.push({
|
|
265
473
|
name: "custom-readiness",
|
|
266
|
-
ok:
|
|
474
|
+
ok: ready,
|
|
267
475
|
durationMs: Date.now() - start,
|
|
268
476
|
...!ready ? {
|
|
269
477
|
error: "Readiness check returned false"
|
|
270
478
|
} : {}
|
|
271
479
|
});
|
|
272
480
|
} catch (err) {
|
|
273
|
-
const message = err
|
|
481
|
+
const message = getErrorMessage(err);
|
|
274
482
|
results.push({
|
|
275
483
|
name: "custom-readiness",
|
|
276
484
|
ok: false,
|
|
@@ -286,7 +494,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
286
494
|
}
|
|
287
495
|
/** `/startupz` — Startup probe */
|
|
288
496
|
handleStartup(res, isHead = false) {
|
|
289
|
-
if (this.
|
|
497
|
+
if (this.startupComplete) {
|
|
290
498
|
this.sendProbe(res, 200, "ok", [], isHead);
|
|
291
499
|
} else {
|
|
292
500
|
this.sendProbe(res, 503, "unhealthy", [
|
|
@@ -294,7 +502,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
294
502
|
name: "startup",
|
|
295
503
|
ok: false,
|
|
296
504
|
durationMs: 0,
|
|
297
|
-
error: "
|
|
505
|
+
error: "Application startup not complete"
|
|
298
506
|
}
|
|
299
507
|
], isHead);
|
|
300
508
|
}
|
|
@@ -305,13 +513,17 @@ var HealthzServer = class _HealthzServer {
|
|
|
305
513
|
const start = Date.now();
|
|
306
514
|
try {
|
|
307
515
|
const result = await this.executeCheck(check, this.config.checkTimeoutMs);
|
|
516
|
+
const ok = result === void 0 ? true : !!result;
|
|
308
517
|
return {
|
|
309
518
|
name,
|
|
310
|
-
ok
|
|
311
|
-
durationMs: Date.now() - start
|
|
519
|
+
ok,
|
|
520
|
+
durationMs: Date.now() - start,
|
|
521
|
+
...!ok ? {
|
|
522
|
+
error: "Check returned false"
|
|
523
|
+
} : {}
|
|
312
524
|
};
|
|
313
525
|
} catch (err) {
|
|
314
|
-
const message = err
|
|
526
|
+
const message = getErrorMessage(err);
|
|
315
527
|
return {
|
|
316
528
|
name,
|
|
317
529
|
ok: false,
|
|
@@ -322,6 +534,9 @@ var HealthzServer = class _HealthzServer {
|
|
|
322
534
|
}));
|
|
323
535
|
}
|
|
324
536
|
sendProbe(res, httpStatus, status, checks, isHead = false) {
|
|
537
|
+
if (this.shuttingDown) {
|
|
538
|
+
res.setHeader("Connection", "close");
|
|
539
|
+
}
|
|
325
540
|
const body = {
|
|
326
541
|
status,
|
|
327
542
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -333,6 +548,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
333
548
|
this.sendJson(res, httpStatus, body, isHead);
|
|
334
549
|
}
|
|
335
550
|
sendJson(res, status, body, isHead = false) {
|
|
551
|
+
if (res.headersSent) return;
|
|
336
552
|
const payload = JSON.stringify(body);
|
|
337
553
|
res.writeHead(status, {
|
|
338
554
|
"Content-Type": "application/json; charset=utf-8",
|
|
@@ -346,27 +562,37 @@ var HealthzServer = class _HealthzServer {
|
|
|
346
562
|
res.end(payload);
|
|
347
563
|
}
|
|
348
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Executes a check function with timeout and cooperative cancellation.
|
|
567
|
+
*
|
|
568
|
+
* Note on cancellation: The AbortController aborts when `ms` expires, which signals cooperative
|
|
569
|
+
* consumers (e.g., fetch, pg, ioredis) to terminate their work. The attached .catch() on checkPromise
|
|
570
|
+
* prevents unhandled rejections if the check promise rejects after the timeout has already resolved.
|
|
571
|
+
*/
|
|
349
572
|
async executeCheck(fn, ms) {
|
|
350
573
|
const controller = new AbortController();
|
|
351
574
|
let timer;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
try {
|
|
360
|
-
const checkPromise = Promise.resolve().then(() => fn(controller.signal));
|
|
361
|
-
checkPromise.catch(() => {
|
|
575
|
+
if (ms > 0) {
|
|
576
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
577
|
+
timer = setTimeout(() => {
|
|
578
|
+
const error = new Error(`Check timed out after ${ms}ms`);
|
|
579
|
+
controller.abort(error);
|
|
580
|
+
reject(error);
|
|
581
|
+
}, ms);
|
|
362
582
|
});
|
|
363
|
-
|
|
364
|
-
checkPromise
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
583
|
+
try {
|
|
584
|
+
const checkPromise = Promise.resolve().then(() => fn(controller.signal));
|
|
585
|
+
checkPromise.catch(() => {
|
|
586
|
+
});
|
|
587
|
+
return await Promise.race([
|
|
588
|
+
checkPromise,
|
|
589
|
+
timeoutPromise
|
|
590
|
+
]);
|
|
591
|
+
} finally {
|
|
592
|
+
if (timer) clearTimeout(timer);
|
|
593
|
+
}
|
|
369
594
|
}
|
|
595
|
+
return await Promise.resolve().then(() => fn(controller.signal));
|
|
370
596
|
}
|
|
371
597
|
async initiateShutdown() {
|
|
372
598
|
if (this.shuttingDown) return;
|
|
@@ -381,8 +607,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
381
607
|
process.off("SIGTERM", this.onSigterm);
|
|
382
608
|
process.off("SIGINT", this.onSigint);
|
|
383
609
|
this.ready = false;
|
|
384
|
-
this.
|
|
385
|
-
this.shuttingDown = false;
|
|
610
|
+
this.startupComplete = false;
|
|
386
611
|
}
|
|
387
612
|
};
|
|
388
613
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catbee/utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "A modular, production-grade utility toolkit for Node.js and TypeScript, designed for robust, scalable applications (including Express-based services). All utilities are tree-shakable and can be imported independently.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|