Files
Announcements/server/src/runtime.js
T

179 lines
4.1 KiB
JavaScript

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,
};