import { readFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; async function main() { const base = process.env.PDFSCRIBE_API_BASE?.replace(/\/$/, ""); const key = process.env.PDFSCRIBE_API_KEY, file = process.env.PDFSCRIBE_FILE; if (!base || !key || !file) throw new Error("Set PDFSCRIBE_API_BASE, PDFSCRIBE_API_KEY, and PDFSCRIBE_FILE"); const timeout = Number(process.env.PDFSCRIBE_POLL_TIMEOUT_SECONDS || "300"); if (!Number.isInteger(timeout) || timeout < 1 || timeout > 3600) throw new Error("PDFSCRIBE_POLL_TIMEOUT_SECONDS must be 1 to 3600"); const bytes = await readFile(file); const api = async (path, options = {}, milliseconds = 30000) => { const response = await fetch(base + path, { ...options, signal: options.signal ?? AbortSignal.timeout(Math.max(1, Math.ceil(milliseconds))), headers: { Authorization: `Bearer ${key}`, ...options.headers } }); if (!response.ok) throw new Error(`API request failed with HTTP ${response.status}`); return response.json(); }; const upload = await api("/uploads", { method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": `quickstart-upload-${randomUUID()}` }, body: JSON.stringify({ content_type: process.env.PDFSCRIBE_CONTENT_TYPE || "application/pdf", size_bytes: bytes.byteLength }) }); const signed = await fetch(upload.upload.url, { method: upload.upload.method, signal: AbortSignal.timeout(30000), headers: Object.fromEntries(Object.entries(upload.upload.headers).map(([k, v]) => [k, v.join(",")])), body: bytes }); if (!signed.ok) throw new Error(`Signed upload failed with HTTP ${signed.status}`); await signed.arrayBuffer(); await api(`/uploads/${upload.id}/complete`, { method: "POST" }); let job = await api("/extractions", { method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": `quickstart-${randomUUID()}` }, body: JSON.stringify({ upload_id: upload.id, mode: "ocr", pages: "all", output: { layout: "ordered", include: ["text", "tables"], table_format: "markdown", confidence: "block" } }) }); console.error(`Accepted extraction ID: ${job.id}`); const terminal = ["succeeded", "partially_succeeded", "failed", "cancelled", "expired"]; const deadline = performance.now() + timeout * 1000; while (!terminal.includes(job.status)) { const remaining = deadline - performance.now(); if (remaining <= 0) { console.error(`Polling timed out; resume checking the existing extraction ID: ${job.id}`); process.exitCode = 1; return; } const pollSignal = AbortSignal.timeout(Math.max(1, Math.ceil(Math.min(30000, remaining)))); try { job = await api(`/extractions/${job.id}`, { signal: pollSignal }); } catch (error) { // Reading a response body can report AbortError even when this signal's // timeout reason is TimeoutError. Do not relabel HTTP or network failures. if (remaining <= 30000 && pollSignal.aborted && (error === pollSignal.reason || error.name === "AbortError")) { console.error(`Polling timed out; resume checking the existing extraction ID: ${job.id}`); process.exitCode = 1; return; } throw error; } if (!terminal.includes(job.status)) await new Promise(resolve => setTimeout(resolve, Math.min(1000, Math.max(0, deadline - performance.now())))); } if (!["succeeded", "partially_succeeded"].includes(job.status)) { console.error(`Extraction ended with status: ${job.status}`); process.exitCode = 1; return; } for (const path of [`/extractions/${job.id}/result?view=ordered&include=text,tables&table_format=markdown&confidence=block`, "/credits", "/usage?page=1"]) console.log(JSON.stringify(await api(path), null, 2)); } main().catch(() => { console.error("Quickstart request failed; check connectivity and the existing extraction before retrying."); process.exitCode = 1; });