File size: 2,591 Bytes
529090e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { prisma } from '../database/prisma.js';
import { neo4jAdapter } from '../adapters/Neo4jAdapter.js';

// BootstrapGate ensures critical infra is reachable before we start listening
export class BootstrapGate {
  constructor(
    private readonly options = {
      timeoutMs: 10000, // Increased timeout for slow container startup
      redisUrl: process.env.REDIS_URL || 'redis://localhost:6379',
    }
  ) {}

  async init(): Promise<void> {
    const results = await Promise.all([this.checkPostgres(), this.checkNeo4j(), this.checkRedis()]);

    const failing = results.filter(r => !r.ok);
    if (failing.length > 0) {
      failing.forEach(f => console.error(`❌ Bootstrap check failed: ${f.service} -> ${f.error}`));
      throw new Error('BootstrapGate failed');
    }

    console.log('✅ BootstrapGate: all critical services are reachable');
  }

  private async checkPostgres(): Promise<{ service: string; ok: boolean; error?: string }> {
    const op = async () => {
      // prisma uses configured connection (expected on port 5433 via env)
      await prisma.$queryRaw`SELECT 1`;
    };
    return this.runWithTimeout('postgres', op);
  }

  private async checkNeo4j(): Promise<{ service: string; ok: boolean; error?: string }> {
    const op = async () => {
      const health = await neo4jAdapter.healthCheck();
      if (!health.connected) throw new Error('neo4j not connected');
    };
    return this.runWithTimeout('neo4j', op);
  }

  private async checkRedis(): Promise<{ service: string; ok: boolean; error?: string }> {
    const op = async () => {
      // Dynamic import to avoid hard failure if dependency missing during tests
      const Redis = (await import('ioredis')).default;
      const client = new Redis(this.options.redisUrl, { maxRetriesPerRequest: 2 });
      try {
        const res = await client.ping();
        if (res !== 'PONG') throw new Error(`unexpected ping reply: ${res}`);
      } finally {
        client.disconnect();
      }
    };
    return this.runWithTimeout('redis', op);
  }

  private async runWithTimeout(
    service: string,
    fn: () => Promise<void>
  ): Promise<{ service: string; ok: boolean; error?: string }> {
    const timeout = new Promise<never>((_, reject) => {
      setTimeout(() => reject(new Error('timeout')), this.options.timeoutMs);
    });

    try {
      await Promise.race([fn(), timeout]);
      return { service, ok: true };
    } catch (error: any) {
      return { service, ok: false, error: error?.message || 'unknown error' };
    }
  }
}

export const bootstrapGate = new BootstrapGate();