import { readFile, writeFile } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; import { createHash } from 'node:crypto'; import { VelrimClient, type PollOptions } from '@velrim/sdk'; // Raw JSON Schema preserves partial data; validate before a downstream write. export const schema = { type: 'object', properties: { invoice_number: { type: 'string' }, total: { type: 'number' } }, required: ['invoice_number', 'total'], additionalProperties: false, }; export async function submitJob( client: VelrimClient, bytes: Uint8Array, idempotencyKey: string, save: (receipt: { job_id: string; request_id: string }) => Promise, ) { if (!idempotencyKey || idempotencyKey.length > 255) throw new Error('Supply a stable request key (1–255 characters).'); const job = await client.jobs.create(schema, { bytes }, { idempotencyKey }); // Persist before polling. A rejected save must reach the caller. await save({ job_id: job.job_id, request_id: job.request_id }); return job; } export function resumeJob(client: VelrimClient, jobId: string, options: PollOptions = {}) { return client.jobs.poll(jobId, schema, { intervalMs: 1500, timeoutMs: 60_000, ...options }); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const [mode, receiptPath, pdfPath, key] = process.argv.slice(2); if ( !receiptPath || (mode !== 'submit' && mode !== 'resume') || (mode === 'submit' && (!pdfPath || !key)) ) { throw new Error( 'Usage: npx tsx async-extraction.ts submit receipt.json invoice.pdf stable-key | resume receipt.json', ); } const client = new VelrimClient(); if (mode === 'submit') { const bytes = await readFile(pdfPath); const pending = { idempotency_key: key, created_at: new Date().toISOString(), document_sha256: createHash('sha256').update(bytes).digest('hex'), schema, }; // Exclusive creation prevents accidental reuse of an existing receipt file. await writeFile(receiptPath, JSON.stringify(pending, null, 2), { encoding: 'utf8', flag: 'wx', }); await submitJob(client, bytes, key, async (job) => { await writeFile(receiptPath, JSON.stringify({ ...pending, ...job }, null, 2), 'utf8'); }); console.log({ next: 'resume', receipt: receiptPath }); } else { const receipt: unknown = JSON.parse(await readFile(receiptPath, 'utf8')); if ( typeof receipt !== 'object' || receipt === null || !('job_id' in receipt) || typeof receipt.job_id !== 'string' || !receipt.job_id ) { throw new Error( 'Pending receipt: reconcile submission using the original key and exact body; do not create a new key.', ); } const result = await resumeJob(client, receipt.job_id); await writeFile( `${receiptPath}.result.json`, JSON.stringify({ data: result.data, fields: result.fields, meta: result.meta }, null, 2), { encoding: 'utf8', flag: 'wx' }, ); console.log({ request_id: result.meta.request_id, billed_pages: result.meta.billed_pages }); } }