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