(env: NodeJS.ProcessEnv = process.env)
| 202 | return JSON.stringify({ status: 'ok', state: state.state }); |
| 203 | } |
| 204 | |
| 205 | export async function startController(env: NodeJS.ProcessEnv = process.env): Promise<void> { |
| 206 | // Mutable state ref — read by the health endpoint on every request, |
| 207 | // updated by bootstrap as it progresses through phases. |
| 208 | const controllerState: ControllerStateRef = { |
| 209 | current: { state: 'bootstrapping', phase: 'init' }, |
| 210 | }; |
| 211 | |
| 212 | // ── Phase 1: Start HTTP server ────────────────────────────────────── |
| 213 | // The server starts FIRST so /_kilo/health is always reachable, even |
| 214 | // during bootstrap. During bootstrap, a lightweight inline handler |
| 215 | // serves health probes directly. After bootstrap, the Hono app with |
| 216 | // full routes takes over. |
| 217 | let app: Hono | null = null; |
| 218 | |
| 219 | const server = http.createServer((req, res) => { |
| 220 | // Once bootstrap has completed and the Hono app is ready, delegate all requests. |
| 221 | if (app) { |
| 222 | void handleHttpRequest(app, req, res).catch(error => { |
| 223 | console.error('[controller] HTTP handler failed:', error); |
| 224 | res.statusCode = 500; |
| 225 | res.setHeader('content-type', 'application/json'); |
| 226 | res.end(JSON.stringify({ error: 'Internal Server Error' })); |
| 227 | }); |
| 228 | return; |
| 229 | } |
| 230 | |
| 231 | // Pre-bootstrap: serve health probes inline without Hono. |
| 232 | // Strip query string so e.g. /_kilo/health?ts=123 still matches. |
| 233 | const pathname = (req.url ?? '/').split('?')[0]; |
| 234 | if (pathname === '/_kilo/health' || pathname === '/health') { |
| 235 | res.statusCode = 200; |
| 236 | res.setHeader('content-type', 'application/json'); |
| 237 | if (pathname === '/health') { |
| 238 | // Bare /health for Fly probes — no state details |
| 239 | res.end(JSON.stringify({ status: 'ok' })); |
| 240 | } else { |
| 241 | res.end(healthJson(controllerState.current)); |
| 242 | } |
| 243 | return; |
| 244 | } |
| 245 | |
| 246 | // All other routes: 503 during bootstrap |
| 247 | res.statusCode = 503; |
| 248 | res.setHeader('content-type', 'application/json'); |
| 249 | res.end(JSON.stringify({ error: 'Service starting', state: controllerState.current.state })); |
| 250 | }); |
| 251 | |
| 252 | // Mutable ref for the WebSocket upgrade handler. Set once the Hono app and |
| 253 | // supervisor are ready. Registered on the server early so connections during |
| 254 | // bootstrap or degraded mode get a clean 503 rejection instead of hanging. |
| 255 | const wsUpgradeRef: { |
| 256 | handler: ((req: http.IncomingMessage, socket: Duplex, head: Buffer) => void) | null; |
| 257 | } = { handler: null }; |
| 258 | |
| 259 | server.on('upgrade', (req, socket, head) => { |
| 260 | if (wsUpgradeRef.handler) { |
| 261 | wsUpgradeRef.handler(req, socket, head); |
no test coverage detected