@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/response.ts CHANGED
@@ -1,25 +1,33 @@
1
+ import { lexParse } from '@atproto/lex-json'
1
2
  import {
2
- InferPayloadBody,
3
- InferPayloadEncoding,
3
+ InferMethodOutputBody,
4
+ InferMethodOutputEncoding,
4
5
  Procedure,
5
6
  Query,
6
7
  ResultSuccess,
7
8
  } from '@atproto/lex-schema'
9
+ import {
10
+ LexRpcResponseError,
11
+ LexRpcUpstreamError,
12
+ isLexRpcErrorPayload,
13
+ } from './errors.js'
14
+ import { Payload } from './util.js'
8
15
 
9
- export type XrpcResponseEncoding<M extends Procedure | Query> =
10
- InferPayloadEncoding<M['output']>
16
+ export type LexRpcResponseBody<M extends Procedure | Query> =
17
+ InferMethodOutputBody<M, Uint8Array>
11
18
 
12
- export type XrpcResponseBody<M extends Procedure | Query> = InferPayloadBody<
13
- M['output']
14
- >
19
+ export type LexRpcResponsePayload<M extends Procedure | Query> =
20
+ InferMethodOutputEncoding<M> extends infer E extends string
21
+ ? Payload<LexRpcResponseBody<M>, E>
22
+ : null
15
23
 
16
24
  /**
17
25
  * Small container for XRPC response data.
18
26
  *
19
- * @implements {ResultSuccess<XrpcResponse<M>>} for convenience in result handling contexts.
27
+ * @implements {ResultSuccess<LexRpcResponse<M>>} for convenience in result handling contexts.
20
28
  */
21
- export class XrpcResponse<M extends Procedure | Query>
22
- implements ResultSuccess<XrpcResponse<M>>
29
+ export class LexRpcResponse<const M extends Procedure | Query>
30
+ implements ResultSuccess<LexRpcResponse<M>>
23
31
  {
24
32
  /** @see {@link ResultSuccess.success} */
25
33
  readonly success = true as const
@@ -29,14 +37,192 @@ export class XrpcResponse<M extends Procedure | Query>
29
37
  return this
30
38
  }
31
39
 
