@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/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,6 +84,8 @@ var HealthzServer = class _HealthzServer {
|
|
|
65
84
|
server;
|
|
66
85
|
config;
|
|
67
86
|
startedAt = Date.now();
|
|
87
|
+
/** Running address info for the Healthz probe server */
|
|
88
|
+
addressInfo = null;
|
|
68
89
|
/** Whether the health HTTP server is currently running and listening on its port */
|
|
69
90
|
running = false;
|
|
70
91
|
/** Whether application startup has completed (for `/startupz`) */
|
|
@@ -72,10 +93,21 @@ var HealthzServer = class _HealthzServer {
|
|
|
72
93
|
/** Whether the service is ready to receive traffic (for `/readyz`) */
|
|
73
94
|
ready = false;
|
|
74
95
|
shuttingDown = false;
|
|
96
|
+
livenessChecks;
|
|
97
|
+
readinessChecks;
|
|
98
|
+
stoppingPromise;
|
|
75
99
|
onSigterm = /* @__PURE__ */ __name(() => this.initiateShutdown(), "onSigterm");
|
|
76
100
|
onSigint = /* @__PURE__ */ __name(() => this.initiateShutdown(), "onSigint");
|
|
77
101
|
constructor(config) {
|
|
78
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
|
+
];
|
|
79
111
|
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
80
112
|
if (this.config.handleSignals !== false) {
|
|
81
113
|
process.once("SIGTERM", this.onSigterm);
|
|
@@ -97,78 +129,181 @@ var HealthzServer = class _HealthzServer {
|
|
|
97
129
|
const config = resolveConfig(opts);
|
|
98
130
|
const instance = new _HealthzServer(config);
|
|
99
131
|
_global[SINGLETON_KEY] = instance;
|
|
132
|
+
const bindHost = config.host.startsWith("[") && config.host.endsWith("]") ? config.host.slice(1, -1) : config.host;
|
|
100
133
|
return new Promise((resolve, reject) => {
|
|
101
134
|
const onError = /* @__PURE__ */ __name((err) => {
|
|
102
135
|
instance.cleanup();
|
|
136
|
+
instance.running = false;
|
|
137
|
+
instance.shuttingDown = false;
|
|
138
|
+
instance.addressInfo = null;
|
|
103
139
|
delete _global[SINGLETON_KEY];
|
|
104
140
|
reject(err);
|
|
105
141
|
}, "onError");
|
|
106
142
|
instance.server.once("error", onError);
|
|
107
143
|
instance.server.listen({
|
|
108
|
-
host:
|
|
144
|
+
host: bindHost,
|
|
109
145
|
port: config.port
|
|
110
146
|
}, () => {
|
|
111
147
|
instance.server.off("error", onError);
|
|
148
|
+
instance.server.on("error", () => {
|
|
149
|
+
});
|
|
112
150
|
instance.running = true;
|
|
113
151
|
const addr = instance.server.address();
|
|
114
152
|
if (!addr || typeof addr === "string") {
|
|
115
153
|
instance.cleanup();
|
|
154
|
+
instance.running = false;
|
|
155
|
+
instance.shuttingDown = false;
|
|
156
|
+
instance.addressInfo = null;
|
|
116
157
|
delete _global[SINGLETON_KEY];
|
|
117
158
|
reject(new Error("Failed to resolve health server address"));
|
|
118
159
|
return;
|
|
119
160
|
}
|
|
120
|
-
|
|
161
|
+
instance.addressInfo = {
|
|
121
162
|
address: addr.address,
|
|
122
163
|
family: addr.family,
|
|
123
164
|
port: addr.port
|
|
165
|
+
};
|
|
166
|
+
resolve({
|
|
167
|
+
...instance.addressInfo
|
|
124
168
|
});
|
|
125
169
|
});
|
|
126
170
|
});
|
|
127
171
|
}
|
|
128
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. */
|
|
129
177
|
static isRunning() {
|
|
130
178
|
return _global[SINGLETON_KEY]?.running ?? false;
|
|
131
179
|
}
|
|
132
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). */
|
|
133
186
|
static markStartupComplete() {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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;
|
|
137
201
|
}
|
|
138
202
|
/** Whether application startup has completed (for `/startupz`). */
|
|
139
203
|
static isStartupComplete() {
|
|
140
|
-
return _global[SINGLETON_KEY]?.
|
|
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;
|
|
141
210
|
}
|
|
142
211
|
/** Mark the service as ready / not-ready for traffic. */
|
|
143
212
|
static setReady(ready) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
213
|
+
_global[SINGLETON_KEY]?.setReady(ready);
|
|
214
|
+
}
|
|
215
|
+
/** Whether the service is currently marked as ready. */
|
|
216
|
+
isReady() {
|
|
217
|
+
return this.ready;
|
|
147
218
|
}
|
|
148
219
|
/** Whether the service is currently marked as ready. */
|
|
149
220
|
static isReady() {
|
|
150
|
-
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();
|
|
151
260
|
}
|
|
152
261
|
/** Gracefully stop the health-check server. */
|
|
153
262
|
static async stop() {
|
|
154
263
|
const instance = _global[SINGLETON_KEY];
|
|
155
264
|
if (!instance) return;
|
|
265
|
+
if (instance.stoppingPromise) {
|
|
266
|
+
return instance.stoppingPromise;
|
|
267
|
+
}
|
|
268
|
+
instance.shuttingDown = true;
|
|
269
|
+
instance.ready = false;
|
|
156
270
|
instance.cleanup();
|
|
157
|
-
|
|
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");
|
|
158
285
|
if (typeof instance.server.closeIdleConnections === "function") {
|
|
159
286
|
instance.server.closeIdleConnections();
|
|
160
287
|
}
|
|
161
|
-
instance.server.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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));
|
|
165
298
|
});
|
|
299
|
+
return instance.stoppingPromise;
|
|
166
300
|
}
|
|
167
301
|
static getInstance() {
|
|
168
302
|
return _global[SINGLETON_KEY];
|
|
169
303
|
}
|
|
170
304
|
/**
|
|
171
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.
|
|
172
307
|
*
|
|
173
308
|
* @param check - The named check to register
|
|
174
309
|
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
@@ -176,18 +311,57 @@ var HealthzServer = class _HealthzServer {
|
|
|
176
311
|
*/
|
|
177
312
|
registerCheck(check, type = "readiness") {
|
|
178
313
|
if (type === "liveness" || type === "both") {
|
|
179
|
-
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);
|
|
180
342
|
}
|
|
181
343
|
if (type === "readiness" || type === "both") {
|
|
182
|
-
this.
|
|
183
|
-
|
|
184
|
-
];
|
|
185
|
-
this.config.readinessChecks.push(check);
|
|
344
|
+
const idx = this.readinessChecks.findIndex((c) => c.name === name);
|
|
345
|
+
if (idx !== -1) this.readinessChecks.splice(idx, 1);
|
|
186
346
|
}
|
|
187
347
|
return this;
|
|
188
348
|
}
|
|
189
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
|
+
/**
|
|
190
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.
|
|
191
365
|
*
|
|
192
366
|
* @param check - The named check to register
|
|
193
367
|
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
@@ -195,51 +369,73 @@ var HealthzServer = class _HealthzServer {
|
|
|
195
369
|
static registerCheck(check, type = "readiness") {
|
|
196
370
|
_global[SINGLETON_KEY]?.registerCheck(check, type);
|
|
197
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
|
+
}
|
|
198
390
|
async handleRequest(req, res) {
|
|
199
391
|
const isHead = req.method === "HEAD";
|
|
200
392
|
if (req.method !== "GET" && !isHead) {
|
|
393
|
+
res.setHeader("Allow", "GET, HEAD");
|
|
201
394
|
this.sendJson(res, 405, {
|
|
202
395
|
error: "Method Not Allowed"
|
|
203
396
|
}, isHead);
|
|
204
397
|
return;
|
|
205
398
|
}
|
|
206
|
-
const
|
|
399
|
+
const rawUrl = req.url ?? "/";
|
|
400
|
+
const pathname = normalizeProbePath(rawUrl.split("?")[0]);
|
|
401
|
+
const matchPath = /* @__PURE__ */ __name((configured) => pathname === normalizeProbePath(configured), "matchPath");
|
|
207
402
|
try {
|
|
208
|
-
if (
|
|
403
|
+
if (matchPath(this.config.healthzPath)) {
|
|
209
404
|
await this.handleLiveness(res, isHead);
|
|
210
|
-
} else if (
|
|
405
|
+
} else if (matchPath(this.config.readyzPath)) {
|
|
211
406
|
await this.handleReadiness(res, isHead);
|
|
212
|
-
} else if (
|
|
407
|
+
} else if (matchPath(this.config.startupzPath)) {
|
|
213
408
|
this.handleStartup(res, isHead);
|
|
214
409
|
} else {
|
|
215
410
|
this.sendJson(res, 404, {
|
|
216
411
|
error: "Not Found"
|
|
217
412
|
}, isHead);
|
|
218
413
|
}
|
|
219
|
-
} catch (
|
|
220
|
-
const message = err instanceof Error ? err.message : "Internal Server Error";
|
|
414
|
+
} catch (_err) {
|
|
221
415
|
this.sendJson(res, 500, {
|
|
222
|
-
error:
|
|
416
|
+
error: "Internal Server Error"
|
|
223
417
|
}, isHead);
|
|
224
418
|
}
|
|
225
419
|
}
|
|
226
420
|
/** `/healthz` — Liveness probe */
|
|
227
421
|
async handleLiveness(res, isHead = false) {
|
|
228
|
-
const results = await this.runChecks(this.
|
|
229
|
-
|
|
422
|
+
const results = await this.runChecks(this.livenessChecks);
|
|
423
|
+
const livenessCheck = this.config.onLivenessCheck ?? this.config.onHealthCheck;
|
|
424
|
+
if (livenessCheck) {
|
|
230
425
|
const start = Date.now();
|
|
231
426
|
try {
|
|
232
|
-
const
|
|
427
|
+
const result = await this.executeCheck(livenessCheck, this.config.checkTimeoutMs);
|
|
428
|
+
const ok = result === void 0 ? true : !!result;
|
|
233
429
|
results.push({
|
|
234
430
|
name: "custom",
|
|
235
|
-
ok
|
|
431
|
+
ok,
|
|
236
432
|
durationMs: Date.now() - start,
|
|
237
433
|
...!ok ? {
|
|
238
434
|
error: "Health check returned false"
|
|
239
435
|
} : {}
|
|
240
436
|
});
|
|
241
437
|
} catch (err) {
|
|
242
|
-
const message = err
|
|
438
|
+
const message = getErrorMessage(err);
|
|
243
439
|
results.push({
|
|
244
440
|
name: "custom",
|
|
245
441
|
ok: false,
|
|
@@ -267,22 +463,22 @@ var HealthzServer = class _HealthzServer {
|
|
|
267
463
|
], isHead);
|
|
268
464
|
return;
|
|
269
465
|
}
|
|
270
|
-
const
|
|
271
|
-
const results = await this.runChecks(checksToRun);
|
|
466
|
+
const results = await this.runChecks(this.readinessChecks);
|
|
272
467
|
if (this.config.onReadinessCheck) {
|
|
273
468
|
const start = Date.now();
|
|
274
469
|
try {
|
|
275
|
-
const
|
|
470
|
+
const result = await this.executeCheck(this.config.onReadinessCheck, this.config.checkTimeoutMs);
|
|
471
|
+
const ready = result === void 0 ? true : !!result;
|
|
276
472
|
results.push({
|
|
277
473
|
name: "custom-readiness",
|
|
278
|
-
ok:
|
|
474
|
+
ok: ready,
|
|
279
475
|
durationMs: Date.now() - start,
|
|
280
476
|
...!ready ? {
|
|
281
477
|
error: "Readiness check returned false"
|
|
282
478
|
} : {}
|
|
283
479
|
});
|
|
284
480
|
} catch (err) {
|
|
285
|
-
const message = err
|
|
481
|
+
const message = getErrorMessage(err);
|
|
286
482
|
results.push({
|
|
287
483
|
name: "custom-readiness",
|
|
288
484
|
ok: false,
|
|
@@ -317,13 +513,17 @@ var HealthzServer = class _HealthzServer {
|
|
|
317
513
|
const start = Date.now();
|
|
318
514
|
try {
|
|
319
515
|
const result = await this.executeCheck(check, this.config.checkTimeoutMs);
|
|
516
|
+
const ok = result === void 0 ? true : !!result;
|
|
320
517
|
return {
|
|
321
518
|
name,
|
|
322
|
-
ok
|
|
323
|
-
durationMs: Date.now() - start
|
|
519
|
+
ok,
|
|
520
|
+
durationMs: Date.now() - start,
|
|
521
|
+
...!ok ? {
|
|
522
|
+
error: "Check returned false"
|
|
523
|
+
} : {}
|
|
324
524
|
};
|
|
325
525
|
} catch (err) {
|
|
326
|
-
const message = err
|
|
526
|
+
const message = getErrorMessage(err);
|
|
327
527
|
return {
|
|
328
528
|
name,
|
|
329
529
|
ok: false,
|
|
@@ -334,6 +534,9 @@ var HealthzServer = class _HealthzServer {
|
|
|
334
534
|
}));
|
|
335
535
|
}
|
|
336
536
|
sendProbe(res, httpStatus, status, checks, isHead = false) {
|
|
537
|
+
if (this.shuttingDown) {
|
|
538
|
+
res.setHeader("Connection", "close");
|
|
539
|
+
}
|
|
337
540
|
const body = {
|
|
338
541
|
status,
|
|
339
542
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -345,6 +548,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
345
548
|
this.sendJson(res, httpStatus, body, isHead);
|
|
346
549
|
}
|
|
347
550
|
sendJson(res, status, body, isHead = false) {
|
|
551
|
+
if (res.headersSent) return;
|
|
348
552
|
const payload = JSON.stringify(body);
|
|
349
553
|
res.writeHead(status, {
|
|
350
554
|
"Content-Type": "application/json; charset=utf-8",
|
|
@@ -358,27 +562,37 @@ var HealthzServer = class _HealthzServer {
|
|
|
358
562
|
res.end(payload);
|
|
359
563
|
}
|
|
360
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
|
+
*/
|
|
361
572
|
async executeCheck(fn, ms) {
|
|
362
573
|
const controller = new AbortController();
|
|
363
574
|
let timer;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
try {
|
|
372
|
-
const checkPromise = Promise.resolve().then(() => fn(controller.signal));
|
|
373
|
-
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);
|
|
374
582
|
});
|
|
375
|
-
|
|
376
|
-
checkPromise
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
}
|
|
381
594
|
}
|
|
595
|
+
return await Promise.resolve().then(() => fn(controller.signal));
|
|
382
596
|
}
|
|
383
597
|
async initiateShutdown() {
|
|
384
598
|
if (this.shuttingDown) return;
|
|
@@ -394,8 +608,6 @@ var HealthzServer = class _HealthzServer {
|
|
|
394
608
|
process.off("SIGINT", this.onSigint);
|
|
395
609
|
this.ready = false;
|
|
396
610
|
this.startupComplete = false;
|
|
397
|
-
this.running = false;
|
|
398
|
-
this.shuttingDown = false;
|
|
399
611
|
}
|
|
400
612
|
};
|
|
401
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"
|