{"version":3,"file":"preflight.mjs","names":[],"sources":["../../src/infra/preflight.ts"],"sourcesContent":["import { createConnection } from \"node:net\";\nimport { Data, Effect } from \"effect\";\n\nclass PreflightConnectionError extends Data.TaggedError(\"PreflightConnectionError\")<{\n  readonly cause: unknown;\n}> {\n  override get message() {\n    return `Postgres connection failed: ${\n      this.cause instanceof Error ? this.cause.message : String(this.cause)\n    }`;\n  }\n}\n\nconst scheduleTimeout = (ms: number, fn: () => void): ReturnType<typeof setTimeout> =>\n  setTimeout(fn, ms);\n\nexport interface PreflightTarget {\n  secret: string;\n  host: string;\n  port: number;\n  kind: \"postgres\" | \"redis\";\n  url: string;\n}\n\nexport interface PreflightFailure {\n  secret: string;\n  host: string;\n  port: number;\n  error: string;\n  /** True when the target port accepted a TCP connection (so docker compose cannot fix this failure). */\n  tcpReachable: boolean;\n}\n\nfunction parseLocalUrl(url: string): { host: string; port: number } | null {\n  try {\n    const parsed = new URL(url);\n    const host = parsed.hostname;\n    if (host !== \"localhost\" && host !== \"127.0.0.1\" && host !== \"::1\") return null;\n    const port = Number.parseInt(parsed.port, 10);\n    if (Number.isNaN(port)) {\n      const fallback = parsed.protocol === \"redis:\" ? 6379 : 5432;\n      return { host, port: fallback };\n    }\n    return { host, port };\n  } catch {\n    return null;\n  }\n}\n\nfunction checkTcpReachable(host: string, port: number, timeoutMs = 2000): Effect.Effect<boolean> {\n  return Effect.callback<boolean>((resume) => {\n    const socket = createConnection({ host, port });\n    const timer = scheduleTimeout(timeoutMs, () => {\n      socket.destroy();\n      resume(Effect.succeed(false));\n    });\n\n    socket.once(\"connect\", () => {\n      clearTimeout(timer);\n      socket.destroy();\n      resume(Effect.succeed(true));\n    });\n\n    socket.once(\"error\", () => {\n      clearTimeout(timer);\n      socket.destroy();\n      resume(Effect.succeed(false));\n    });\n  });\n}\n\nfunction checkPgConnection(url: string): Effect.Effect<boolean> {\n  return Effect.gen(function* () {\n    const reachable = yield* checkTcpReachable(\n      new URL(url).hostname,\n      Number(new URL(url).port) || 5432,\n      4000,\n    );\n    if (!reachable) return false;\n\n    return yield* Effect.tryPromise({\n      try: async () => {\n        const { Pool } = await import(\"pg\");\n        const pool = new Pool({ connectionString: url, connectionTimeoutMillis: 3000 });\n        try {\n          const client = await pool.connect();\n          try {\n            await client.query(\"SELECT 1\");\n            return true;\n          } finally {\n            client.release();\n          }\n        } finally {\n          await pool.end().catch(() => {});\n        }\n      },\n      catch: (cause) => new PreflightConnectionError({ cause }),\n    }).pipe(Effect.orElseSucceed(() => false));\n  });\n}\n\nfunction preflightTargetsFromEnv(env: Record<string, string>): PreflightTarget[] {\n  const targets: PreflightTarget[] = [];\n  for (const [secret, value] of Object.entries(env)) {\n    if (!value) continue;\n    if (secret.endsWith(\"_DATABASE_URL\")) {\n      const parsed = parseLocalUrl(value);\n      if (parsed) {\n        targets.push({\n          secret,\n          host: parsed.host,\n          port: parsed.port,\n          kind: \"postgres\",\n          url: value,\n        });\n      }\n    } else if (secret.endsWith(\"_REDIS_URL\")) {\n      const parsed = parseLocalUrl(value);\n      if (parsed) {\n        targets.push({ secret, host: parsed.host, port: parsed.port, kind: \"redis\", url: value });\n      }\n    }\n  }\n  return targets;\n}\n\nexport function preflightLocalInfra(\n  env: Record<string, string>,\n  overrides?: Record<string, string>,\n): Effect.Effect<PreflightFailure[], never> {\n  const merged = overrides ? { ...env, ...overrides } : env;\n  const targets = preflightTargetsFromEnv(merged);\n  if (targets.length === 0) return Effect.succeed([]);\n\n  return Effect.forEach(\n    targets,\n    (target) =>\n      Effect.gen(function* () {\n        if (target.kind === \"postgres\") {\n          const ok = yield* checkPgConnection(target.url);\n          if (ok) return null;\n          const tcpOk = yield* checkTcpReachable(target.host, target.port);\n          const appDb =\n            target.secret === \"API_DATABASE_URL\" || target.secret === \"AUTH_DATABASE_URL\";\n          const pluginContext = appDb\n            ? \" Run `docker compose up -d --wait` to start local Postgres.\"\n            : ` The plugin for ${target.secret} runs inside the local host process, so this DB must be reachable. Run \\`docker compose up -d --wait\\` to start local Postgres.`;\n          if (tcpOk) {\n            return {\n              secret: target.secret,\n              host: target.host,\n              port: target.port,\n              error: `${target.secret} at ${target.host}:${target.port} is reachable but Postgres connection failed. Check credentials and database name.${pluginContext}`,\n              tcpReachable: true,\n            } satisfies PreflightFailure;\n          }\n          return {\n            secret: target.secret,\n            host: target.host,\n            port: target.port,\n            error: `${target.secret} points to ${target.host}:${target.port} but nothing is listening.${pluginContext}`,\n            tcpReachable: false,\n          } satisfies PreflightFailure;\n        }\n\n        const reachable = yield* checkTcpReachable(target.host, target.port);\n        if (reachable) return null;\n        return {\n          secret: target.secret,\n          host: target.host,\n          port: target.port,\n          error: `${target.secret} points to ${target.host}:${target.port} but nothing is listening`,\n          tcpReachable: false,\n        } satisfies PreflightFailure;\n      }),\n    { concurrency: \"unbounded\" },\n  ).pipe(Effect.map((results) => results.filter((r): r is PreflightFailure => r !== null)));\n}\n"],"mappings":"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"}