// retriever.mjs — Node 18+
export const API = process.env.RETRIEVER_API_URL;
export const KEY = process.env.RETRIEVER_API_KEY;
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export class RetrieverError extends Error {
constructor(status, code, message) {
super(`${status} ${code}: ${message}`);
Object.assign(this, { status, code });
}
}
export async function call(path, { method = "GET", body, headers = {}, retries = 5, timeoutMs = 60_000 } = {}) {
const canRetryFailure = method === "GET" || "Idempotency-Key" in headers;
for (let attempt = 0; ; attempt++) {
let res;
try {
res = await fetch(`${API}${path}`, {
method,
signal: AbortSignal.timeout(timeoutMs),
headers: {
Authorization: `Bearer ${KEY}`,
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
...headers,
},
body: body !== undefined ? JSON.stringify(body) : undefined,
});
} catch (networkError) {
if (!canRetryFailure || attempt >= retries) throw networkError;
await sleep(Math.min(30_000, 500 * 2 ** attempt));
continue;
}
if (res.ok) return res.json();
const payload = await res.json().catch(() => ({}));
const error = payload.error ?? {};
const retryable = res.status === 429 || (res.status >= 500 && canRetryFailure);
if (!retryable || attempt >= retries) {
throw new RetrieverError(res.status, error.code ?? `http_${res.status}`, error.message ?? res.statusText);
}
const retryAfter = Number(res.headers.get("retry-after"));
await sleep(retryAfter > 0 ? retryAfter * 1000 : Math.min(30_000, 500 * 2 ** attempt));
}
}
export async function download(path) {
const res = await fetch(`${API}${path}`, { headers: { Authorization: `Bearer ${KEY}` } });
if (!res.ok) throw new RetrieverError(res.status, `http_${res.status}`, "download failed");
return Buffer.from(await res.arrayBuffer());
}
export async function waitRun(id, { timeoutMs = 60 * 60_000 } = {}) {
const deadline = Date.now() + timeoutMs;
for (;;) {
const run = await call(`/v1/runs/${id}`);
if (!["queued", "running", "recovering"].includes(run.status)) return run;
if (Date.now() > deadline) throw new Error(`Run ${id} toujours actif après ${timeoutMs} ms`);
await sleep(15_000);
}
}
export async function* paginateRows(rowsPath) {
let cursor = null;
do {
const qs = new URLSearchParams({ limit: "1000", ...(cursor ? { cursor } : {}) });
const page = await call(`${rowsPath}?${qs}`);
yield* page.items;
cursor = page.next_cursor;
} while (cursor);
}