Rl-Auto / apps /api /src /server.ts
Lazywords's picture
Deploy RL Auto Docker Space
c4ae742
Raw
History Blame Contribute Delete
4.86 kB
// apps/api/src/server.ts
// Fastify server entry. Hosts:
// - /api/health
// - /api/analysis/* (REST endpoints in routes/analysis.ts)
// - / (static legacy SPA from apps/web-legacy/, with SPA fallback)
import Fastify, { type FastifyInstance } from "fastify";
import fastifyStatic from "@fastify/static";
import fastifyMultipart from "@fastify/multipart";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { existsSync, mkdirSync } from "node:fs";
import { loadConfig } from "./config.js";
import { createLogger } from "./log.js";
import { createStore } from "./queue/store-factory.js";
import { registerHealthRoutes } from "./routes/health.js";
import { registerAnalysisRoutes } from "./routes/analysis.js";
import { registerPolishRoutes } from "./routes/polish.js";
import { QueueRunner } from "./queue/runner.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Inside the container the layout is:
// /app/apps/api/dist/server.js ← this file
// /app/apps/web-legacy/dist/index.html
// In dev (tsx) it's the source path with the same parent layout.
function resolveWebLegacyRoot(): string {
const webRoot = resolve(__dirname, "../../web-legacy/dist");
if (!existsSync(resolve(webRoot, "index.html"))) {
throw new Error("ε‰η«―ι™ζ€δΊ§η‰©δΈε­˜εœ¨οΌŒθ―·ε…ˆθΏθ‘Œ pnpm --filter @task-optimizer/web-legacy build ζˆ– pnpm build");
}
return webRoot;
}
async function buildServer(): Promise<{ app: FastifyInstance; runner: QueueRunner }> {
const config = loadConfig();
const logger = createLogger(config.logLevel);
// Ensure storage directories exist (volume-mounted in Docker)
for (const dir of [config.uploadDir, config.exportDir]) {
try {
mkdirSync(dir, { recursive: true });
} catch (err) {
logger.error({ err, dir }, "Failed to create storage directory");
throw err;
}
}
const store = await createStore(config);
const runner = new QueueRunner({ store, config, logger });
// Recover any jobs left in `running` state across restarts (mark queued items
// as queued is a no-op; the runner picks them back up on demand).
for (const job of await store.listResumableJobs()) {
logger.info({ jobId: job.id }, "Recovering resumable job from previous run");
await store.recoverInterruptedJob(job.id);
runner.scheduleJob(job.id);
}
const app = Fastify({
// Fastify 5 requires `loggerInstance` (not `logger`) when passing an
// already-constructed pino instance.
loggerInstance: logger,
bodyLimit: config.maxUploadMB * 1024 * 1024,
disableRequestLogging: false,
trustProxy: true,
});
await app.register(fastifyMultipart, {
limits: {
fileSize: config.maxUploadMB * 1024 * 1024,
files: config.maxBatchFiles,
},
});
// ── /api routes ─────────────────────────────────────────────────────────
await registerHealthRoutes(app);
await registerPolishRoutes(app, { config });
await registerAnalysisRoutes(app, { store, config, runner });
app.addHook("onClose", async () => {
await store.close();
});
// ── static SPA ──────────────────────────────────────────────────────────
const webRoot = resolveWebLegacyRoot();
await app.register(fastifyStatic, {
root: webRoot,
prefix: "/",
decorateReply: false,
});
// SPA fallback β€” anything that isn't /api/* and isn't a real file falls back
// to index.html so the legacy frontend can handle its own routes.
app.setNotFoundHandler((req, reply) => {
if (req.raw.url && req.raw.url.startsWith("/api/")) {
reply.code(404).send({ code: 404, msg: "Not Found" });
return;
}
reply.sendFile("index.html", webRoot);
});
return { app, runner };
}
async function main() {
const { app, runner } = await buildServer();
const config = loadConfig();
const shutdown = async (signal: string) => {
app.log.info({ signal }, "Shutdown signal received, closing server");
runner.requestShutdown();
try {
await app.close();
} catch (err) {
app.log.error({ err }, "Error closing Fastify");
}
process.exit(0);
};
process.once("SIGINT", () => void shutdown("SIGINT"));
process.once("SIGTERM", () => void shutdown("SIGTERM"));
try {
await app.listen({ port: config.port, host: "0.0.0.0" });
app.log.info({ port: config.port }, "Server listening");
} catch (err) {
app.log.error({ err }, "Server failed to start");
process.exit(1);
}
}
main().catch((err) => {
console.error("Fatal startup error:", err);
process.exit(1);
});