32
- get encoding(): XrpcResponseEncoding<M> {
33
- return this.method.output?.encoding
34
- }
35
-
36
40
  constructor(
37
41
  readonly method: M,
38
42
  readonly status: number,
39
43
  readonly headers: Headers,
40
- readonly body: XrpcResponseBody<M>,
44
+ readonly payload: LexRpcResponsePayload<M>,
41
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 LexRpcResponseBody<M>
61
+ }
62
+
63
+ /**
64
+ * @throws {LexRpcResponseError} in case of (valid) XRPC error responses. Use
65
+ * {@link LexRpcResponseError.matchesSchema} to narrow the error type based on
66
+ * the method's declared error schema.
67
+ * @throws {LexRpcUpstreamError} when the response is not a valid XRPC
68
+ * response, or if the response does not conform to the method's schema.
69
+ */
70
+ static async fromFetchResponse<const M extends Procedure | Query>(
71
+ method: M,
72
+ response: Response,
73
+ options?: { validateResponse?: boolean },
74
+ ): Promise<LexRpcResponse<M>> {
75
+ // @NOTE The body MUST either be read or canceled to avoid resource leaks.
76
+ // Since nothing should cause an exception before "readPayload" is
77
+ // called, we can safely not use a try/finally here.
78
+
79
+ // @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
80
+ if (response.status < 200 || response.status >= 300) {
81
+ // Always parse json for error responses
82
+ const payload = await readPayload(response, { parse: true })
83
+
84
+ if (response.status >= 400 && isLexRpcErrorPayload(payload)) {
85
+ throw new LexRpcResponseError(
86
+ method,
87
+ response.status,
88
+ response.headers,
89
+ payload,
90
+ )
91
+ }
92
+
93
+ if (response.status >= 500) {
94
+ throw new LexRpcUpstreamError(
95
+ 'UpstreamFailure',
96
+ `Upstream server encountered an error`,
97
+ response,
98
+ payload,
99
+ )
100
+ }
101
+
102
+ throw new LexRpcUpstreamError(
103
+ 'InvalidResponse',
104
+ response.status >= 400
105
+ ? `Upstream server returned an invalid response payload`
106
+ : `Upstream server returned an invalid status code`,
107
+ response,
108
+ payload,
109
+ )
110
+ }
111
+
112
+ // Only parse json if the schema expects it
113
+ const payload = await readPayload(response, {
114
+ parse: shouldParse(method),
115
+ })
116
+
117
+ // Response is successful (2xx). Validate payload (data and encoding) against schema.
118
+ if (method.output.encoding == null) {
119
+ // Schema expects no payload
120
+ if (payload) {
121
+ throw new LexRpcUpstreamError(
122
+ 'InvalidResponse',
123
+ `Expected response with no body, got ${payload.encoding}`,
124
+ response,
125
+ payload,
126
+ )
127
+ }
128
+ } else {
129
+ // Schema expects a payload
130
+ if (!payload || !method.output.matchesEncoding(payload.encoding)) {
131
+ throw new LexRpcUpstreamError(
132
+ 'InvalidResponse',
133
+ payload
134
+ ? `Expected ${method.output.encoding} response, got ${payload.encoding}`
135
+ : `Expected non-empty response with content-type ${method.output.encoding}`,
136
+ response,
137
+ payload,
138
+ )
139
+ }
140
+
141
+ // Assert valid response body.
142
+ if (method.output.schema && options?.validateResponse !== false) {
143
+ const result = method.output.schema.safeParse(payload.body, {
144
+ allowTransform: false,
145
+ })
146
+
147
+ if (!result.success) {
148
+ throw new LexRpcUpstreamError(
149
+ 'InvalidResponse',
150
+ `Response validation failed: ${result.reason.message}`,
151
+ response,
152
+ payload,
153
+ { cause: result.reason },
154
+ )
155
+ }
156
+ }
157
+ }
158
+
159
+ return new LexRpcResponse<M>(
160
+ method,
161
+ response.status,
162
+ response.headers,
163
+ payload as LexRpcResponsePayload<M>,
164
+ )
165
+ }
166
+ }
167
+
168
+ function shouldParse(method: Procedure | Query) {
169
+ return method.output.encoding === 'application/json'
170
+ }
171
+
172
+ /**
173
+ * @note this function always consumes the response body
174
+ */
175
+ async function readPayload(
176
+ response: Response,
177
+ options?: { parse?: boolean },
178
+ ): Promise<Payload | null> {
179
+ // @TODO Should we limit the maximum response size here (this could also be
180
+ // done by the FetchHandler)?
181
+
182
+ const encoding = response.headers
183
+ .get('content-type')
184
+ ?.split(';')[0]
185
+ .trim()
186
+ .toLowerCase()
187
+
188
+ // Response content-type is undefined
189
+ if (!encoding) {
190
+ // If the body is empty, return null (= no payload)
191
+ const body = await response.arrayBuffer()
192
+ if (body.byteLength === 0) return null
193
+
194
+ // If we got data despite no content-type, treat it as binary
195
+ return {
196
+ encoding: 'application/octet-stream',
197
+ body: new Uint8Array(body),
198
+ }
199
+ }
200
+
201
+ if (options?.parse && encoding === 'application/json') {
202
+ // @NOTE It might be worth returning the raw bytes here (Uint8Array) and
203
+ // perform the lex parsing using cborg/json, allowing to do
204
+ // bytes->LexValue in one step instead of bytes->text->JSON->LexValue.
205
+ // This would require adding encode/decode utilities to lex-json (similar
206
+ // to @ipld/dag-json)
207
+ const text = await response.text()
208
+
209
+ try {
210
+ // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as
211
+ // using a reviver function during JSON.parse should be faster than
212
+ // parsing to JSON then converting to Lex (?)
213
+
214
+ // @TODO verify statement above
215
+ return { encoding, body: lexParse(text) }
216
+ } catch (cause) {
217
+ throw new LexRpcUpstreamError(
218
+ 'InvalidResponse',
219
+ 'Invalid JSON response body',
220
+ response,
221
+ null,
222
+ { cause },
223
+ )
224
+ }
225
+ }
226
+
227
+ return { encoding, body: new Uint8Array(await response.arrayBuffer()) }
42
228
  }
package/src/types.ts CHANGED
@@ -10,12 +10,33 @@ 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
 
17
- export type Namespace<T> = T | { main: T }
18
-
19
- export function getMain<T extends object>(ns: Namespace<T>): T {
20
- return 'main' in ns ? ns.main : ns
21
- }
36
+ export type BinaryBodyInit =
37
+ | Uint8Array
38
+ | ArrayBuffer
39
+ | Blob
40
+ | ReadableStream<Uint8Array>
41
+ | AsyncIterable<Uint8Array>
42
+ | string
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
+ }