feat(server): boot: migrate, load plugins, worker, scheduler, daily backup, web placeholder
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6963e297b0
commit
02b7eed5ec
@@ -0,0 +1,18 @@
|
||||
import { enqueue } from "@sttracker/core/jobs";
|
||||
import type { Db } from "@sttracker/db/db";
|
||||
|
||||
/** The next 03:00 local time strictly after `now`. */
|
||||
export function nextBackupAt(now: Date = new Date()): Date {
|
||||
const d = new Date(now);
|
||||
d.setHours(3, 0, 0, 0);
|
||||
if (d.getTime() <= now.getTime()) d.setDate(d.getDate() + 1);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue the daily `backup` job for the next 03:00 unless one is already queued or running (lockKey "backup").
|
||||
* Returns the job id, or null when one is pending.
|
||||
*/
|
||||
export function ensureBackupScheduled(db: Db, now: Date = new Date()): Promise<string | null> {
|
||||
return enqueue(db, "backup", {}, { lockKey: "backup", runAt: nextBackupAt(now).toISOString() });
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { backupHandler } from "@sttracker/core/backup";
|
||||
import { syncHandler } from "@sttracker/core/connections";
|
||||
import { startScheduler, startWorker } from "@sttracker/core/jobs";
|
||||
import { loadPlugins } from "@sttracker/core/registry";
|
||||
import { createDb } from "@sttracker/db/db";
|
||||
import { migrateToLatest } from "@sttracker/db/migrate";
|
||||
import index from "../../web/index.html";
|
||||
import { createApp } from "./app.ts";
|
||||
import { ensureBackupScheduled } from "./boot.ts";
|
||||
import { loadConfig } from "./config.ts";
|
||||
|
||||
const BACKUP_CHECK_MS = 60 * 60 * 1000;
|
||||
|
||||
const config = loadConfig();
|
||||
await mkdir(join(config.dataDir, "plugins"), { recursive: true });
|
||||
const db = await createDb(config.databaseUrl);
|
||||
await migrateToLatest(db);
|
||||
const registry = await loadPlugins(config.pluginDirs);
|
||||
|
||||
const ensureBackup = () => ensureBackupScheduled(db).catch((e) => console.error("[backup] scheduling failed", e));
|
||||
const runBackup = backupHandler({ db, dataDir: config.dataDir });
|
||||
const worker = startWorker(db, {
|
||||
sync: syncHandler({ db, secret: config.secret, registry }),
|
||||
backup: async () => {
|
||||
try {
|
||||
await runBackup();
|
||||
} finally {
|
||||
// The finishing job still holds lockKey "backup" until the worker marks it done; queue the next one shortly after.
|
||||
setTimeout(() => void ensureBackup(), 5_000);
|
||||
}
|
||||
},
|
||||
});
|
||||
const scheduler = startScheduler(db);
|
||||
await ensureBackup();
|
||||
// Safety net: re-queue the daily backup if a run's follow-up was missed (e.g. it failed for good).
|
||||
const backupTimer = setInterval(() => void ensureBackup(), BACKUP_CHECK_MS);
|
||||
|
||||
const app = createApp({ db, registry, secret: config.secret, dataDir: config.dataDir, publicUrl: config.publicUrl });
|
||||
const server = Bun.serve({
|
||||
port: config.port,
|
||||
development: process.env.NODE_ENV !== "production",
|
||||
routes: {
|
||||
"/api/*": (req, srv) => app.fetch(req, srv),
|
||||
"/ingest/*": (req, srv) => app.fetch(req, srv),
|
||||
"/*": index,
|
||||
},
|
||||
fetch: (req, srv) => app.fetch(req, srv),
|
||||
});
|
||||
console.log(`STTracker listening on ${server.url} (${db.dialect}, ${registry.list().length} plugin(s))`);
|
||||
|
||||
let stopping = false;
|
||||
async function shutdown(signal: string): Promise<void> {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
console.log(`${signal}: shutting down`);
|
||||
clearInterval(backupTimer);
|
||||
scheduler.stop();
|
||||
await server.stop();
|
||||
await worker.stop();
|
||||
await db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => void shutdown("SIGINT"));
|
||||
Reference in New Issue
Block a user