@atproto/lex-client 0.0.4 → 0.0.6

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 (103) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/agent.d.ts +10 -9
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +3 -0
  5. package/dist/agent.js.map +1 -1
  6. package/dist/client.d.ts +51 -113
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +38 -42
  9. package/dist/client.js.map +1 -1
  10. package/dist/errors.d.ts +82 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +132 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts +7 -7
  19. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts.map +1 -1
  20. package/dist/lexicons/com/atproto/repo/createRecord.defs.js +8 -12
  21. package/dist/lexicons/com/atproto/repo/createRecord.defs.js.map +1 -1
  22. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts +7 -7
  23. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts.map +1 -1
  24. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js +8 -12
  25. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js.map +1 -1
  26. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts +5 -6
  27. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts.map +1 -1
  28. package/dist/lexicons/com/atproto/repo/getRecord.defs.js +6 -10
  29. package/dist/lexicons/com/atproto/repo/getRecord.defs.js.map +1 -1
  30. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts +5 -6
  31. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts.map +1 -1
  32. package/dist/lexicons/com/atproto/repo/listRecords.defs.js +5 -8
  33. package/dist/lexicons/com/atproto/repo/listRecords.defs.js.map +1 -1
  34. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts +7 -7
  35. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts.map +1 -1
  36. package/dist/lexicons/com/atproto/repo/putRecord.defs.js +8 -12
  37. package/dist/lexicons/com/atproto/repo/putRecord.defs.js.map +1 -1
  38. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts +7 -7
  39. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts.map +1 -1
  40. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js +6 -9
  41. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js.map +1 -1
  42. package/dist/lexicons/com/atproto/sync/getBlob.d.ts +3 -0
  43. package/dist/lexicons/com/atproto/sync/getBlob.d.ts.map +1 -0
  44. package/dist/lexicons/com/atproto/sync/getBlob.defs.d.ts +25 -0
  45. package/dist/lexicons/com/atproto/sync/getBlob.defs.d.ts.map +1 -0
  46. package/dist/lexicons/com/atproto/sync/getBlob.defs.js +27 -0
  47. package/dist/lexicons/com/atproto/sync/getBlob.defs.js.map +1 -0
  48. package/dist/lexicons/com/atproto/sync/getBlob.js +10 -0
  49. package/dist/lexicons/com/atproto/sync/getBlob.js.map +1 -0
  50. package/dist/lexicons/com/atproto/sync.d.ts +2 -0
  51. package/dist/lexicons/com/atproto/sync.d.ts.map +1 -0
  52. package/dist/lexicons/com/atproto/sync.js +9 -0
  53. package/dist/lexicons/com/atproto/sync.js.map +1 -0
  54. package/dist/lexicons/com/atproto.d.ts +1 -0
  55. package/dist/lexicons/com/atproto.d.ts.map +1 -1
  56. package/dist/lexicons/com/atproto.js +2 -1
  57. package/dist/lexicons/com/atproto.js.map +1 -1
  58. package/dist/lexicons.d.ts +2 -0
  59. package/dist/lexicons.d.ts.map +1 -0
  60. package/dist/lexicons.js +6 -0
  61. package/dist/lexicons.js.map +1 -0
  62. package/dist/response.d.ts +25 -8
  63. package/dist/response.d.ts.map +1 -1
  64. package/dist/response.js +123 -10
  65. package/dist/response.js.map +1 -1
  66. package/dist/types.d.ts +18 -4
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/types.js +0 -4
  69. package/dist/types.js.map +1 -1
  70. package/dist/util.d.ts +14 -0
  71. package/dist/util.d.ts.map +1 -0
  72. package/dist/util.js +65 -0
  73. package/dist/util.js.map +1 -0
  74. package/dist/xrpc.d.ts +35 -32
  75. package/dist/xrpc.d.ts.map +1 -1
  76. package/dist/xrpc.js +116 -124
  77. package/dist/xrpc.js.map +1 -1
  78. package/package.json +10 -10
  79. package/src/agent.ts +18 -14
  80. package/src/client.ts +135 -114
  81. package/src/errors.ts +206 -0
  82. package/src/index.ts +1 -1
  83. package/src/lexicons/com/atproto/repo/createRecord.defs.ts +31 -36
  84. package/src/lexicons/com/atproto/repo/deleteRecord.defs.ts +27 -32
  85. package/src/lexicons/com/atproto/repo/getRecord.defs.ts +12 -17
  86. package/src/lexicons/com/atproto/repo/listRecords.defs.ts +13 -15
  87. package/src/lexicons/com/atproto/repo/putRecord.defs.ts +32 -37
  88. package/src/lexicons/com/atproto/repo/uploadBlob.defs.ts +13 -15
  89. package/src/lexicons/com/atproto/sync/getBlob.defs.ts +37 -0
  90. package/src/lexicons/com/atproto/sync/getBlob.ts +6 -0
  91. package/src/lexicons/com/atproto/sync.ts +5 -0
  92. package/src/lexicons/com/atproto.ts +1 -0
  93. package/src/lexicons.ts +1 -0
  94. package/src/response.ts +201 -15
  95. package/src/types.ts +26 -5
  96. package/src/util.ts +84 -0
  97. package/src/xrpc.ts +220 -232
  98. package/tsconfig.tests.json +4 -7
  99. package/dist/error.d.ts +0 -66
  100. package/dist/error.d.ts.map +0 -1
  101. package/dist/error.js +0 -100
  102. package/dist/error.js.map +0 -1
  103. package/src/error.ts +0 -145
