/** * Minimal Scrapfly render helper that exposes `browser_data.xhr_call` (the XHR requests/responses the * rendered page performed). The shared engine in @rareindex/connectors only returns the page content; * Goldin's lot data lives in the page's own search XHR, so this connector needs the browser data. * TODO(framework): add `FetchOptions.captureXhr` + `ExtractionResult.browserData` and drop this file. */ export interface XhrCall { url: string; method: string; requestBody: string | null; responseBody: string | null; status: number | null; } export interface ScrapflyRenderResult { ok: boolean; status: number | null; html: string | null; xhr: XhrCall[]; cost: number; durationMs: number; error: string | null; } export interface RenderOptions { apiKey: string; renderingWaitMs?: number; country?: string; timeoutMs?: number; } export async function scrapflyRender(url: string, o: RenderOptions): Promise { const started = Date.now(); const params = new URLSearchParams({ key: o.apiKey, url, asp: 'true', render_js: 'true', country: (o.country ?? 'us').toLowerCase(), retry: 'true', rendering_wait: String(Math.min(o.renderingWaitMs ?? 6000, 25_000)) }); const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), (o.timeoutMs ?? 170_000) + 5_000); try { const res = await fetch(`https://api.scrapfly.io/scrape?${params.toString()}`, { signal: ctrl.signal }); const text = await res.text(); let json: any; try { json = JSON.parse(text); } catch { return { ok: false, status: null, html: null, xhr: [], cost: 0, durationMs: Date.now() - started, error: `scrapfly HTTP ${res.status}: ${text.slice(0, 160)}` }; } const r = json.result ?? {}; const cost: number = json.context?.cost?.total ?? 0; if (!res.ok) return { ok: false, status: null, html: null, xhr: [], cost, durationMs: Date.now() - started, error: `scrapfly HTTP ${res.status}: ${json.message ?? r.error?.message ?? 'error'}` }; const xhrRaw: any[] = r.browser_data?.xhr_call ?? []; const xhr: XhrCall[] = xhrRaw.map((c) => ({ url: String(c.url ?? ''), method: String(c.method ?? 'GET'), requestBody: typeof c.body === 'string' ? c.body : c.body ? JSON.stringify(c.body) : null, responseBody: typeof c.response?.body === 'string' ? c.response.body : c.response?.body ? JSON.stringify(c.response.body) : null, status: c.response?.status ?? null, })); const status: number | null = r.status_code ?? null; const ok = r.success !== false && (status === null || status < 400); return { ok, status, html: r.content ?? null, xhr, cost, durationMs: Date.now() - started, error: ok ? null : (r.error?.message ?? `status ${status}`) }; } catch (err) { return { ok: false, status: null, html: null, xhr: [], cost: 0, durationMs: Date.now() - started, error: err instanceof Error ? err.message : String(err) }; } finally { clearTimeout(timer); } }