Build Phase 1 application foundation
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user