package/src/xrpc.ts CHANGED
@@ -1,62 +1,140 @@
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,
6
+ Main,
7
7
  Params,
8
8
  ParamsSchema,
9
+ Payload as LexPayload,
9
10
  Procedure,
10
11
  Query,
11
12
  Restricted,
12
13
  Subscription,
14
+ getMain,
13
15
  } from '@atproto/lex-schema'
14
16
  import { Agent } from './agent.js'
15
17
  import {
16
- KnownError,
17
- 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'
23
-
24
- export type XrpcOptions<M extends Procedure | Query = Procedure | Query> =
25
- CallOptions & XrpcRequestUrlOptions<M> & XrpcRequestInitOptions<M>
18
+ LexRpcResponseError,
19
+ LexRpcUnexpectedError,
20
+ LexRpcUpstreamError,
21
+ } from './errors.js'
22
+ import { LexRpcResponse } from './response.js'
23
+ import { BinaryBodyInit, CallOptions } from './types.js'
24
+ import {
25
+ Payload,
26
+ buildAtprotoHeaders,
27
+ isAsyncIterable,
28
+ isBlobLike,
29
+ toReadableStream,
30
+ } from './util.js'
31
+
32
+ // If all params are optional, allow omitting the params object
33
+ type LexRpcParamsOptions<P extends Params> =
34
+ NonNullable<unknown> extends P ? { params?: P } : { params: P }
35
+
36
+ type LexRpcRequestPayload<M extends Procedure | Query> = InferMethodInput<
37
+ M,
38
+ BinaryBodyInit
39
+ >
40
+
41
+ type LexRpcInputOptions<In> = In extends { body: infer B; encoding: infer E }
42
+ ? // encoding will be inferred from the schema at runtime if not provided
43
+ { body: B; encoding?: E }
44
+ : { body?: undefined; encoding?: undefined }
45
+
46
+ export type LexRpcOptions<M extends Procedure | Query = Procedure | Query> =
47
+ CallOptions &
48
+ LexRpcInputOptions<LexRpcRequestPayload<M>> &
49
+ LexRpcParamsOptions<InferMethodParams<M>>
50
+
51
+ export type LexRpcFailure<M extends Procedure | Query> =
52
+ // The server returned a valid XRPC error response
53
+ | LexRpcResponseError<M>
54
+ // The response was not a valid XRPC response, or it does not match the schema
55
+ | LexRpcUpstreamError
56
+ // Something went wrong (network error, etc.)
57
+ | LexRpcUnexpectedError
58
+
59
+ export type LexRpcResult<M extends Procedure | Query> =
60
+ | LexRpcResponse<M>
61
+ | LexRpcFailure<M>
62
+
63
+ /**
64
+ * Utility method to type cast the error thrown by {@link xrpc} to an
65
+ * {@link LexRpcFailure} matching the provided method. Only use this function
66
+ * inside a catch block right after calling {@link xrpc}, and use the same
67
+ * method type parameter as used in the {@link xrpc} call.
68
+ */
69
+ export function asLexRpcFailure<
70
+ M extends Procedure | Query = Procedure | Query,
71
+ >(err: unknown): LexRpcFailure<M> {
72
+ if (err instanceof LexRpcResponseError) return err
73
+ if (err instanceof LexRpcUpstreamError) return err
74
+ return LexRpcUnexpectedError.from(err)
75
+ }
26
76
 
