@atproto/lex-client 0.0.4 → 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 +27 -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
package/src/xrpc.ts CHANGED
@@ -1,29 +1,83 @@
1
- import { LexValue } from '@atproto/lex-data'
2
- import { lexParse, lexStringify } from '@atproto/lex-json'
1
+ import { LexValue, isLexScalar, isPlainObject } from '@atproto/lex-data'
2
+ import { lexStringify } from '@atproto/lex-json'
3
3
  import {
4
- DidString,
5
- InferParamsSchema,
6
- InferPayloadBody,
4
+ InferMethodInput,
5
+ InferMethodParams,
7
6
  Params,
8
7
  ParamsSchema,
8
+ Payload as LexPayload,
9
9
  Procedure,
10
10
  Query,
11
11
  Restricted,
12
12
  Subscription,
13
13
  } from '@atproto/lex-schema'
14
14
  import { Agent } from './agent.js'
15
+ import { BinaryBodyInit, CallOptions, Namespace, getMain } from './types.js'
15
16
  import {
16
- KnownError,
17
+ Payload,
18
+ buildAtprotoHeaders,
19
+ isAsyncIterable,
20
+ isBlobLike,
21
+ toReadableStream,
22
+ } from './util.js'
23
+ import {
24
+ XrpcInvalidResponseError,
17
25
  XrpcResponseError,
18
- XrpcServiceError,
19
- xrpcErrorBodySchema,
20
- } from './error.js'
21
- import { XrpcResponse, XrpcResponseBody } from './response.js'
22
- import { CallOptions, Namespace, Service, getMain } from './types.js'
26
+ XrpcUnexpectedError,
27
+ } from './xrpc-error.js'
28
+ import { XrpcResponse } from './xrpc-response.js'
29
+
30
+ export * from './xrpc-error.js'
31
+ export * from './xrpc-response.js'
32
+
33
+ // If all params are optional, allow omitting the params object
34
+ type XrpcParamsOptions<P extends Params> =
35
+ NonNullable<unknown> extends P ? { params?: P } : { params: P }
36
+
37
+ type XrpcRequestPayload<M extends Procedure | Query> = InferMethodInput<
38
+ M,
39
+ BinaryBodyInit
40
+ >
41
+
42
+ type XrpcInputOptions<In> = In extends { body: infer B; encoding: infer E }
43
+ ? // encoding will be inferred from the schema at runtime if not provided
44
+ { body: B; encoding?: E }
45
+ : { body?: undefined; encoding?: undefined }
23
46
 
24
47
  export type XrpcOptions<M extends Procedure | Query = Procedure | Query> =
25
- CallOptions & XrpcRequestUrlOptions<M> & XrpcRequestInitOptions<M>
48
+ CallOptions &
49
+ XrpcInputOptions<XrpcRequestPayload<M>> &
50
+ XrpcParamsOptions<InferMethodParams<M>>
51
+
52
+ export type XrpcFailure<M extends Procedure | Query> =
53
+ // The server returned a valid XRPC error response
54
+ | XrpcResponseError<M>
55
+ // The response was not a valid XRPC response, or it does not match the schema
56
+ | XrpcInvalidResponseError
57
+ // Something went wrong (network error, etc.)
58
+ | XrpcUnexpectedError
59
+
60
+ export type XrpcResult<M extends Procedure | Query> =
61
+ | XrpcResponse<M>
62
+ | XrpcFailure<M>
63
+
64
+ /**
65
+ * Utility method to type cast the error thrown by {@link xrpc} to an
66
+ * {@link XrpcFailure} matching the provided method. Only use this function
67
+ * inside a catch block right after calling {@link xrpc}, and use the same
68
+ * method type parameter as used in the {@link xrpc} call.
69
+ */
70
+ function asXrpcFailure<M extends Procedure | Query>(
71
+ err: unknown,
72
+ ): XrpcFailure<M> {
73
+ if (err instanceof XrpcResponseError) return err
74
+ if (err instanceof XrpcInvalidResponseError) return err
75
+ return XrpcUnexpectedError.from(err)
76
+ }
26
77
 
