arena/server/index.js

133 lines
3.5 KiB
JavaScript

import fastifyStatic from "@fastify/static";
import Fastify from "fastify";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { getConfig } from "./config.js";
import { closeMongoConnection, getMongoClient, hasMongoConfig } from "./db.js";
import { deathStatsRoutes } from "./deathStats.js";
import { visitorRoutes } from "./visitors.js";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const distPath = path.join(root, "dist");
const isProduction = process.env.NODE_ENV === "production" || process.argv.includes("--production");
const appConfig = getConfig();
const port = appConfig.SERVER_PORT;
const host = appConfig.SERVER_HOST;
let viteDevServer;
const app = Fastify({
bodyLimit: 16 * 1024,
});
app.addContentTypeParser("*", { parseAs: "string" }, (request, body, done) => {
done(null, body);
});
app.get("/api/health", async () => {
return {
ok: true,
dbConfigured: hasMongoConfig(),
};
});
if (!isProduction) {
const { createServer: createViteServer } = await import("vite");
viteDevServer = await createViteServer({
root,
server: {
middlewareMode: true,
hmr: {
server: app.server,
},
},
appType: "spa",
});
}
await app.register(visitorRoutes, { prefix: "/api/visitors" });
await app.register(deathStatsRoutes, { prefix: "/api/death-stats" });
if (isProduction) {
await app.register(fastifyStatic, {
root: distPath,
prefix: "/",
});
app.setNotFoundHandler((request, reply) => {
const acceptsHtml = String(request.headers.accept || "").includes("text/html");
if (request.method !== "GET" || request.url.startsWith("/api/") || !acceptsHtml) {
reply.code(404).send({ error: "not_found" });
return;
}
reply.sendFile("index.html");
});
} else {
app.setNotFoundHandler((request, reply) => {
if (request.url.startsWith("/api/")) {
reply.code(404).send({ error: "not_found" });
return;
}
reply.hijack();
viteDevServer.middlewares(request.raw, reply.raw, (error) => {
if (error) {
viteDevServer.ssrFixStacktrace(error);
console.error(error);
if (!reply.raw.headersSent) {
reply.raw.statusCode = 500;
reply.raw.end(error.stack);
}
return;
}
if (!reply.raw.headersSent && !reply.raw.writableEnded) {
reply.raw.statusCode = 404;
reply.raw.end("Not found");
}
});
});
}
app.setErrorHandler((error, request, reply) => {
const isMissingMongoConfig = error.message.includes("MongoDB configuration");
const status = isMissingMongoConfig ? 503 : 500;
console.error(error);
reply.code(status).send({
error: isMissingMongoConfig ? "mongodb_not_configured" : "internal_server_error",
message: isProduction ? "Arena tracking is unavailable." : error.message,
});
});
await app.listen({ port, host });
console.log(`Arena Picker listening on http://localhost:${port}`);
if (hasMongoConfig()) {
getMongoClient()
.then(() => {
console.log("MongoDB connection pool is ready.");
})
.catch((error) => {
console.error("MongoDB connection failed. Visitor API will retry on request.", error);
});
}
["SIGINT", "SIGTERM"].forEach((signal) => {
process.on(signal, () => {
shutdown(signal);
});
});
function shutdown(signal) {
console.log(`${signal} received. Closing server.`);
app.close()
.then(closeMongoConnection)
.finally(() => {
process.exit(0);
});
}