// XRPC transport layer for ATProto lexicon calls. export class XrpcError extends Error { constructor( public status: number, public errorName: string, message: string, ) { super(message) this.name = 'XrpcError' } } interface XrpcClientOptions { fetchFn: typeof fetch baseUrl: string } export class XrpcClient { readonly #fetchFn: typeof fetch readonly #baseUrl: string constructor(options: XrpcClientOptions) { this.#fetchFn = options.fetchFn this.#baseUrl = options.baseUrl } async query(nsid: string, params?: P): Promise { const url = new URL(`/xrpc/${nsid}`, this.#baseUrl) if (params) { const searchParams = new URLSearchParams() for (const [key, value] of Object.entries( params as Record, )) { if (value === undefined || value === null) continue searchParams.set(key, String(value)) } url.search = searchParams.toString() } const res = await this.#fetchFn(url.toString()) if (!res.ok) { throw await this.#parseError(res) } try { return (await res.json()) as R } catch { throw new XrpcError( res.status, 'ParseError', 'Failed to parse response as JSON', ) } } async procedure(nsid: string, input?: I): Promise { const url = new URL(`/xrpc/${nsid}`, this.#baseUrl) const res = await this.#fetchFn(url.toString(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: input !== undefined ? JSON.stringify(input) : undefined, }) if (!res.ok) { throw await this.#parseError(res) } if (res.status === 204) { return undefined as O } const text = await res.text() if (!text) { return undefined as O } try { return JSON.parse(text) as O } catch { throw new XrpcError( res.status, 'ParseError', 'Failed to parse response as JSON', ) } } async #parseError(res: Response): Promise { try { const body = (await res.json()) as Record const errorName = typeof body.error === 'string' ? body.error : 'UnknownError' const message = typeof body.message === 'string' ? body.message : `XRPC request failed with status ${res.status}` return new XrpcError(res.status, errorName, message) } catch { return new XrpcError( res.status, 'UnknownError', `XRPC request failed with status ${res.status}`, ) } } }