@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.
@@ -26,28 +26,42 @@
26
26
  * A health check function that can be synchronous or asynchronous.
27
27
  * Receives an AbortSignal that is triggered when `checkTimeoutMs` is exceeded.
28
28
  *
29
- * > **Cancellation Note**: Cancellation is cooperative. The check function must listen to
30
- * > `signal.aborted` or pass `signal` to underlying asynchronous APIs (e.g. database drivers, `fetch`).
31
- * > Synchronous CPU-bound loops or operations that ignore `signal` cannot be forcibly stopped by JavaScript.
29
+ * > **Cancellation & Timeout Semantics**:
30
+ * > When `checkTimeoutMs` expires, the HTTP probe response immediately fast-fails with 503
31
+ * > and aborts the supplied `AbortSignal`.
32
+ * >
33
+ * > However, runtime cancellation in JavaScript is **strictly cooperative**. The probe server cannot
34
+ * > preemptively interrupt executing JavaScript or force-cancel asynchronous work that does not listen
35
+ * > to the signal. If a check executes asynchronous tasks without passing `signal`
36
+ * > (e.g., `await db.query(...)` without `{ signal }`), that task will continue running in the background.
37
+ * > To prevent background resource leaks, always pass `signal` to underlying drivers/clients
38
+ * > (e.g. `fetch(url, { signal })`, database clients, HTTP clients) or check `signal?.aborted`.
32
39
  *
33
- * - Return `true` (or resolve to true) to signal healthy.
40
+ * - Return `true` (or resolve to true, or resolve void without error) to signal healthy.
34
41
  * - Return `false` (or resolve to false) to signal unhealthy.
35
42
  * - Throw an error (or reject) to signal unhealthy with an error message.
36
43
  */
37
- type HealthCheckFn = (signal?: AbortSignal) => boolean | Promise<boolean>;
44
+ type HealthCheckFn = (signal?: AbortSignal) => boolean | void | Promise<boolean | void>;
38
45
  /**
39
46
  * A readiness check function.
40
47
  * Receives an AbortSignal that is triggered when `checkTimeoutMs` is exceeded.
41
48
  *
42
- * > **Cancellation Note**: Cancellation is cooperative. The check function must listen to
43
- * > `signal.aborted` or pass `signal` to underlying asynchronous APIs (e.g. database drivers, `fetch`).
44
- * > Synchronous CPU-bound loops or operations that ignore `signal` cannot be forcibly stopped by JavaScript.
49
+ * > **Cancellation & Timeout Semantics**:
50
+ * > When `checkTimeoutMs` expires, the HTTP probe response immediately fast-fails with 503
51
+ * > and aborts the supplied `AbortSignal`.
52
+ * >
53
+ * > However, runtime cancellation in JavaScript is **strictly cooperative**. The probe server cannot
54
+ * > preemptively interrupt executing JavaScript or force-cancel asynchronous work that does not listen
55
+ * > to the signal. If a check executes asynchronous tasks without passing `signal`
56
+ * > (e.g., `await db.query(...)` without `{ signal }`), that task will continue running in the background.
57
+ * > To prevent background resource leaks, always pass `signal` to underlying drivers/clients
58
+ * > (e.g. `fetch(url, { signal })`, database clients, HTTP clients) or check `signal?.aborted`.
45
59
  *
46
- * - Return `true` (or resolve to true) to signal ready.
60
+ * - Return `true` (or resolve to true, or resolve void without error) to signal ready.
47
61
  * - Return `false` (or resolve to false) to signal not ready.
48
62
  * - Throw an error (or reject) to signal not ready with an error.
49
63
  */
50
- type ReadinessCheckFn = (signal?: AbortSignal) => boolean | Promise<boolean>;
64
+ type ReadinessCheckFn = (signal?: AbortSignal) => boolean | void | Promise<boolean | void>;
51
65
  /**
52
66
  * A named health check with an associated check function.
53
67
  */
