const http = require("node:http"); const { Server: SocketServer } = require("socket.io"); const { createApp } = require("./app"); const { openDatabase } = require("./database"); const { runMigrations } = require("./database/migrate"); function listen(httpServer, port, host) { return new Promise((resolve, reject) => { function onError(error) { httpServer.off("listening", onListening); reject(error); } function onListening() { httpServer.off("error", onError); resolve(httpServer.address()); } httpServer.once("error", onError); httpServer.once("listening", onListening); httpServer.listen(port, host); }); } function createRuntime({ config, logger = console }) { const database = openDatabase(config.database); let migrations; try { migrations = runMigrations(database); } catch (error) { database.close(); throw error; } const lifecycle = { ready: false, stopping: false, }; const app = createApp({ config, database, lifecycle, migrations }); const httpServer = http.createServer(app); const io = new SocketServer(httpServer, { serveClient: false, }); io.on("connection", (socket) => { logger.info?.(`Socket.IO client connected: ${socket.id}`); socket.on("disconnect", () => { logger.info?.(`Socket.IO client disconnected: ${socket.id}`); }); }); let address = null; let startPromise = null; let stopPromise = null; async function start() { if (stopPromise) { throw new Error("Cannot start a runtime after shutdown has begun"); } if (startPromise) { return startPromise; } startPromise = listen(httpServer, config.port, config.host) .then((listeningAddress) => { address = listeningAddress; lifecycle.ready = true; return listeningAddress; }) .catch((error) => { database.close(); throw error; }); return startPromise; } async function stop(reason = "shutdown") { if (stopPromise) { return stopPromise; } lifecycle.ready = false; lifecycle.stopping = true; stopPromise = new Promise((resolve) => { let settled = false; const finish = () => { if (settled) return; settled = true; clearTimeout(timeout); if (database.open) { database.close(); } resolve(); }; const timeout = setTimeout(() => { logger.warn?.( `Graceful shutdown timed out after ${config.shutdownTimeoutMs}ms`, ); httpServer.closeAllConnections?.(); finish(); }, config.shutdownTimeoutMs); timeout.unref?.(); if (!httpServer.listening) { io.close(); finish(); return; } logger.info?.(`Stopping Announcements API (${reason})`); io.close(finish); }); return stopPromise; } return { app, config, database, httpServer, io, lifecycle, migrations, get address() { return address; }, start, stop, }; } module.exports = { createRuntime, };