const http = require("node:http"); const { Server: SocketServer } = require("socket.io"); const { createApp } = require("./app"); const { openDatabase } = require("./database"); const { runMigrations } = require("./database/migrate"); const { createImageProcessor } = require("./image-processing"); const { createStorageService } = require("./storage"); 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, imageProcessor: suppliedImageProcessor, logger = console, storage: suppliedStorage, }) { const database = openDatabase(config.database); let migrations; try { migrations = runMigrations(database); } catch (error) { database.close(); throw error; } const lifecycle = { ready: false, stopping: false, }; let storage; let imageProcessor; try { storage = suppliedStorage || createStorageService(config.storage, { logger }); imageProcessor = suppliedImageProcessor || createImageProcessor({ maxInputPixels: config.processing.maxInputPixels }); } catch (error) { database.close(); throw error; } let app; let io = null; try { app = createApp({ config, database, imageProcessor, lifecycle, logger, migrations, onPlaylistsChanged(event) { io?.emit("playlists:changed", event); }, storage, }); } catch (error) { storage.stop(); database.close(); throw error; } const httpServer = http.createServer(app); 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; Promise.resolve() .then(() => storage.start()) .catch(() => { logger.warn?.("Object storage health checks could not be started"); }); return listeningAddress; }) .catch((error) => { storage.stop(); 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); storage.stop(); 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, storage, imageProcessor, get address() { return address; }, start, stop, }; } module.exports = { createRuntime, };