Coves frontend - a photon fork
at main 110 lines 2.6 kB view raw
1// XRPC transport layer for ATProto lexicon calls. 2 3export class XrpcError extends Error { 4 constructor( 5 public status: number, 6 public errorName: string, 7 message: string, 8 ) { 9 super(message) 10 this.name = 'XrpcError' 11 } 12} 13 14interface XrpcClientOptions { 15 fetchFn: typeof fetch 16 baseUrl: string 17} 18 19export class XrpcClient { 20 readonly #fetchFn: typeof fetch 21 readonly #baseUrl: string 22 23 constructor(options: XrpcClientOptions) { 24 this.#fetchFn = options.fetchFn 25 this.#baseUrl = options.baseUrl 26 } 27 28 async query<P, R>(nsid: string, params?: P): Promise<R> { 29 const url = new URL(`/xrpc/${nsid}`, this.#baseUrl) 30 31 if (params) { 32 const searchParams = new URLSearchParams() 33 for (const [key, value] of Object.entries( 34 params as Record<string, unknown>, 35 )) { 36 if (value === undefined || value === null) continue 37 searchParams.set(key, String(value)) 38 } 39 url.search = searchParams.toString() 40 } 41 42 const res = await this.#fetchFn(url.toString()) 43 44 if (!res.ok) { 45 throw await this.#parseError(res) 46 } 47 48 try { 49 return (await res.json()) as R 50 } catch { 51 throw new XrpcError( 52 res.status, 53 'ParseError', 54 'Failed to parse response as JSON', 55 ) 56 } 57 } 58 59 async procedure<I, O>(nsid: string, input?: I): Promise<O> { 60 const url = new URL(`/xrpc/${nsid}`, this.#baseUrl) 61 62 const res = await this.#fetchFn(url.toString(), { 63 method: 'POST', 64 headers: { 'Content-Type': 'application/json' }, 65 body: input !== undefined ? JSON.stringify(input) : undefined, 66 }) 67 68 if (!res.ok) { 69 throw await this.#parseError(res) 70 } 71 72 if (res.status === 204) { 73 return undefined as O 74 } 75 76 const text = await res.text() 77 if (!text) { 78 return undefined as O 79 } 80 81 try { 82 return JSON.parse(text) as O 83 } catch { 84 throw new XrpcError( 85 res.status, 86 'ParseError', 87 'Failed to parse response as JSON', 88 ) 89 } 90 } 91 92 async #parseError(res: Response): Promise<XrpcError> { 93 try { 94 const body = (await res.json()) as Record<string, unknown> 95 const errorName = 96 typeof body.error === 'string' ? body.error : 'UnknownError' 97 const message = 98 typeof body.message === 'string' 99 ? body.message 100 : `XRPC request failed with status ${res.status}` 101 return new XrpcError(res.status, errorName, message) 102 } catch { 103 return new XrpcError( 104 res.status, 105 'UnknownError', 106 `XRPC request failed with status ${res.status}`, 107 ) 108 } 109 } 110}