/** * GET /api/print-agent/jobs/next?wait=25&max=50 (Bearer agent token) * * Long-poll: returns immediately when queued jobs (or commands) exist, otherwise re-reads the * SQLite spool every 200 ms for up to `wait` seconds. The spool file is shared by all PM2 * workers, so a job inserted by another worker is picked up within ~200 ms. * * Jobs are claimed atomically (status → sent). Files ≤ 300 KB travel inline as base64 * (one HTTP exchange per label); larger ones are fetched from jobs/:id/file. * If the agent disconnects while we hold claimed jobs, they go straight back to 'queued'. */ import { readFile } from 'node:fs/promises' import { requireAgent } from '../../../utils/printAgentAuth' import { claimJobs, claimCommands, retryJob, touchAgent, requeueSentJobsForAgent, listUnconfirmedSpooledJobs, type PrintJobRow } from '../../../utils/printSpoolDb' const INLINE_LIMIT = 300 * 1024 const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)) const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(max, v)) export default defineEventHandler(async (event) => { const agent = requireAgent(event) const q = getQuery(event) const wait = clamp(Number(q.wait) || 0, 0, 25) const max = clamp(Number(q.max) || 50, 1, 100) touchAgent(agent.id) // `fresh=1` = the agent's first poll after a (re)start: whatever it had claimed before is gone // from its memory, so give those jobs back immediately instead of waiting for the 3-min stale // re-queue. Jobs that already reached lp are protected by the agent's ledger (idempotent ack). let reconcile: { id: number; cupsJobId: string | null; queue: string }[] | undefined if (String(q.fresh || '') === '1') { const n = requeueSentJobsForAgent(agent.id) if (n) console.log(`[print-agent] agent #${agent.id} "${agent.name}" restarted — re-queued ${n} claimed job(s)`) // jobs CUPS accepted whose completion we never heard about: the agent checks them against its CUPS history reconcile = listUnconfirmedSpooledJobs(agent.id).map(j => ({ id: j.id, cupsJobId: j.cups_job_id, queue: j.queue })) if (reconcile.length) console.log(`[print-agent] agent #${agent.id} gets ${reconcile.length} unconfirmed spooled job(s) to reconcile`) } let closed = false event.node.req.on('close', () => { closed = true }) const deadline = Date.now() + wait * 1000 let jobs: PrintJobRow[] = [] let commands: any[] = [] for (;;) { jobs = claimJobs(agent.id, max) commands = claimCommands(agent.id) if (jobs.length || commands.length || closed || Date.now() >= deadline) break await sleep(200) } if (closed) { // the agent is gone — hand the claimed jobs back immediately for (const j of jobs) retryJob(j.id) return { jobs: [], commands, reconcile } } const out: any[] = [] for (const j of jobs) { let content: string | null = null if (j.file_size > 0 && j.file_size <= INLINE_LIMIT) { try { content = (await readFile(j.file_path)).toString('base64') } catch { content = null } } out.push({ id: j.id, batchId: j.batch_id, queue: j.queue, options: j.options, fileName: j.file_name, fileSize: j.file_size, source: j.source, createdAt: j.created_at, attempt: j.attempts, content, fileUrl: content ? null : `/api/print-agent/jobs/${j.id}/file` }) } if (out.length) console.log(`[print-agent] agent #${agent.id} "${agent.name}" picked up ${out.length} job(s)`) return { jobs: out, commands, reconcile, serverTime: new Date().toISOString() } })