import { sql } from "@/db"; export type JobKind = "process_pdf" | "embed_paper" | "relations" | "daily_stats" | "backup" | "ingest_scan" | "ingest_candidate"; export async function enqueue(kind: JobKind, payload: Record = {}, opts: { runAfter?: Date; dedupe?: boolean } = {}) { if (opts.dedupe) { const [dup] = await sql`SELECT id FROM jobs WHERE kind = ${kind} AND payload = ${sql.json(payload as never)} AND status IN ('queued', 'running') LIMIT 1`; if (dup) return Number(dup.id); } const [r] = await sql`INSERT INTO jobs (kind, payload, run_after) VALUES (${kind}, ${sql.json(payload as never)}, ${opts.runAfter ?? new Date()}) RETURNING id`; return Number(r.id); } export async function claimJob(): Promise<{ id: number; kind: JobKind; payload: Record; attempts: number } | null> { const rows = await sql`UPDATE jobs SET status = 'running', locked_at = now(), attempts = attempts + 1 WHERE id = (SELECT id FROM jobs WHERE (status = 'queued' AND run_after <= now()) OR (status = 'running' AND locked_at < now() - interval '30 minutes') ORDER BY run_after LIMIT 1 FOR UPDATE SKIP LOCKED) RETURNING id, kind, payload, attempts`; if (!rows.length) return null; const r = rows[0]; return { id: Number(r.id), kind: r.kind, payload: r.payload, attempts: r.attempts }; } export async function finishJob(id: number, ok: boolean, error?: string, retryable = true) { if (ok) { await sql`UPDATE jobs SET status = 'done', finished_at = now(), last_error = NULL WHERE id = ${id}`; return; } const [j] = await sql`SELECT attempts, max_attempts FROM jobs WHERE id = ${id}`; if (retryable && j && j.attempts < j.max_attempts) await sql`UPDATE jobs SET status = 'queued', run_after = now() + (${j.attempts} * interval '2 minutes'), last_error = ${error ?? null} WHERE id = ${id}`; else await sql`UPDATE jobs SET status = 'failed', finished_at = now(), last_error = ${error ?? null} WHERE id = ${id}`; }