File size: 4,855 Bytes
c4ae742 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | // 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);
});
|