78
+ /**
79
+ * @throws XrpcFailure<M>
80
+ */
27
81
  export async function xrpc<const M extends Query | Procedure>(
28
82
  agent: Agent,
29
83
  ns: NonNullable<unknown> extends XrpcOptions<M>
@@ -40,23 +94,48 @@ export async function xrpc<const M extends Query | Procedure>(
40
94
  ns: Namespace<M>,
41
95
  options: XrpcOptions<M> = {} as XrpcOptions<M>,
42
96
  ): Promise<XrpcResponse<M>> {
43
- options.signal?.throwIfAborted()
97
+ try {
98
+ return await xrpcRequest<M>(agent, ns, options)
99
+ } catch (err) {
100
+ throw asXrpcFailure<M>(err)
101
+ }
102
+ }
103
+
104
+ export async function xrpcSafe<const M extends Query | Procedure>(
105
+ agent: Agent,
106
+ ns: NonNullable<unknown> extends XrpcOptions<M>
107
+ ? Namespace<M>
108
+ : Restricted<'This XRPC method requires an "options" argument'>,
109
+ ): Promise<XrpcResult<M>>
110
+ export async function xrpcSafe<const M extends Query | Procedure>(
111
+ agent: Agent,
112
+ ns: Namespace<M>,
113
+ options: XrpcOptions<M>,
114
+ ): Promise<XrpcResult<M>>
115
+ export async function xrpcSafe<const M extends Query | Procedure>(
116
+ agent: Agent,
117
+ ns: Namespace<M>,
118
+ options: XrpcOptions<M> = {} as XrpcOptions<M>,
119
+ ): Promise<XrpcResult<M>> {
120
+ return xrpcRequest<M>(agent, ns, options).catch(asXrpcFailure<M>)
121
+ }
122
+
123
+ async function xrpcRequest<const M extends Query | Procedure>(
124
+ agent: Agent,
125
+ ns: Namespace<M>,
126
+ options: XrpcOptions<M> = {} as XrpcOptions<M>,
127
+ ): Promise<XrpcResponse<M>> {
44
128
  const method = getMain(ns)
129
+ options.signal?.throwIfAborted()
45
130
  const url = xrpcRequestUrl(method, options)
46
131
  const request = xrpcRequestInit(method, options)
47
132
  const response = await agent.fetchHandler(url, request)
48
- return xrpcResponseHandler<M>(response, method, options)
133
+ return XrpcResponse.fromFetchResponse<M>(method, response, options)
49
134
  }
50
135
 
51
- export type XrpcRequestUrlOptions<M extends Query | Procedure | Subscription> =
52
- CallOptions &
53
- (undefined extends InferParamsSchema<M['parameters']>
54
- ? { params?: InferParamsSchema<M['parameters']> }
55
- : { params: InferParamsSchema<M['parameters']> })
56
-
57
- export function xrpcRequestUrl<M extends Procedure | Query | Subscription>(
136
+ function xrpcRequestUrl<M extends Procedure | Query | Subscription>(
58
137
  method: M,
59
- options: XrpcRequestUrlOptions<M>,
138
+ options: CallOptions & { params?: Params },
60
139
  ) {
61
140
  const path = `/xrpc/${method.nsid}`
62
141
 
@@ -67,7 +146,7 @@ export function xrpcRequestUrl<M extends Procedure | Query | Subscription>(
67
146
  return queryString ? `${path}?${queryString}` : path
68
147
  }
69
148
 
70
- export function xrpcRequestParams(
149
+ function xrpcRequestParams(
71
150
  schema: ParamsSchema | undefined,
72
151
  params: Params | undefined,
73
152
  options: CallOptions,
@@ -78,32 +157,37 @@ export function xrpcRequestParams(
78
157
  return urlSearchParams?.size ? urlSearchParams.toString() : undefined
79
158
  }
80
159
 
81
- export type XrpcRequestInitOptions<T extends Query | Procedure> = CallOptions &
82
- (T extends Procedure
83
- ? never extends InferPayloadBody<T['input']>
84
- ? { body?: InferPayloadBody<T['input']> }
85
- : { body: InferPayloadBody<T['input']> }
86
- : { body?: never })
87
-
88
- export function xrpcRequestInit<T extends Procedure | Query>(
160
+ function xrpcRequestInit<T extends Procedure | Query>(
89
161
  schema: T,
90
- options: XrpcRequestInitOptions<T>,
162
+ options: CallOptions & {
163
+ body?: LexValue | BinaryBodyInit
164
+ encoding?: string
165
+ },
91
166
  ): RequestInit & { duplex?: 'half' } {
92
- const headers = xrpcRequestHeaders(options)
167
+ const headers = buildAtprotoHeaders(options)
168
+
169
+ // Tell the server what type of response we're expecting
170
+ if (schema.output.encoding) {
171
+ headers.set('accept', schema.output.encoding)
172
+ }
173
+
174
+ // Caller should not set content-type header
175
+ if (headers.has('content-type')) {
176
+ const contentType = headers.get('content-type')
177
+ throw new TypeError(`Unexpected content-type header (${contentType})`)
178
+ }
93
179
 
94
180
  // Requests with body
95
- if ('input' in schema && schema.input?.encoding) {
96
- if (
97
- options.validateRequest &&
98
- schema.input == null &&
99
- options.body !== undefined
100
- ) {
101
- throw new TypeError(
102
- `XRPC method ${schema.nsid} does not accept a request body`,
103
- )
181
+ if ('input' in schema) {
182
+ const encodingHint = options.encoding
183
+ const input = xrpcProcedureInput(schema, options, encodingHint)
184
+
185
+ if (input) {
186
+ headers.set('content-type', input.encoding)
187
+ } else if (encodingHint != null) {
188
+ throw new TypeError(`Unexpected encoding hint (${encodingHint})`)
104
189
  }
105
190
 
106
- headers.set('content-type', schema.input.encoding)
107
191
  return {
108
192
  duplex: 'half',
109
193
  redirect: 'follow',
@@ -112,12 +196,7 @@ export function xrpcRequestInit<T extends Procedure | Query>(
112
196
  signal: options.signal,
113
197
  method: 'POST',
114
198
  headers,
115
- body: xrpcRequestBody(
116
- schema.input?.encoding,
117
- options.validateRequest
118
- ? schema.input?.body.parse(options.body)
119
- : options.body,
120
- ),
199
+ body: input?.body,
121
200
  }
122
201
  }
123
202
 
@@ -128,208 +207,118 @@ export function xrpcRequestInit<T extends Procedure | Query>(
128
207
  referrerPolicy: 'strict-origin-when-cross-origin', // (default)
129
208
  mode: 'cors', // (default)
130
209
  signal: options.signal,
131
- method: schema instanceof Query ? 'GET' : 'POST',
210
+ method: 'GET',
132
211
  headers,
133
212
  }
134
213
  }
135
214
 
136
- export function xrpcRequestHeaders(options: {
137
- headers?: HeadersInit
138
- service?: Service
139
- labelers?: Iterable<DidString>
140
- }): Headers {
141
- const headers = new Headers(options.headers)
142
-
143
- if (options.service && !headers.has('atproto-proxy')) {
144
- headers.set('atproto-proxy', options.service)
145
- }
215
+ function xrpcProcedureInput(
216
+ method: Procedure,
217
+ options: CallOptions & { body?: LexValue | BinaryBodyInit },
218
+ encodingHint?: string,
219
+ ): null | Payload<BodyInit> {
220
+ const { input } = method
221
+ const { body } = options
146
222
 
147
- if (options.labelers) {
148
- headers.set(
149
- 'atproto-accept-labelers',
150
- [...options.labelers, headers.get('atproto-accept-labelers')?.trim()]
151
- .filter(Boolean)
152
- .join(', '),
153
- )
223
+ if (options.validateRequest) {
224
+ input.schema?.check(body)
154
225
  }
155
226
 
156
- return headers
157
- }
227
+ // Special handling for endpoints expecting application/json input
228
+ if (input.encoding === 'application/json') {
229
+ // @NOTE **NOT** using isLexValue here to avoid deep checks in order to
230
+ // distinguish between LexValue and BinaryBodyInit.
231
+ if (!isLexScalar(body) && !isPlainObject(body) && !Array.isArray(body)) {
232
+ throw new TypeError(`Expected LexValue body, got ${typeof body}`)
233
+ }
158
234
 
159
- function xrpcRequestBody(
160
- encoding: string | undefined,
161
- body: LexValue | undefined,
162
- ): BodyInit | null {
163
- if (encoding === undefined) {
164
- return null
235
+ return buildPayload(input, lexStringify(body), encodingHint)
165
236
  }
166
237
 
167
- if (encoding === 'application/json') {
168
- if (body !== undefined) return lexStringify(body)
169
- } else if (encoding.startsWith('text/')) {
170
- if (typeof body === 'string') return body
171
- } else {
172
- if (ArrayBuffer.isView(body) || body instanceof ArrayBuffer) return body
238
+ // Other encodings will be sent unaltered (ie. as binary data)
239
+ switch (typeof body) {
240
+ case 'undefined':
241
+ case 'string':
242
+ return buildPayload(input, body, encodingHint)
243
+ case 'object': {
244
+ if (body === null) break
245
+ if (
246
+ ArrayBuffer.isView(body) ||
247
+ body instanceof ArrayBuffer ||
248
+ body instanceof ReadableStream
249
+ ) {
250
+ return buildPayload(input, body, encodingHint)
251
+ } else if (isAsyncIterable(body)) {
252
+ return buildPayload(input, toReadableStream(body), encodingHint)
253
+ } else if (isBlobLike(body)) {
254
+ return buildPayload(input, body, encodingHint || body.type)
255
+ }
256
+ }
173
257
  }
174
258
 
175
- throw new TypeError(`Invalid ${typeof body} body for ${encoding} encoding`)
259
+ throw new TypeError(
260
+ `Invalid ${typeof body} body for ${input.encoding} encoding`,
261
+ )
176
262
  }
177
263
 
178
- export async function xrpcResponseHandler<M extends Procedure | Query>(
179
- response: Response,
180
- schema: M,
181
- options?: { validateResponse?: boolean },
182
- ): Promise<XrpcResponse<M>> {
183
- // @NOTE The body MUST either be read or canceled to avoid resource leaks.
184
- // Since nothing should cause an exception before "readXrpcResponseBody" is
185
- // called, we can safely not use a try/finally here.
186
-
187
- const encoding = extractEncoding(response.headers)
188
-
189
- const body = await readResponseBody(response, encoding).catch((cause) => {
190
- throw new XrpcServiceError(
191
- KnownError.InvalidResponse,
192
- response.status,
193
- response.headers,
194
- undefined,
195
- 'Failed to read XRPC response',
196
- { cause },
197
- )
198
- })
199
-
200
- // @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
201
- if (response.status < 200 || response.status >= 300) {
202
- // All unsuccessful responses should follow a standard error response
203
- // schema. The Content-Type should be application/json, and the payload
204
- // should be a JSON object with the following fields:
205
- // - error (string, required): type name of the error (generic ASCII
206
- // constant, no whitespace)
207
- // - message (string, optional): description of the error, appropriate for
208
- // display to humans
209
- if (
210
- body != null &&
211
- encoding === 'application/json' &&
212
- xrpcErrorBodySchema.matches(body)
213
- ) {
214
- throw new XrpcResponseError(
215
- response.status,
216
- response.headers,
217
- encoding,
218
- body,
264
+ function buildPayload(
265
+ schema: LexPayload,
266
+ body: undefined | BodyInit,
267
+ encodingHint?: string,
268
+ ): null | Payload<BodyInit> {
269
+ if (schema.encoding === undefined) {
270
+ if (body !== undefined) {
271
+ throw new TypeError(
272
+ `Cannot send a ${typeof body} body with undefined encoding`,
219
273
  )
220
274
  }
221
275
 
222
- throw new XrpcServiceError(
223
- response.status >= 500
224
- ? KnownError.InternalServerError
225
- : KnownError.InvalidResponse,
226
- response.status,
227
- response.headers,
228
- body,
229
- )
276
+ return null
230
277
  }
231
278
 
232
- // Check response encoding
233
- if (schema.output.encoding !== encoding) {
234
- throw new XrpcServiceError(
235
- KnownError.InvalidResponse,
236
- response.status,
237
- response.headers,
238
- body,
239
- `Expected response with content-type ${schema.output.encoding}, got ${encoding}`,
240
- )
279
+ if (body === undefined) {
280
+ // This error would be returned by the server, but we can catch it earlier
281
+ // to avoid un-necessary requests. Note that a content-length of 0 does not
282
+ // necessary mean that the body is "empty" (e.g. an empty txt file).
283
+ throw new TypeError(`A request body is expected but none was provided`)
241
284
  }
242
285
 
243
- if (schema.output.encoding == null) {
244
- if (body !== undefined) {
245
- throw new XrpcServiceError(
246
- KnownError.InvalidResponse,
247
- response.status,
248
- response.headers,
249
- body,
250
- `Expected empty response body`,
251
- )
252
- }
286
+ const encoding = buildEncoding(schema, encodingHint)
287
+ return { encoding, body }
288
+ }
289
+
290
+ function buildEncoding(schema: LexPayload, encodingHint?: string): string {
291
+ // Should never happen (required for type safety)
292
+ if (!schema.encoding) {
293
+ throw new TypeError('Unexpected payload')
294
+ }
253
295
 
254
- return new XrpcResponse<M>(
255
- schema,
256
- response.status,
257
- response.headers,
258
- undefined as XrpcResponseBody<M>,
259
- )
260
- } else {
261
- // @NOTE this should already be enforced by readXrpcResponseBody
262
- if (body === undefined) {
263
- throw new XrpcServiceError(
264
- KnownError.InvalidResponse,
265
- response.status,
266
- response.headers,
267
- body,
268
- `Expected non-empty response body`,
296
+ if (encodingHint?.length) {
297
+ if (!schema.matchesEncoding(encodingHint)) {
298
+ throw new TypeError(
299
+ `Cannot send a body with content-type "${encodingHint}" for "${schema.encoding}" encoding`,
269
300
  )
270
301
  }
271
-
272
- return new XrpcResponse<M>(
273
- schema,
274
- response.status,
275
- response.headers,
276
- schema.output.schema == null || options?.validateResponse === false
277
- ? (body as XrpcResponseBody<M>)
278
- : (schema.output.schema.parse(body) as XrpcResponseBody<M>),
279
- )
302
+ return encodingHint
280
303
  }
281
- }
282
304
 
283
- export function extractEncoding(headers: Headers): string | undefined {
284
- const contentType = headers.get('content-type')
285
- if (!contentType) return undefined
286
- return contentType.split(';')[0].trim()
287
- }
305
+ // Fallback
288
306
 
289
- export async function readResponseBody(
290
- response: Response,
291
- encoding: string,
292
- ): Promise<LexValue>
293
- export async function readResponseBody(
294
- response: Response,
295
- encoding: string | undefined,
296
- ): Promise<LexValue | undefined>
297
- export async function readResponseBody(
298
- response: Response,
299
- encoding: string | undefined,
300
- ): Promise<LexValue | undefined> {
301
- // When encoding is undefined or empty, we expect no body
302
- if (encoding == null) {
303
- if (response.body == null) return undefined
304
-
305
- // Let's make sure the body is empty (while avoiding reading it all).
306
- if (!('getReader' in response.body)) {
307
- // Some environments may not support body.getReader(), fall back to
308
- // reading the whole body.
309
- const buffer = await response.arrayBuffer()
310
- if (buffer.byteLength === 0) return undefined
311
- } else {
312
- const reader = response.body.getReader()
313
- const next = await reader.read()
314
- if (next.done) return undefined
315
- await reader.cancel() // Drain the rest of the (non-empty) body stream
316
- }
317
-
318
- throw new SyntaxError('Content-type is undefined but body is not empty')
307
+ if (schema.encoding === '*/*') {
308
+ return 'application/octet-stream'
319
309
  }
320
310
 
321
- if (encoding === 'application/json') {
322
- // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as using
323
- // a reviver function during JSON.parse should be faster than parsing to
324
- // JSON then converting to Lex (?)
325
-
326
- // @TODO verify statement above
327
- return lexParse(await response.text())
311
+ if (schema.encoding.startsWith('text/')) {
312
+ return schema.encoding.includes('*')
313
+ ? 'text/plain; charset=utf-8'
314
+ : `${schema.encoding}; charset=utf-8`
328
315
  }
329
316
 
330
- if (encoding.startsWith('text/')) {
331
- return response.text()
317
+ if (!schema.encoding.includes('*')) {
318
+ return schema.encoding
332
319
  }
333
320
 
334
- return new Uint8Array(await response.arrayBuffer())
321
+ throw new TypeError(
322
+ `Unable to determine payload encoding. Please provide a 'content-type' header matching ${schema.encoding}.`,
323
+ )
335
324
  }
package/dist/error.d.ts DELETED
@@ -1,66 +0,0 @@
1
- import { LexValue } from '@atproto/lex-data';
2
- import { l } from '@atproto/lex-schema';
3
- export declare enum KnownError {
4
- Unknown = "Unknown",
5
- AuthenticationRequired = "AuthenticationRequired",
6
- Forbidden = "Forbidden",
7
- InternalServerError = "InternalServerError",
8
- InvalidRequest = "InvalidRequest",
9
- InvalidResponse = "InvalidResponse",
10
- MethodNotImplemented = "MethodNotImplemented",
11
- NotAcceptable = "NotAcceptable",
12
- NotEnoughResources = "NotEnoughResources",
13
- PayloadTooLarge = "PayloadTooLarge",
14
- RateLimitExceeded = "RateLimitExceeded",
15
- UnsupportedMediaType = "UnsupportedMediaType",
16
- UpstreamFailure = "UpstreamFailure",
17
- UpstreamTimeout = "UpstreamTimeout",
18
- XRPCNotSupported = "XRPCNotSupported"
19
- }
20
- export type XrpcFailure<N extends string, E> = l.ResultFailure<E> & {
21
- name: N;
22
- };
23
- export type XrpcErrorName = l.UnknownString | KnownError;
24
- export declare const xrpcErrorNameSchema: l.StringSchema<{
25
- readonly minLength: 1;
26
- }>;
27
- export type XrpcErrorBody<N extends XrpcErrorName = XrpcErrorName> = {
28
- error: N;
29
- message?: string;
30
- };
31
- export declare const xrpcErrorBodySchema: l.ObjectSchema<{
32
- readonly error: l.StringSchema<{
33
- readonly minLength: 1;
34
- }>;
35
- readonly message: l.OptionalSchema<string>;
36
- }>;
37
- /**
38
- * @implements {XrpcFailure<N, XrpcError<N>>} for convenience in result handling contexts.
39
- */
40
- export declare class XrpcError<N extends XrpcErrorName = XrpcErrorName> extends Error implements XrpcFailure<N, XrpcError<N>> {
41
- readonly name: N;
42
- constructor(name: N, message?: string, options?: ErrorOptions);
43
- /** @see {@link l.ResultFailure.success} */
44
- readonly success: false;
45
- /** @see {@link l.ResultFailure.error} */
46
- get error(): this;
47
- static from(cause: unknown, message?: string): XrpcError;
48
- }
49
- export declare class XrpcServiceError<N extends XrpcErrorName = XrpcErrorName> extends XrpcError<N> {
50
- readonly status: number;
51
- readonly headers: Headers;
52
- readonly body: undefined | LexValue;
53
- constructor(name: N, status: number, headers: Headers, body: undefined | LexValue, message?: string, options?: ErrorOptions);
54
- }
55
- export declare class XrpcResponseError<N extends XrpcErrorName = XrpcErrorName, B extends XrpcErrorBody<N> = XrpcErrorBody<N>> extends XrpcError<N> {
56
- readonly status: number;
57
- readonly headers: Headers;
58
- readonly encoding: undefined | string;
59
- readonly body: B;
60
- constructor(status: number, headers: Headers, encoding: undefined | string, body: B, options?: ErrorOptions);
61
- }
62
- export type XrpcRequestFailure<M extends l.Procedure | l.Query> = (M extends {
63
- errors: readonly (infer N extends string)[];
64
- } ? XrpcResponseError<N> : never) | XrpcFailure<'Unknown', XrpcResponseError<string>> | XrpcFailure<'UnexpectedError', unknown>;
65
- export declare function asXrpcRequestFailureFor<M extends l.Procedure | l.Query>(schema: M): (error: unknown) => XrpcRequestFailure<M>;
66
- //# sourceMappingURL=error.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"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"}