# All files / src/preview health-checker.ts

6.52% Statements 3/46
0% Branches 0/19
0% Functions 0/8
7.69% Lines 3/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103

    8x

    8x

    8x

     /**
     * Health Checker
     *
     * Polls service healthcheck endpoints through the Caddy proxy port
     * to verify that preview services are ready.
     */

    import type { PreviewConfig, HealthResult } from './types.js';
    import { createModuleLogger } from '../logger/index.js';

    const log = createModuleLogger('preview:health');

    /** Default timeout for health checks (120 seconds) */
    const DEFAULT_TIMEOUT_MS = 120_000;

    /** Default polling interval (3 seconds) */
    const DEFAULT_INTERVAL_MS = 3_000;

    /**
     * Wait for all non-internal services to become healthy by polling their
     * healthcheck endpoints through the Caddy proxy.
     *
     * @param port - The host port where Caddy is listening
     * @param config - Preview config with service definitions
     * @param timeoutMs - Maximum time to wait (default: 120s)
     * @returns Per-service health results
     */
    export async function waitForHealthy(
      port: number,
      config: PreviewConfig,
      timeoutMs = DEFAULT_TIMEOUT_MS,
    ): Promise<HealthResult[]> {
      const services = Object.values(config.services).filter((svc) => {
        if (svc.internal) return false;
        if (!svc.healthcheck?.path) return false;
        return true;
      });

      if (services.length === 0) {
        log.info('no healthcheck endpoints configured, skipping health wait');
        return [];
      }

      const deadline = Date.now() + timeoutMs;
      const results = new Map<string, HealthResult>();

      // Initialize all as unhealthy
      for (const svc of services) {
        results.set(svc.name, { name: svc.name, healthy: false });
      }

      while (Date.now() < deadline) {
        const pending = services.filter((svc) => !results.get(svc.name)!.healthy);
        if (pending.length === 0) break;

        await Promise.all(
          pending.map(async (svc) => {
            const route = svc.route ?? '/';
            const healthPath = svc.healthcheck!.path;
            // Build URL through proxy route
            const basePath = route === '/' ? '' : route;
            const url = `http://127.0.0.1:${port}${basePath}${healthPath}`;

            try {
              const response = await fetch(url, {
                signal: AbortSignal.timeout(5000),
              });
              if (response.ok) {
                log.info({ service: svc.name, url }, 'service healthy');
                results.set(svc.name, { name: svc.name, healthy: true });
              }
            } catch {
              // Not ready yet
            }
          }),
        );

        const stillPending = services.filter((svc) => !results.get(svc.name)!.healthy);
        if (stillPending.length === 0) break;

        log.debug(
          { pending: stillPending.map((s) => s.name) },
          'waiting for services to become healthy',
        );
        await sleep(DEFAULT_INTERVAL_MS);
      }

      // Mark timed-out services
      for (const svc of services) {
        const result = results.get(svc.name)!;
        if (!result.healthy) {
          result.error = 'health check timed out';
          log.warn({ service: svc.name }, 'service health check timed out');
        }
      }

      return Array.from(results.values());
    }

    function sleep(ms: number): Promise<void> {
      return new Promise((resolve) => setTimeout(resolve, ms));
    }