deadhead-space-freight-tycoon/apps/server/src/app.ts

72 lines
2.7 KiB
TypeScript

import Fastify, { type FastifyInstance } from "fastify";
import { createAuth } from "./auth";
import { type Clock, createOffsetClock, systemClock } from "./clock";
import type { ServerConfig } from "./config";
import { createDatabase, type Database } from "./db/client";
import { registerErrorHandler } from "./http/errors";
import { requirePlayerId } from "./http/session";
import { appJobs } from "./jobs/definitions";
import { createPgBossQueue, type JobQueue } from "./jobs/queue";
import { registerBoardRoutes } from "./routes/boards";
import type { AppContext } from "./routes/context";
import { registerDispatchRoutes } from "./routes/dispatches";
import { registerFleetRoutes } from "./routes/fleet";
import { registerPlayerRoutes } from "./routes/player";
import { registerTestClockRoute } from "./routes/test-clock";
export interface AppDeps {
clock?: Clock;
/** An existing database (tests). Otherwise the app opens one and closes it on shutdown. */
database?: Database;
/** Background job queue. Defaults to pg-boss on the app's database. */
jobs?: JobQueue;
}
/** Builds the Fastify app without listening, so tests can start it on any port. */
export function buildApp(config: ServerConfig, deps: AppDeps = {}): FastifyInstance {
const database = deps.database ?? createDatabase(config.databaseUrl);
const { db } = database;
const auth = createAuth(db, { secret: config.authSecret, baseUrl: config.authUrl });
const clock = createOffsetClock(deps.clock ?? systemClock);
const jobs =
deps.jobs ??
createPgBossQueue(config.databaseUrl, {
workers: config.workersEnabled,
jobs: appJobs({ db, clock: clock.now }),
});
const ctx: AppContext = {
db,
auth,
clock: clock.now,
// pg-boss timers run on the real clock, not the (possibly offset) game clock.
toBaseTime: clock.toBase,
jobs,
options: {
requirePlayer: (request) => requirePlayerId(request, auth, db),
validateResponses: config.env !== "production",
},
};
const app = Fastify({
// Structured JSON logs (pino); silenced in tests unless LOG_LEVEL says otherwise.
logger: { level: config.logLevel },
});
// The queue starts with the app and stops before the database closes.
app.addHook("onReady", () => jobs.start());
app.addHook("onClose", async () => {
await jobs.stop();
if (!deps.database) await database.close();
});
registerErrorHandler(app);
// Under /api so it is reachable through the same-domain proxy (decision 0.5.20).
app.get("/api/health", async () => ({ status: "ok" }));
registerPlayerRoutes(app, ctx);
registerBoardRoutes(app, ctx);
registerDispatchRoutes(app, ctx);
registerFleetRoutes(app, ctx);
if (config.env !== "production") registerTestClockRoute(app, clock);
return app;
}