@atproto/lex-client 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/agent.d.ts +9 -8
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js.map +1 -1
  5. package/dist/client.d.ts +32 -96
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +31 -31
  8. package/dist/client.js.map +1 -1
  9. package/dist/index.d.ts +0 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +0 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts +7 -7
  14. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts.map +1 -1
  15. package/dist/lexicons/com/atproto/repo/createRecord.defs.js +3 -5
  16. package/dist/lexicons/com/atproto/repo/createRecord.defs.js.map +1 -1
  17. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts +7 -7
  18. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts.map +1 -1
  19. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js +3 -5
  20. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js.map +1 -1
  21. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts +5 -6
  22. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts.map +1 -1
  23. package/dist/lexicons/com/atproto/repo/getRecord.defs.js +3 -5
  24. package/dist/lexicons/com/atproto/repo/getRecord.defs.js.map +1 -1
  25. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts +5 -6
  26. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts.map +1 -1
  27. package/dist/lexicons/com/atproto/repo/listRecords.defs.js +3 -5
  28. package/dist/lexicons/com/atproto/repo/listRecords.defs.js.map +1 -1
  29. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts +7 -7
  30. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts.map +1 -1
  31. package/dist/lexicons/com/atproto/repo/putRecord.defs.js +3 -5
  32. package/dist/lexicons/com/atproto/repo/putRecord.defs.js.map +1 -1
  33. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts +7 -7
  34. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts.map +1 -1
  35. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js +3 -5
  36. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js.map +1 -1
  37. package/dist/lexicons/com/atproto/sync/getBlob.d.ts +3 -0
  38. package/dist/lexicons/com/atproto/sync/getBlob.d.ts.map +1 -0
  39. package/dist/lexicons/com/atproto/sync/getBlob.defs.d.ts +25 -0
  40. package/dist/lexicons/com/atproto/sync/getBlob.defs.d.ts.map +1 -0
  41. package/dist/lexicons/com/atproto/sync/getBlob.defs.js +27 -0
  42. package/dist/lexicons/com/atproto/sync/getBlob.defs.js.map +1 -0
  43. package/dist/lexicons/com/atproto/sync/getBlob.js +10 -0
  44. package/dist/lexicons/com/atproto/sync/getBlob.js.map +1 -0
  45. package/dist/lexicons/com/atproto/sync.d.ts +2 -0
  46. package/dist/lexicons/com/atproto/sync.d.ts.map +1 -0
  47. package/dist/lexicons/com/atproto/sync.js +9 -0
  48. package/dist/lexicons/com/atproto/sync.js.map +1 -0
  49. package/dist/lexicons/com/atproto.d.ts +1 -0
  50. package/dist/lexicons/com/atproto.d.ts.map +1 -1
  51. package/dist/lexicons/com/atproto.js +2 -1
  52. package/dist/lexicons/com/atproto.js.map +1 -1
  53. package/dist/lexicons.d.ts +2 -0
  54. package/dist/lexicons.d.ts.map +1 -0
  55. package/dist/lexicons.js +6 -0
  56. package/dist/lexicons.js.map +1 -0
  57. package/dist/types.d.ts +18 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/types.js.map +1 -1
  60. package/dist/util.d.ts +14 -0
  61. package/dist/util.d.ts.map +1 -0
  62. package/dist/util.js +65 -0
  63. package/dist/util.js.map +1 -0
  64. package/dist/xrpc-error.d.ts +87 -0
  65. package/dist/xrpc-error.d.ts.map +1 -0
  66. package/dist/xrpc-error.js +127 -0
  67. package/dist/xrpc-error.js.map +1 -0
  68. package/dist/xrpc-response.d.ts +35 -0
  69. package/dist/xrpc-response.d.ts.map +1 -0
  70. package/dist/xrpc-response.js +140 -0
  71. package/dist/xrpc-response.js.map +1 -0
  72. package/dist/xrpc.d.ts +29 -32
  73. package/dist/xrpc.d.ts.map +1 -1
  74. package/dist/xrpc.js +119 -125
  75. package/dist/xrpc.js.map +1 -1
  76. package/package.json +6 -6
  77. package/src/agent.ts +12 -12
  78. package/src/client.ts +92 -77
  79. package/src/index.ts +0 -2
  80. package/src/lexicons/com/atproto/repo/createRecord.defs.ts +9 -8
  81. package/src/lexicons/com/atproto/repo/deleteRecord.defs.ts +9 -8
  82. package/src/lexicons/com/atproto/repo/getRecord.defs.ts +7 -7
  83. package/src/lexicons/com/atproto/repo/listRecords.defs.ts +7 -6
  84. package/src/lexicons/com/atproto/repo/putRecord.defs.ts +9 -8
  85. package/src/lexicons/com/atproto/repo/uploadBlob.defs.ts +9 -8
  86. package/src/lexicons/com/atproto/sync/getBlob.defs.ts +37 -0
  87. package/src/lexicons/com/atproto/sync/getBlob.ts +6 -0
  88. package/src/lexicons/com/atproto/sync.ts +5 -0
  89. package/src/lexicons/com/atproto.ts +1 -0
  90. package/src/lexicons.ts +1 -0
  91. package/src/types.ts +27 -0
  92. package/src/util.ts +84 -0
  93. package/src/xrpc-error.ts +195 -0
  94. package/src/xrpc-response.ts +213 -0
  95. package/src/xrpc.ts +209 -220
  96. package/dist/error.d.ts +0 -66
  97. package/dist/error.d.ts.map +0 -1
  98. package/dist/error.js +0 -100
  99. package/dist/error.js.map +0 -1
  100. package/dist/response.d.ts +0 -21
  101. package/dist/response.d.ts.map +0 -1
  102. package/dist/response.js +0 -31
  103. package/dist/response.js.map +0 -1
  104. package/src/error.ts +0 -145
  105. package/src/response.ts +0 -42
