import { PgBoss } from 'pg-boss'; /** * Job names shared with workers/main.ts. Queues use the `singleton` policy so a connector (or the * counters/rank maintenance) never runs twice concurrently — the `singletonKey` is the connector id. */ export const JOBS = { connectorRun: 'connector.run', counters: 'maintenance.counters', rank: 'maintenance.rank', intel: 'maintenance.intel', healthProbe: 'health.probe', } as const; export type JobName = (typeof JOBS)[keyof typeof JOBS]; export const PGBOSS_SCHEMA = 'pgboss'; let boss: PgBoss | null = null; let starting: Promise | null = null; /** Lazy pg-boss client for enqueueing only (no supervision, no cron — the worker owns those). */ export async function getBoss(): Promise { if (boss) return boss; if (!starting) { starting = (async () => { const b = new PgBoss({ connectionString: process.env.DATABASE_URL ?? 'postgres://localhost:5432/cancerindex', schema: PGBOSS_SCHEMA, application_name: 'cancerindex-api', supervise: false, schedule: false, max: 2, }); b.on('error', () => { /* logged by caller on send failure */ }); await b.start(); boss = b; return b; })(); } return starting; } /** * Queue policy `stately`: at most one job per singletonKey in created/retry/active state, i.e. a * connector is never queued twice nor run concurrently (`send` returns null when deduplicated). * The worker owns queue creation; this only covers the case where the API enqueues first. */ export const QUEUE_POLICY = 'stately' as const; export async function ensureQueue(b: PgBoss, name: JobName): Promise { const existing = await b.getQueue(name); if (existing) return; // pg-boss refuses `undefined` option values — pass only defined keys. await b.createQueue(name, { policy: QUEUE_POLICY, retryLimit: 0, expireInSeconds: 3 * 3600 }); } export async function enqueue(name: JobName, data: Record, singletonKey: string): Promise { const b = await getBoss(); await ensureQueue(b, name); return b.send(name, data, { singletonKey }); } export async function closeBoss(): Promise { if (boss) { await boss.stop({ graceful: false, close: true, timeout: 5000 }); boss = null; starting = null; } }