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