Repository Public
1import "../../scripts/_env";2import { j, sql } from "../lib/db";3import { mailConfigured } from "../lib/env";4import { expireStale } from "../lib/services/booking";5import { processOutbox } from "../lib/services/outbox";6import { syncDue } from "../lib/services/external";7import { purgeExpiredData } from "../lib/services/maintenance";89/**10 * Background worker: e-mail outbox, expiry of unanswered requests, external calendar refresh, retention.11 * Every task is idempotent and safe to run again after a crash or a restart.12 */13const TICK_MS = 10_000;14let stopping = false;15let lastSync = 0, lastPurge = 0;1617async function tick() {18 const detail: Record<string, unknown> = {};19 detail.expired = await expireStale();20 detail.outbox = await processOutbox();21 if (Date.now() - lastSync > 60_000) { lastSync = Date.now(); detail.sync = await syncDue(); }22 if (Date.now() - lastPurge > 6 * 3_600_000) { lastPurge = Date.now(); detail.purge = await purgeExpiredData(); }23 await sql`INSERT INTO worker_heartbeat (name, beat_at, detail) VALUES ('worker', now(), ${j(detail)})24 ON CONFLICT (name) DO UPDATE SET beat_at = now(), detail = EXCLUDED.detail`;25}2627async function main() {28 const mail = mailConfigured();29 console.log(`[worker] started — mail transport: ${mail.transport}${mail.ok ? "" : ` (NOT configured: ${mail.reason})`}`);30 while (!stopping) {31 try { await tick(); } catch (e) { console.error("[worker] tick failed:", e instanceof Error ? e.message : e); }32 await new Promise((r) => setTimeout(r, TICK_MS));33 }34 await sql.end({ timeout: 5 });35 console.log("[worker] stopped");36}37for (const s of ["SIGINT", "SIGTERM"] as const) process.on(s, () => { stopping = true; });38main().catch((e) => { console.error(e); process.exit(1); });39