@@ -0,0 +1,37 @@
1
+ /*
2
+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
3
+ */
4
+
5
+ import { l } from '@atproto/lex-schema'
6
+
7
+ const $nsid = 'com.atproto.sync.getBlob'
8
+
9
+ export { $nsid }
10
+
11
+ /** Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS. */
12
+ const main =
13
+ /*#__PURE__*/
14
+ l.query(
15
+ $nsid,
16
+ /*#__PURE__*/ l.params({
17
+ did: /*#__PURE__*/ l.string({ format: 'did' }),
18
+ cid: /*#__PURE__*/ l.string({ format: 'cid' }),
19
+ }),
20
+ /*#__PURE__*/ l.payload('*/*'),
21
+ [
22
+ 'BlobNotFound',
23
+ 'RepoNotFound',
24
+ 'RepoTakendown',
25
+ 'RepoSuspended',
26
+ 'RepoDeactivated',
27
+ ],
28
+ )
29
+ export { main }
30
+
31
+ export type Params = l.InferMethodParams<typeof main>
32
+ export type Output = l.InferMethodOutput<typeof main>
33
+ export type OutputBody = l.InferMethodOutputBody<typeof main>
34
+
35
+ export const $lxm = /*#__PURE__*/ main.nsid,
36
+ $params = main.parameters,
37
+ $output = main.output
@@ -0,0 +1,6 @@
1
+ /*
2
+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
3
+ */
4
+
5
+ export * from './getBlob.defs.js'
6
+ export * as $defs from './getBlob.defs.js'
@@ -0,0 +1,5 @@
1
+ /*
2
+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
3
+ */
4
+
5
+ export * as getBlob from './sync/getBlob.js'
@@ -3,3 +3,4 @@
3
3
  */
4
4
 
5
5
  export * as repo from './atproto/repo.js'
6
+ export * as sync from './atproto/sync.js'
@@ -0,0 +1 @@
1
+ export * as com from './lexicons/com.js'
package/src/types.ts CHANGED
@@ -10,10 +10,37 @@ export type CallOptions = {
10
10
  signal?: AbortSignal
11
11
  headers?: HeadersInit
12
12
  service?: Service
13
+
14
+ /**
15
+ * Whether to validate the request against the method's input schema. Enabling
16
+ * this can help catch errors early but may have a performance cost. This
17
+ * would typically only be set to `true` in development or debugging
18
+ * scenarios.
19
+ *
20
+ * @default false
21
+ */
13
22
  validateRequest?: boolean
23
+
24
+ /**
25
+ * Whether to validate the response against the method's output schema.
26
+ * Disabling this can improve performance but may lead to runtime errors if
27
+ * the response does not conform to the expected schema. Only set this to
28
+ * `false` if you are certain that the upstream service will always return
29
+ * valid responses.
30
+ *
31
+ * @default true
32
+ */
14
33
  validateResponse?: boolean
15
34
  }