@@ -56,10 +70,11 @@ interface NamedCheck {
56
70
  name: string;
57
71
  /**
58
72
  * Check function — return false or throw to indicate failure.
73
+ * Resolving void without throwing is considered healthy/ready.
59
74
  * Receives an AbortSignal that is triggered when the check times out.
60
- * Cancellation via the signal is cooperative.
75
+ * Cancellation via the signal is cooperative; pass `signal` to underlying operations.
61
76
  */
62
- check: (signal?: AbortSignal) => boolean | Promise<boolean>;
77
+ check: (signal?: AbortSignal) => boolean | void | Promise<boolean | void>;
63
78
  }
64
79
  /**
65
80
  * Result of a single named health check.
@@ -148,14 +163,27 @@ interface CatbeeHealthzServerConfig {
148
163
  /**
149
164
  * Custom liveness check function. Runs *in addition to* `checks`.
150
165
  * Return `false` or throw to indicate unhealthy.
166
+ * Can also be provided via `onLivenessCheck`.
151
167
  */
152
168
  onHealthCheck?: HealthCheckFn;
169
+ /**
170
+ * Symmetrical alias for `onHealthCheck` — custom liveness check function.
171
+ * Runs *in addition to* `checks` on `/healthz`.
172
+ * Return `false` or throw to indicate unhealthy.
173
+ */
174
+ onLivenessCheck?: HealthCheckFn;
153
175
  /**
154
176
  * Custom readiness check function. Runs *in addition to* `readinessChecks`.
155
177
  * Return `false` or throw to indicate not ready.
156
178
  */
157
179
  onReadinessCheck?: ReadinessCheckFn;
158
- /** Timeout (ms) per individual check before it's considered failed
180
+ /**
181
+ * Timeout (ms) per individual check before it's considered failed.
182
+ *
183
+ * When exceeded, the probe immediately fast-fails with status 503 and triggers
184
+ * `abort()` on the check's `AbortSignal`. Note that cancellation is cooperative;
185
+ * checks should forward `signal` to downstream clients (e.g. database drivers, fetch)
186
+ * to avoid orphaned background execution.
159
187
  * - **default**: `5000`
160
188
  * - **env**: `HEALTHZ_CHECK_TIMEOUT_MS`
161
189
  */
