import { pathToFileURL } from "node:url"; import { PrismaClient } from "@prisma/client"; import { ModerationWorker } from "./moderation-worker.js"; import { LeaseReaper } from "./lease-reaper.processor.js"; type SignalSource = { once(signal: "SIGTERM" | "SIGINT", listener: () => void): unknown; removeListener(signal: "SIGTERM" | "SIGINT", listener: () => void): unknown; }; type RunWorkerOptions = { worker: { runOnce(): Promise }; connect: () => Promise; disconnect: () => Promise; once: boolean; sleep?: () => Promise; signals?: SignalSource; }; type Worker = { runOnce(): Promise }; export function createCombinedWorker( reaper: Worker, moderation: Worker, ): Worker { return { async runOnce() { const reaped = await reaper.runOnce(); const moderated = await moderation.runOnce(); return reaped || moderated; }, }; } export async function runWorker(options: RunWorkerOptions) { const signals = options.signals ?? process; const sleep = options.sleep ?? (() => new Promise((resolve) => setTimeout(resolve, 1000))); let stopping = false; const stop = () => { stopping = true; }; if (!options.once) { signals.once("SIGTERM", stop); signals.once("SIGINT", stop); } try { await options.connect(); do { const handled = await options.worker.runOnce(); if (options.once || stopping) break; if (!handled) await sleep(); } while (!stopping); } finally { if (!options.once) { signals.removeListener("SIGTERM", stop); signals.removeListener("SIGINT", stop); } await options.disconnect(); } } export async function main() { const prisma = new PrismaClient(); const moderation = new ModerationWorker(prisma); const reaper = new LeaseReaper(prisma); const worker = createCombinedWorker(reaper, moderation); await runWorker({ worker, connect: () => prisma.$connect(), disconnect: () => prisma.$disconnect(), once: process.argv.includes("--once"), }); } export function logTopLevelError( error: unknown, logger: Pick = console, ) { logger.error( JSON.stringify({ scope: "moderation-worker", errorClass: error instanceof Error && error.name ? error.name : "UnknownError", }), ); } type TopLevelErrorOptions = { logger?: Pick; processState?: { exitCode?: number }; }; export function handleTopLevelError( error: unknown, options: TopLevelErrorOptions = {}, ) { logTopLevelError(error, options.logger); (options.processState ?? process).exitCode = 1; } const isEntrypoint = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href; if (isEntrypoint) { main().catch(handleTopLevelError); }