import { createORPCClient } from '@orpc/client' import { OpenAPILink } from '@orpc/openapi-client/fetch' import type { ContractRouterClient } from '@orpc/contract' import { contract, type V1Contract } from './contract.js' export type MarketV1Client = ContractRouterClient const DEFAULT_BASE_URL = 'https://market.drawcall.ai' /** * Escalating read deadlines: the first bounds a stalled connection (observed * in bursts between Fly and Cloudflare) to a short detour, the later ones * leave room for an endpoint that is legitimately slow under load — a fixed * short timeout there turns load into a cancel-and-retry storm. Jittered * pauses between attempts keep concurrent clients from retrying in lockstep. */ const READ_TIMEOUT_STEPS_MS = [5_000, 10_000, 20_000] export interface MarketV1ClientOptions { baseUrl?: string fetch?: typeof globalThis.fetch authToken?: string } /** * GETs are idempotent, so a stalled connection is retried on a fresh one with a * bounded timeout — an unbounded hang in one read would otherwise hang whole * installs. Writes (uploads run to a gigabyte) keep the caller's fetch as-is. */ function resilientReads(baseFetch: typeof globalThis.fetch): typeof globalThis.fetch { return async (input, init) => { const method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase() if (method !== 'GET' && method !== 'HEAD') return baseFetch(input, init) let lastError: unknown for (const [attempt, timeoutMs] of READ_TIMEOUT_STEPS_MS.entries()) { const timeout = AbortSignal.timeout(timeoutMs) const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout try { return await baseFetch(input, { ...init, signal }) } catch (error) { lastError = error if (init?.signal?.aborted || attempt === READ_TIMEOUT_STEPS_MS.length - 1) throw error await new Promise((resolve) => setTimeout(resolve, 200 + Math.random() * 400)) } } throw lastError } } /** * A typed client for the v1 REST surface (`/api/v1`). Same call shapes as the legacy RPC client — * `client.asset.search({ refs })` — but speaking plain HTTP: reads are GETs on resource URLs, so * they are curl-able, shareable and cacheable. */ export function createClient(opts: MarketV1ClientOptions = {}): MarketV1Client { const link = new OpenAPILink(contract, { url: new URL('/api/v1', opts.baseUrl ?? DEFAULT_BASE_URL).href, fetch: resilientReads(opts.fetch ?? ((input, init) => globalThis.fetch(input, init))), headers: opts.authToken ? { authorization: `Bearer ${opts.authToken}` } : undefined, }) return createORPCClient(link) }