77
+ /**
78
+ * @throws LexRpcFailure<M>
79
+ */
27
80
  export async function xrpc<const M extends Query | Procedure>(
28
81
  agent: Agent,
29
- ns: NonNullable<unknown> extends XrpcOptions<M>
30
- ? Namespace<M>
82
+ ns: NonNullable<unknown> extends LexRpcOptions<M>
83
+ ? Main<M>
31
84
  : Restricted<'This XRPC method requires an "options" argument'>,
32
- ): Promise<XrpcResponse<M>>
85
+ ): Promise<LexRpcResponse<M>>
33
86
  export async function xrpc<const M extends Query | Procedure>(
34
87
  agent: Agent,
35
- ns: Namespace<M>,
36
- options: XrpcOptions<M>,
37
- ): Promise<XrpcResponse<M>>
88
+ ns: Main<M>,
89
+ options: LexRpcOptions<M>,
90
+ ): Promise<LexRpcResponse<M>>
38
91
  export async function xrpc<const M extends Query | Procedure>(
39
92
  agent: Agent,
40
- ns: Namespace<M>,
41
- options: XrpcOptions<M> = {} as XrpcOptions<M>,
42
- ): Promise<XrpcResponse<M>> {
43
- options.signal?.throwIfAborted()
93
+ ns: Main<M>,
94
+ options: LexRpcOptions<M> = {} as LexRpcOptions<M>,
95
+ ): Promise<LexRpcResponse<M>> {
96
+ try {
97
+ return await lexRpcRequest<M>(agent, ns, options)
98
+ } catch (err) {
99
+ throw asLexRpcFailure<M>(err)
100
+ }
101
+ }
102
+
103
+ export async function xrpcSafe<const M extends Query | Procedure>(
104
+ agent: Agent,
105
+ ns: NonNullable<unknown> extends LexRpcOptions<M>
106
+ ? Main<M>
107
+ : Restricted<'This XRPC method requires an "options" argument'>,
108
+ ): Promise<LexRpcResult<M>>
109
+ export async function xrpcSafe<const M extends Query | Procedure>(
110
+ agent: Agent,
111
+ ns: Main<M>,
112
+ options: LexRpcOptions<M>,
113
+ ): Promise<LexRpcResult<M>>
114
+ export async function xrpcSafe<const M extends Query | Procedure>(
115
+ agent: Agent,
116
+ ns: Main<M>,
117
+ options: LexRpcOptions<M> = {} as LexRpcOptions<M>,
118
+ ): Promise<LexRpcResult<M>> {
119
+ return lexRpcRequest<M>(agent, ns, options).catch(asLexRpcFailure<M>)
120
+ }
121
+
122
+ async function lexRpcRequest<const M extends Query | Procedure>(
123
+ agent: Agent,
124
+ ns: Main<M>,
125
+ options: LexRpcOptions<M> = {} as LexRpcOptions<M>,
126
+ ): Promise<LexRpcResponse<M>> {
44
127
  const method = getMain(ns)
128
+ options.signal?.throwIfAborted()
45
129
  const url = xrpcRequestUrl(method, options)
46
130
  const request = xrpcRequestInit(method, options)
47
131
  const response = await agent.fetchHandler(url, request)
48
- return xrpcResponseHandler<M>(response, method, options)
132
+ return LexRpcResponse.fromFetchResponse<M>(method, response, options)
49
133
  }