@@ -194,8 +222,9 @@ interface HealthzAddressInfo {
194
222
  * "omitted" (-> fall back to `checks`) is distinguishable from
195
223
  * "explicitly empty" (-> run nothing).
196
224
  */
197
- interface ResolvedHealthzConfig extends Required<Omit<CatbeeHealthzServerConfig, 'onHealthCheck' | 'onReadinessCheck' | 'readinessChecks'>> {
225
+ interface ResolvedHealthzConfig extends Required<Omit<CatbeeHealthzServerConfig, 'onHealthCheck' | 'onLivenessCheck' | 'onReadinessCheck' | 'readinessChecks'>> {
198
226
  onHealthCheck?: CatbeeHealthzServerConfig['onHealthCheck'];
227
+ onLivenessCheck?: CatbeeHealthzServerConfig['onLivenessCheck'];
199
228
  onReadinessCheck?: CatbeeHealthzServerConfig['onReadinessCheck'];
200
229
  readinessChecks?: CatbeeHealthzServerConfig['readinessChecks'];
201
230
  }
@@ -219,7 +248,7 @@ declare function resolveConfig(userConfig?: CatbeeHealthzServerConfig): Resolved
219
248
  * |-----------|-------------|------------------|-----------|
220
249
  * | Liveness | `/healthz` | `livenessProbe` | Runs configured checks; 200 = alive, 503 = unhealthy |
221
250
  * | Readiness | `/readyz` | `readinessProbe` | Checks readiness flag + readiness checks; 503 while not ready |
222
- * | Startup | `/startupz` | `startupProbe` | 200 once the server has successfully started listening; 503 before that |
251
+ * | Startup | `/startupz` | `startupProbe` | 200 once application startup completes (`markStartupComplete()`); 503 while booting |
223
252
  *
224
253
  * ### Kubernetes Probe Best Practices:
225
254
  * - **Liveness (`/healthz`)**: Keep these checks extremely lightweight (e.g. process is responsive,
@@ -229,7 +258,8 @@ declare function resolveConfig(userConfig?: CatbeeHealthzServerConfig): Resolved
229
258
  * - **Readiness (`/readyz`)**: Place external dependency checks (DB, Redis, downstream APIs) here via
230
259
  * `readinessChecks`. If a dependency fails, Kubernetes temporarily pulls the pod from service endpoints
231
260
  * without restarting the container, allowing it to recover gracefully.
232
- * - **Startup (`/startupz`)**: Verifies the health server process has started listening.
261
+ * - **Startup (`/startupz`)**: Verifies the application has completed its startup sequence
262
+ * (signaled via `markStartupComplete()`). Protects slow-starting applications from premature liveness kills.
233
263
  *
234
264
  * @example
235
265
  * ```ts
@@ -237,11 +267,9 @@ declare function resolveConfig(userConfig?: CatbeeHealthzServerConfig): Resolved
237
267
  *
238
268
  * const addr = await HealthzServer.start({
239
269
  * port: 8282,
240
- * // Keep liveness lightweight:
241
270
  * checks: [
242
271
  * { name: 'process', check: () => true },
243
272
  * ],
244
- * // Place dependency checks on readiness:
245
273
  * readinessChecks: [
246
274
  * { name: 'database', check: (signal) => db.ping({ signal }) },
247
275
  * { name: 'redis', check: (signal) => redis.ping({ signal }) },
@@ -249,6 +277,9 @@ declare function resolveConfig(userConfig?: CatbeeHealthzServerConfig): Resolved
249
277
  * shutdownDelayMs: 10_000,
250
278
  * });
251
279
  *
280
+ * // Signal application startup complete (switches /startupz to 200):
281
+ * HealthzServer.markStartupComplete();
282
+ *
252
283
  * // Signal readiness after all background services and migrations are ready:
253
284
  * HealthzServer.setReady(true);
254
285
  * ```
@@ -257,11 +288,18 @@ declare class HealthzServer {
257
288
  private readonly server;
258
289
  private readonly config;
259
290
  private readonly startedAt;
260
- /** Whether the health server has successfully started listening (for `/startupz`) */
261
- private started;
291
+ /** Running address info for the Healthz probe server */
292
+ private addressInfo;
293
+ /** Whether the health HTTP server is currently running and listening on its port */
294
+ private running;
295
+ /** Whether application startup has completed (for `/startupz`) */
296
+ private startupComplete;
262
297
  /** Whether the service is ready to receive traffic (for `/readyz`) */
263
298
  private ready;
264
299
  private shuttingDown;
300
+ private readonly livenessChecks;
301
+ private readonly readinessChecks;
302
+ private stoppingPromise?;
265
303
  private readonly onSigterm;
266
304
  private readonly onSigint;
267
305
  private constructor();
@@ -274,30 +312,97 @@ declare class HealthzServer {
274
312
  * Returns `null` if a server is already running in this process.
275
313
  */
276
314
  static start(opts?: CatbeeHealthzServerConfig): Promise<HealthzAddressInfo | null>;
277
- /** Whether the health-check server is currently running and started. */
278
- static isStarted(): boolean;
315
+ /** Whether the Healthz HTTP probe server is currently running and listening on its port. */
316
+ isRunning(): boolean;
317
+ /** Whether the Healthz HTTP probe server is currently running and listening on its port. */
318
+ static isRunning(): boolean;
319
+ /** Mark application startup as completed (switches `/startupz` to 200). */
320
+ markStartupComplete(): this;
321
+ /** Mark application startup as completed (switches `/startupz` to 200). */
322
+ static markStartupComplete(): void;
323
+ /** Set application startup completion status. */
324
+ setStartupComplete(complete: boolean): this;
325
+ /** Set application startup completion status. */
326
+ static setStartupComplete(complete: boolean): void;
327
+ /** Whether application startup has completed (for `/startupz`). */
328
+ isStartupComplete(): boolean;
329
+ /** Whether application startup has completed (for `/startupz`). */
330
+ static isStartupComplete(): boolean;
331
+ /** Mark the service as ready / not-ready for traffic. */
332
+ setReady(ready: boolean): this;
279
333
  /** Mark the service as ready / not-ready for traffic. */
280
334
  static setReady(ready: boolean): void;
281
335
  /** Whether the service is currently marked as ready. */
336
+ isReady(): boolean;
337
+ /** Whether the service is currently marked as ready. */
282
338
  static isReady(): boolean;
339
+ /** Get address info of this health server instance. */
340
+ getAddress(): HealthzAddressInfo | null;
341
+ /** Get address info of the active singleton health server. */
342
+ static getAddress(): HealthzAddressInfo | null;
343
+ /** Get port the health server is listening on. */
344
+ getPort(): number | undefined;
345
+ /** Get port the active singleton health server is listening on. */
346
+ static getPort(): number | undefined;
347
+ /** Get configured host. */
348
+ getHost(): string | undefined;
349
+ /** Get configured host of the active singleton health server. */
350
+ static getHost(): string | undefined;
351
+ /** Get full URL for this health server instance. */
352
+ getUrl(): string | undefined;
353
+ /** Get full URL for the active singleton health server. */
354
+ static getUrl(): string | undefined;
355
+ /** Gracefully stop this health server instance. */
356
+ stop(): Promise<void>;
283
357
  /** Gracefully stop the health-check server. */
284
358
  static stop(): Promise<void>;
285
359
  static getInstance(): HealthzServer | undefined;
286
360
  /**
287
361
  * Register a named check on this HealthzServer instance dynamically.
362
+ * If a check with the same name already exists in the target probe, it is updated in-place.
288
363
  *
289
364
  * @param check - The named check to register
290
365
  * @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
291
366
  * @returns This instance for chaining
292
367
  */
293
368
  registerCheck(check: NamedCheck, type?: 'liveness' | 'readiness' | 'both'): this;
369
+ /**
370
+ * Unregister a named check on this HealthzServer instance.
371
+ *
372
+ * @param name - The name of the check to remove
373
+ * @param type - Which probe to remove from ('liveness', 'readiness', or 'both')
374
+ * @returns This instance for chaining
375
+ */
376
+ unregisterCheck(name: string, type?: 'liveness' | 'readiness' | 'both'): this;
377
+ /**
378
+ * Get all registered checks on this instance.
379
+ */
380
+ getChecks(): {
381
+ liveness: NamedCheck[];
382
+ readiness: NamedCheck[];
383
+ };
294
384
  /**
295
385
  * Register a named check on the active singleton HealthzServer instance (if started).
386
+ * If a check with the same name already exists in the target probe, it is updated in-place.
296
387
  *
297
388
  * @param check - The named check to register
298
389
  * @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
299
390
  */
300
391
  static registerCheck(check: NamedCheck, type?: 'liveness' | 'readiness' | 'both'): void;
392
+ /**
393
+ * Unregister a named check on the active singleton HealthzServer instance (if started).
394
+ *
395
+ * @param name - Name of the check to remove
396
+ * @param type - Which probe to remove from ('liveness', 'readiness', or 'both')
397
+ */
398
+ static unregisterCheck(name: string, type?: 'liveness' | 'readiness' | 'both'): void;
399
+ /**
400
+ * Get all registered checks on the active singleton HealthzServer instance.
401
+ */
402
+ static getChecks(): {
403
+ liveness: NamedCheck[];
404
+ readiness: NamedCheck[];
405
+ };
301
406
  private handleRequest;
302
407
  /** `/healthz` — Liveness probe */
303
408
  private handleLiveness;
@@ -308,6 +413,13 @@ declare class HealthzServer {
308
413
  private runChecks;
309
414
  private sendProbe;
310
415
  private sendJson;
416
+ /**
417
+ * Executes a check function with timeout and cooperative cancellation.
418
+ *
419
+ * Note on cancellation: The AbortController aborts when `ms` expires, which signals cooperative
420
+ * consumers (e.g., fetch, pg, ioredis) to terminate their work. The attached .catch() on checkPromise
421
+ * prevents unhandled rejections if the check promise rejects after the timeout has already resolved.
422
+ */
311
423
  private executeCheck;
312
424
  private initiateShutdown;
313
425
  private cleanup;