16
35
 
36
+ export type BinaryBodyInit =
37
+ | Uint8Array
38
+ | ArrayBuffer
39
+ | Blob
40
+ | ReadableStream<Uint8Array>
41
+ | AsyncIterable<Uint8Array>
42
+ | string
43
+
17
44
  export type Namespace<T> = T | { main: T }
18
45
 
19
46
  export function getMain<T extends object>(ns: Namespace<T>): T {
package/src/util.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { DidString } from '@atproto/lex-schema'
2
+
3
+ export type Payload<B = unknown, E extends string = string> = {
4
+ body: B
5
+ encoding: E
6
+ }
7
+
8
+ export function isBlobLike(value: unknown): value is Blob {
9
+ if (value == null) return false
10
+ if (typeof value !== 'object') return false
11
+ if (typeof Blob === 'function' && value instanceof Blob) return true
12
+
13
+ // Support for Blobs provided by libraries that don't use the native Blob
14
+ // (e.g. fetch-blob from node-fetch).
15
+ // https://github.com/node-fetch/fetch-blob/blob/a1a182e5978811407bef4ea1632b517567dda01f/index.js#L233-L244
16
+
17
+ const tag = (value as any)[Symbol.toStringTag]
18
+ if (tag === 'Blob' || tag === 'File') {
19
+ return 'stream' in value && typeof value.stream === 'function'
20
+ }
21
+
22
+ return false
23
+ }
24
+
25
+ export function isAsyncIterable<T>(
26
+ value: T,
27
+ ): value is unknown extends T
28
+ ? T & AsyncIterable<unknown>
29
+ : Extract<T, AsyncIterable<any>> {
30
+ return (
31
+ value != null && typeof (value as any)[Symbol.asyncIterator] === 'function'
32
+ )
33
+ }
34
+
35
+ export function buildAtprotoHeaders(options: {
36
+ headers?: HeadersInit
37
+ service?: `${DidString}#${string}`
38
+ labelers?: Iterable<DidString>
39
+ }): Headers {
40
+ const headers = new Headers(options?.headers)
41
+
42
+ if (options.service && !headers.has('atproto-proxy')) {
43
+ headers.set('atproto-proxy', options.service)
44
+ }
45
+
46
+ if (options.labelers) {
47
+ headers.set(
48
+ 'atproto-accept-labelers',
49
+ [...options.labelers, headers.get('atproto-accept-labelers')?.trim()]
50
+ .filter(Boolean)
51
+ .join(', '),
52
+ )
53
+ }
54
+
55
+ return headers
56
+ }
57
+
58
+ export function toReadableStream(
59
+ data: AsyncIterable<Uint8Array>,
60
+ ): ReadableStream<Uint8Array> {
61
+ // Use the native ReadableStream.from() if available.
62
+ if ('from' in ReadableStream && typeof ReadableStream.from === 'function') {
63
+ return ReadableStream.from(data)
64
+ }
65
+
66
+ let iterator: AsyncIterator<Uint8Array> | undefined
67
+ return new ReadableStream({
68
+ async pull(controller) {
69
+ try {
70
+ iterator ??= data[Symbol.asyncIterator]()
71
+ const result = await iterator!.next()
72
+ if (result.done) controller.close()
73
+ else controller.enqueue(result.value)
74
+ } catch (err) {
75
+ controller.error(err)
76
+ iterator = undefined
77
+ }
78
+ },
79
+ async cancel() {
80
+ await iterator?.return?.()
81
+ iterator = undefined
82
+ },
83
+ })
84
+ }
@@ -0,0 +1,195 @@
1
+ import { l } from '@atproto/lex-schema'
2
+ import { Payload } from './util.js'
3
+
4
+ export type XrpcErrorCode = string
5
+ export const xrpcErrorCodeSchema: l.Schema<XrpcErrorCode> = l.string({
6
+ minLength: 1,
7
+ })
8
+
9
+ export class XrpcError<N extends XrpcErrorCode = XrpcErrorCode> extends Error {
10
+ name = 'XrpcError'
11
+
12
+ constructor(
13
+ readonly error: N,
14
+ message: string = `An ${error} XRPC error occurred.`,
15
+ options?: ErrorOptions,
16
+ ) {
17
+ super(message, options)
18
+ }
19
+ }
20
+
21
+ export type XrpcErrorBody<N extends XrpcErrorCode = XrpcErrorCode> = {
22
+ error: N
23
+ message?: string
24
+ }
25
+ export type XrpcErrorPayload<N extends XrpcErrorCode = XrpcErrorCode> = {
26
+ encoding: 'application/json'
27
+ body: XrpcErrorBody<N>
28
+ }
29
+
30
+ const xrpcErrorBodySchema: l.Schema<XrpcErrorBody> = l.object({
31
+ error: xrpcErrorCodeSchema,
32
+ message: l.optional(l.string()),
33
+ })
34
+
35
+ /**
36
+ * All unsuccessful responses should follow a standard error response
37
+ * schema. The Content-Type should be application/json, and the payload
38
+ * should be a JSON object with the following fields:
39
+ *
40
+ * - `error` (string, required): type name of the error (generic ASCII
41
+ * constant, no whitespace)
42
+ * - `message` (string, optional): description of the error, appropriate for
43
+ * display to humans
44
+ *
45
+ * This function checks whether a given payload matches this schema.
46
+ */
47
+ export function isXrpcErrorPayload(
48
+ payload: Payload | null,
49
+ ): payload is XrpcErrorPayload {
50
+ return (
51
+ payload !== null &&
52
+ payload.encoding === 'application/json' &&
53
+ xrpcErrorBodySchema.matches(payload.body)
54
+ )
55
+ }
56
+
57
+ /**
58
+ * Interface representing a failed XRPC request result.
59
+ */
60
+ type XrpcFailureResult<N extends XrpcErrorCode, E> = l.ResultFailure<E> & {
61
+ readonly error: N
62
+ shouldRetry(): boolean
63
+ matchesSchema(): boolean
64
+ }
65
+
66
+ /**
67
+ * Class used to represent an HTTP request that resulted in an XRPC method error
68
+ * That is, a non-2xx response with a valid XRPC error payload.
69
+ */
70
+ export class XrpcResponseError<
71
+ M extends l.Procedure | l.Query = l.Procedure | l.Query,
72
+ N extends XrpcErrorCode = XrpcErrorCode,
73
+ >
74
+ extends XrpcError<N>
75
+ implements XrpcFailureResult<N, XrpcResponseError<M, N>>
76
+ {
77
+ name = 'XrpcResponseError' as const
78
+
79
+ constructor(
80
+ readonly method: M,
81
+ readonly status: number,
82
+ readonly headers: Headers,
83
+ readonly payload: XrpcErrorPayload<N>,
84
+ options?: ErrorOptions,
85
+ ) {
86
+ const { error, message } = payload.body
87
+ super(error, message, options)
88
+ }
89
+
90
+ readonly success = false
91
+
92
+ get reason(): this {
93
+ return this as this
94
+ }
95
+
96
+ get body(): XrpcErrorBody {
97
+ return this.payload.body
98
+ }
99
+
100
+ matchesSchema(): this is M extends {
101
+ errors: readonly (infer E extends string)[]
102
+ }
103
+ ? XrpcResponseError<M, E>
104
+ : never {
105
+ return this.method.errors?.includes(this.error) ?? false
106
+ }
107
+
108
+ shouldRetry(): boolean {
109
+ // Do not retry client errors
110
+ if (this.status < 500) return false
111
+
112
+ return true
113
+ }
114
+ }
115
+
116
+ /**
117
+ * This class represents an invalid XRPC response from the server.
118
+ */
119
+ export class XrpcInvalidResponseError
120
+ extends XrpcError<'UpstreamFailure'>
121
+ implements XrpcFailureResult<'UpstreamFailure', XrpcInvalidResponseError>
122
+ {
123
+ name = 'XrpcInvalidResponseError' as const
124
+
125
+ // For debugging purposes, we keep the response details here
126
+ readonly response: {
127
+ status: number
128
+ headers: Headers
129
+ payload: Payload | null
130
+ }
131
+
132
+ constructor(
133
+ message: string,
134
+ response: { status: number; headers: Headers },
135
+ payload: Payload | null,
136
+ options?: ErrorOptions,
137
+ ) {
138
+ super('UpstreamFailure', message, { cause: options?.cause })
139
+ this.response = {
140
+ status: response.status,
141
+ headers: response.headers,
142
+ payload,
143
+ }
144
+ }
145
+
146
+ readonly success = false as const
147
+
148
+ get reason(): this {
149
+ return this
150
+ }
151
+
152
+ matchesSchema(): false {
153
+ return false
154
+ }
155
+
156
+ shouldRetry(): boolean {
157
+ // Do not retry client errors
158
+ return this.response.status >= 500
159
+ }
160
+ }
161
+
162
+ export class XrpcUnexpectedError
163
+ extends XrpcError<'InternalServerError'>
164
+ implements XrpcFailureResult<'InternalServerError', unknown>
165
+ {
166
+ name = 'XrpcUnexpectedError' as const
167
+
168
+ protected constructor(message: string, options: Required<ErrorOptions>) {
169
+ super('InternalServerError', message, options)
170
+ }
171
+
172
+ readonly success = false
173
+
174
+ get reason() {
175
+ return this.cause
176
+ }
177
+
178
+ matchesSchema(): false {
179
+ return false
180
+ }
181
+
182
+ shouldRetry(): boolean {
183
+ return true
184
+ }
185
+
186
+ static from(
187
+ cause: unknown,
188
+ message: string = cause instanceof XrpcError
189
+ ? cause.message
190
+ : 'XRPC request failed',
191
+ ): XrpcUnexpectedError {
192
+ if (cause instanceof XrpcUnexpectedError) return cause
193
+ return new XrpcUnexpectedError(message, { cause })
194
+ }
195
+ }
@@ -0,0 +1,213 @@
1
+ import { lexParse } from '@atproto/lex-json'
2
+ import {
3
+ InferMethodOutputBody,
4
+ InferMethodOutputEncoding,
5
+ Procedure,
6
+ Query,
7
+ ResultSuccess,
8
+ } from '@atproto/lex-schema'
9
+ import { Payload } from './util.js'
10
+ import {
11
+ XrpcInvalidResponseError,
12
+ XrpcResponseError,
13
+ isXrpcErrorPayload,
14
+ } from './xrpc-error.js'
15
+
16
+ export type XrpcResponseBody<M extends Procedure | Query> =
17
+ InferMethodOutputBody<M, Uint8Array>
18
+
19
+ export type XrpcResponsePayload<M extends Procedure | Query> =
20
+ InferMethodOutputEncoding<M> extends infer E extends string
21
+ ? Payload<XrpcResponseBody<M>, E>
22
+ : null
23
+
24
+ /**
25
+ * Small container for XRPC response data.
26
+ *
27
+ * @implements {ResultSuccess<XrpcResponse<M>>} for convenience in result handling contexts.
28
+ */
29
+ export class XrpcResponse<const M extends Procedure | Query>
30
+ implements ResultSuccess<XrpcResponse<M>>
31
+ {
32
+ /** @see {@link ResultSuccess.success} */
33
+ readonly success = true as const
34
+
35
+ /** @see {@link ResultSuccess.value} */
36
+ get value(): this {
37
+ return this
38
+ }
39
+
40
+ constructor(
41
+ readonly method: M,
42
+ readonly status: number,
43
+ readonly headers: Headers,
44
+ readonly payload: XrpcResponsePayload<M>,
45
+ ) {}
46
+
47
+ /**
48
+ * Whether the response payload was parsed as {@link LexValue} (`true`) or is
49
+ * in binary form {@link Uint8Array} (`false`).
50
+ */
51
+ get isParsed() {
52
+ return this.encoding === 'application/json' && shouldParse(this.method)
53
+ }
54
+
55
+ get encoding() {
56
+ return this.payload?.encoding as InferMethodOutputEncoding<M>
57
+ }
58
+
59
+ get body() {
60
+ return this.payload?.body as XrpcResponseBody<M>
61
+ }
62
+
63
+ /**
64
+ * @throws {XrpcInvalidResponseError} when the response is invalid according
65
+ * to the method schema.
66
+ */
67
+ static async fromFetchResponse<const M extends Procedure | Query>(
68
+ method: M,
69
+ response: Response,
70
+ options?: { validateResponse?: boolean },
71
+ ): Promise<XrpcResponse<M>> {
72
+ // @NOTE The body MUST either be read or canceled to avoid resource leaks.
73
+ // Since nothing should cause an exception before "readPayload" is
74
+ // called, we can safely not use a try/finally here.
75
+
76
+ // @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
77
+ if (response.status < 200 || response.status >= 300) {
78
+ // Always parse json for error responses
79
+ const payload = await readPayload(response, { parse: true })
80
+
81
+ if (response.status >= 400 && isXrpcErrorPayload(payload)) {
82
+ throw new XrpcResponseError(
83
+ method,
84
+ response.status,
85
+ response.headers,
86
+ payload,
87
+ )
88
+ }
89
+
90
+ throw new XrpcInvalidResponseError(
91
+ response.status >= 500
92
+ ? `Upstream server encountered an error`
93
+ : response.status >= 400
94
+ ? `Upstream server returned an invalid response payload`
95
+ : `Upstream server returned an invalid status code`,
96
+ response,
97
+ payload,
98
+ )
99
+ }
100
+
101
+ // Only parse json if the schema expects it
102
+ const payload = await readPayload(response, {
103
+ parse: shouldParse(method),
104
+ })
105
+
106
+ // Response is successful (2xx). Validate payload (data and encoding) against schema.
107
+ if (method.output.encoding == null) {
108
+ // Schema expects no payload
109
+ if (payload) {
110
+ throw new XrpcInvalidResponseError(
111
+ `Expected response with no body, got ${payload.encoding}`,
112
+ response,
113
+ payload,
114
+ )
115
+ }
116
+ } else {
117
+ // Schema expects a payload
118
+ if (!payload || !method.output.matchesEncoding(payload.encoding)) {
119
+ throw new XrpcInvalidResponseError(
120
+ payload
121
+ ? `Expected ${method.output.encoding} response, got ${payload.encoding}`
122
+ : `Expected non-empty response with content-type ${method.output.encoding}`,
123
+ response,
124
+ payload,
125
+ )
126
+ }
127
+
128
+ // Assert valid response body.
129
+ if (method.output.schema && options?.validateResponse !== false) {
130
+ const result = method.output.schema.safeParse(payload.body, {
131
+ allowTransform: false,
132
+ })
133
+
134
+ if (!result.success) {
135
+ throw new XrpcInvalidResponseError(
136
+ `Response validation failed: ${result.reason.message}`,
137
+ response,
138
+ payload,
139
+ { cause: result.reason },
140
+ )
141
+ }
142
+ }
143
+ }
144
+
145
+ return new XrpcResponse<M>(
146
+ method,
147
+ response.status,
148
+ response.headers,
149
+ payload as XrpcResponsePayload<M>,
150
+ )
151
+ }
152
+ }
153
+
154
+ function shouldParse(method: Procedure | Query) {
155
+ return method.output.encoding === 'application/json'
156
+ }
157
+
158
+ /**
159
+ * @note this function always consumes the response body
160
+ */
161
+ async function readPayload(
162
+ response: Response,
163
+ options?: { parse?: boolean },
164
+ ): Promise<Payload | null> {
165
+ // @TODO Should we limit the maximum response size here (this could also be
166
+ // done by the FetchHandler)?
167
+
168
+ const encoding = response.headers
169
+ .get('content-type')
170
+ ?.split(';')[0]
171
+ .trim()
172
+ .toLowerCase()
173
+
174
+ // Response content-type is undefined
175
+ if (!encoding) {
176
+ // If the body is empty, return null (= no payload)
177
+ const body = await response.arrayBuffer()
178
+ if (body.byteLength === 0) return null
179
+
180
+ // If we got data despite no content-type, treat it as binary
181
+ return {
182
+ encoding: 'application/octet-stream',
183
+ body: new Uint8Array(body),
184
+ }
185
+ }
186
+
187
+ if (options?.parse && encoding === 'application/json') {
188
+ // @NOTE It might be worth returning the raw bytes here (Uint8Array) and
189
+ // perform the lex parsing using cborg/json, allowing to do
190
+ // bytes->LexValue in one step instead of bytes->text->JSON->LexValue.
191
+ // This would require adding encode/decode utilities to lex-json (similar
192
+ // to @ipld/dag-json)
193
+ const text = await response.text()
194
+
195
+ try {
196
+ // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as
197
+ // using a reviver function during JSON.parse should be faster than
198
+ // parsing to JSON then converting to Lex (?)
199
+
200
+ // @TODO verify statement above
201
+ return { encoding, body: lexParse(text) }
202
+ } catch (cause) {
203
+ throw new XrpcInvalidResponseError(
204
+ 'Invalid JSON response body',
205
+ response,
206
+ null,
207
+ { cause },
208
+ )
209
+ }
210
+ }
211
+
212
+ return { encoding, body: new Uint8Array(await response.arrayBuffer()) }
213
+ }