50
134
 
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>(
135
+ function xrpcRequestUrl<M extends Procedure | Query | Subscription>(
58
136
  method: M,
59
- options: XrpcRequestUrlOptions<M>,
137
+ options: CallOptions & { params?: Params },
60
138
  ) {
61
139
  const path = `/xrpc/${method.nsid}`
62
140
 
@@ -67,7 +145,7 @@ export function xrpcRequestUrl<M extends Procedure | Query | Subscription>(
67
145
  return queryString ? `${path}?${queryString}` : path
68
146
  }
69
147
 
70
- export function xrpcRequestParams(
148
+ function xrpcRequestParams(
71
149
  schema: ParamsSchema | undefined,
72
150
  params: Params | undefined,
73
151
  options: CallOptions,
@@ -78,32 +156,37 @@ export function xrpcRequestParams(
78
156
  return urlSearchParams?.size ? urlSearchParams.toString() : undefined
79
157
  }
80
158
 
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>(
159
+ function xrpcRequestInit<T extends Procedure | Query>(
89
160
  schema: T,
90
- options: XrpcRequestInitOptions<T>,
161
+ options: CallOptions & {
162
+ body?: LexValue | BinaryBodyInit
163
+ encoding?: string
164
+ },
91
165
  ): RequestInit & { duplex?: 'half' } {
92
- const headers = xrpcRequestHeaders(options)
166
+ const headers = buildAtprotoHeaders(options)
167
+
168
+ // Tell the server what type of response we're expecting
169
+ if (schema.output.encoding) {
170
+ headers.set('accept', schema.output.encoding)
171
+ }
172
+
173
+ // Caller should not set content-type header
174
+ if (headers.has('content-type')) {
175
+ const contentType = headers.get('content-type')
176
+ throw new TypeError(`Unexpected content-type header (${contentType})`)
177
+ }
93
178
 
94
179
  // 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
- )
180
+ if ('input' in schema) {
181
+ const encodingHint = options.encoding
182
+ const input = xrpcProcedureInput(schema, options, encodingHint)
183
+
184
+ if (input) {
185
+ headers.set('content-type', input.encoding)
186
+ } else if (encodingHint != null) {
187
+ throw new TypeError(`Unexpected encoding hint (${encodingHint})`)
104
188
  }
105
189
 
106
- headers.set('content-type', schema.input.encoding)
107
190
  return {
108
191
  duplex: 'half',
109
192
  redirect: 'follow',
@@ -112,12 +195,7 @@ export function xrpcRequestInit<T extends Procedure | Query>(
112
195
  signal: options.signal,
113
196
  method: 'POST',
114
197
  headers,
115
- body: xrpcRequestBody(
116
- schema.input?.encoding,
117
- options.validateRequest
118
- ? schema.input?.body.parse(options.body)
119
- : options.body,
120
- ),
198
+ body: input?.body,
121
199
  }
122
200
  }
123
201
 
@@ -128,208 +206,118 @@ export function xrpcRequestInit<T extends Procedure | Query>(
128
206
  referrerPolicy: 'strict-origin-when-cross-origin', // (default)
129
207
  mode: 'cors', // (default)
130
208
  signal: options.signal,
131
- method: schema instanceof Query ? 'GET' : 'POST',
209
+ method: 'GET',
132
210
  headers,
133
211
  }
134
212
  }
135
213
 
136
- export function xrpcRequestHeaders(options: {
137
- headers?: HeadersInit
138
- service?: Service
139
- labelers?: Iterable<DidString>
140
- }): Headers {
141
- const headers = new Headers(options.headers)
214
+ function xrpcProcedureInput(
215
+ method: Procedure,
216
+ options: CallOptions & { body?: LexValue | BinaryBodyInit },
217
+ encodingHint?: string,
218
+ ): null | Payload<BodyInit> {
219
+ const { input } = method
220
+ const { body } = options
142
221
 
143
- if (options.service && !headers.has('atproto-proxy')) {
144
- headers.set('atproto-proxy', options.service)
222
+ if (options.validateRequest) {
223
+ input.schema?.check(body)
145
224
  }
146
225
 
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
- )
154
- }
155
-
156
- return headers
157
- }
226
+ // Special handling for endpoints expecting application/json input
227
+ if (input.encoding === 'application/json') {
228
+ // @NOTE **NOT** using isLexValue here to avoid deep checks in order to
229
+ // distinguish between LexValue and BinaryBodyInit.
230
+ if (!isLexScalar(body) && !isPlainObject(body) && !Array.isArray(body)) {
231
+ throw new TypeError(`Expected LexValue body, got ${typeof body}`)
232
+ }
158
233
 
159
- function xrpcRequestBody(
160
- encoding: string | undefined,
161
- body: LexValue | undefined,
162
- ): BodyInit | null {
163
- if (encoding === undefined) {
164
- return null
234
+ return buildPayload(input, lexStringify(body), encodingHint)
165
235
  }
166
236
 
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
237
+ // Other encodings will be sent unaltered (ie. as binary data)
238
+ switch (typeof body) {
239
+ case 'undefined':
240
+ case 'string':
241
+ return buildPayload(input, body, encodingHint)
242
+ case 'object': {
243
+ if (body === null) break
244
+ if (
245
+ ArrayBuffer.isView(body) ||
246
+ body instanceof ArrayBuffer ||
247
+ body instanceof ReadableStream
248
+ ) {
249
+ return buildPayload(input, body, encodingHint)
250
+ } else if (isAsyncIterable(body)) {
251
+ return buildPayload(input, toReadableStream(body), encodingHint)
252
+ } else if (isBlobLike(body)) {
253
+ return buildPayload(input, body, encodingHint || body.type)
254
+ }
255
+ }
173
256
  }
174
257
 
175
- throw new TypeError(`Invalid ${typeof body} body for ${encoding} encoding`)
258
+ throw new TypeError(
259
+ `Invalid ${typeof body} body for ${input.encoding} encoding`,
260
+ )
176
261
  }
177
262
 
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,
263
+ function buildPayload(
264
+ schema: LexPayload,
265
+ body: undefined | BodyInit,
266
+ encodingHint?: string,
267
+ ): null | Payload<BodyInit> {
268
+ if (schema.encoding === undefined) {
269
+ if (body !== undefined) {
270
+ throw new TypeError(
271
+ `Cannot send a ${typeof body} body with undefined encoding`,
219
272
  )
220
273
  }
221
274
 
222
- throw new XrpcServiceError(
223
- response.status >= 500
224
- ? KnownError.InternalServerError
225
- : KnownError.InvalidResponse,
226
- response.status,
227
- response.headers,
228
- body,
229
- )
275
+ return null
230
276
  }
231
277
 
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
- )
278
+ if (body === undefined) {
279
+ // This error would be returned by the server, but we can catch it earlier
280
+ // to avoid un-necessary requests. Note that a content-length of 0 does not
281
+ // necessary mean that the body is "empty" (e.g. an empty txt file).
282
+ throw new TypeError(`A request body is expected but none was provided`)
241
283
  }
242
284
 
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
- }
285
+ const encoding = buildEncoding(schema, encodingHint)
286
+ return { encoding, body }
287
+ }
288
+
289
+ function buildEncoding(schema: LexPayload, encodingHint?: string): string {
290
+ // Should never happen (required for type safety)
291
+ if (!schema.encoding) {
292
+ throw new TypeError('Unexpected payload')
293
+ }
253
294
 
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`,
295
+ if (encodingHint?.length) {
296
+ if (!schema.matchesEncoding(encodingHint)) {
297
+ throw new TypeError(
298
+ `Cannot send a body with content-type "${encodingHint}" for "${schema.encoding}" encoding`,
269
299
  )
270
300
  }
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
- )
301
+ return encodingHint
280
302
  }
281
- }
282
-
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
- }
288
303
 
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
- }
304
+ // Fallback
317
305
 
318
- throw new SyntaxError('Content-type is undefined but body is not empty')
306
+ if (schema.encoding === '*/*') {
307
+ return 'application/octet-stream'
319
308
  }
320
309
 
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())
310
+ if (schema.encoding.startsWith('text/')) {
311
+ return schema.encoding.includes('*')
312
+ ? 'text/plain; charset=utf-8'
313
+ : `${schema.encoding}; charset=utf-8`
328
314
  }
329
315
 
330
- if (encoding.startsWith('text/')) {
331
- return response.text()
316
+ if (!schema.encoding.includes('*')) {
317
+ return schema.encoding
332
318
  }
333
319
 
334
- return new Uint8Array(await response.arrayBuffer())
320
+ throw new TypeError(
321
+ `Unable to determine payload encoding. Please provide a 'content-type' header matching ${schema.encoding}.`,
322
+ )
335
323
  }
@@ -1,12 +1,9 @@
1
1
  {
2
- "extends": "../../../tsconfig/tests.json",
3
- "include": ["./tests"],
2
+ "extends": "../../../tsconfig/vitest.json",
3
+ "include": ["./tests", "./src/**/*.test.ts"],
4
4
  "compilerOptions": {
5
5
  "noImplicitAny": true,
6
- "rootDir": "./tests",
7
- "baseUrl": "./tests",
8
- "paths": {
9
- "@atproto/lex-client": ["./dist/index.js"]
10
- }
6
+ "rootDir": "./",
7
+ "baseUrl": "./"
11
8
  }
12
9
  }
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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"}