179 lines
4.1 KiB
JavaScript
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,
|
|
};
|