@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.
- package/CHANGELOG.md +42 -0
- package/dist/agent.d.ts +10 -9
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +3 -0
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +51 -113
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +38 -42
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +82 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +132 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts +7 -7
- package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/createRecord.defs.js +8 -12
- package/dist/lexicons/com/atproto/repo/createRecord.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts +7 -7
- package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js +8 -12
- package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts +5 -6
- package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/getRecord.defs.js +6 -10
- package/dist/lexicons/com/atproto/repo/getRecord.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts +5 -6
- package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/listRecords.defs.js +5 -8
- package/dist/lexicons/com/atproto/repo/listRecords.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts +7 -7
- package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/putRecord.defs.js +8 -12
- package/dist/lexicons/com/atproto/repo/putRecord.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts +7 -7
- package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js +6 -9
- package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/sync/getBlob.d.ts +3 -0
- package/dist/lexicons/com/atproto/sync/getBlob.d.ts.map +1 -0
- package/dist/lexicons/com/atproto/sync/getBlob.defs.d.ts +25 -0
- package/dist/lexicons/com/atproto/sync/getBlob.defs.d.ts.map +1 -0
- package/dist/lexicons/com/atproto/sync/getBlob.defs.js +27 -0
- package/dist/lexicons/com/atproto/sync/getBlob.defs.js.map +1 -0
- package/dist/lexicons/com/atproto/sync/getBlob.js +10 -0
- package/dist/lexicons/com/atproto/sync/getBlob.js.map +1 -0
- package/dist/lexicons/com/atproto/sync.d.ts +2 -0
- package/dist/lexicons/com/atproto/sync.d.ts.map +1 -0
- package/dist/lexicons/com/atproto/sync.js +9 -0
- package/dist/lexicons/com/atproto/sync.js.map +1 -0
- package/dist/lexicons/com/atproto.d.ts +1 -0
- package/dist/lexicons/com/atproto.d.ts.map +1 -1
- package/dist/lexicons/com/atproto.js +2 -1
- package/dist/lexicons/com/atproto.js.map +1 -1
- package/dist/lexicons.d.ts +2 -0
- package/dist/lexicons.d.ts.map +1 -0
- package/dist/lexicons.js +6 -0
- package/dist/lexicons.js.map +1 -0
- package/dist/response.d.ts +25 -8
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +123 -10
- package/dist/response.js.map +1 -1
- package/dist/types.d.ts +18 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -4
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +14 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +65 -0
- package/dist/util.js.map +1 -0
- package/dist/xrpc.d.ts +35 -32
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +116 -124
- package/dist/xrpc.js.map +1 -1
- package/package.json +10 -10
- package/src/agent.ts +18 -14
- package/src/client.ts +135 -114
- package/src/errors.ts +206 -0
- package/src/index.ts +1 -1
- package/src/lexicons/com/atproto/repo/createRecord.defs.ts +31 -36
- package/src/lexicons/com/atproto/repo/deleteRecord.defs.ts +27 -32
- package/src/lexicons/com/atproto/repo/getRecord.defs.ts +12 -17
- package/src/lexicons/com/atproto/repo/listRecords.defs.ts +13 -15
- package/src/lexicons/com/atproto/repo/putRecord.defs.ts +32 -37
- package/src/lexicons/com/atproto/repo/uploadBlob.defs.ts +13 -15
- package/src/lexicons/com/atproto/sync/getBlob.defs.ts +37 -0
- package/src/lexicons/com/atproto/sync/getBlob.ts +6 -0
- package/src/lexicons/com/atproto/sync.ts +5 -0
- package/src/lexicons/com/atproto.ts +1 -0
- package/src/lexicons.ts +1 -0
- package/src/response.ts +201 -15
- package/src/types.ts +26 -5
- package/src/util.ts +84 -0
- package/src/xrpc.ts +220 -232
- package/tsconfig.tests.json +4 -7
- package/dist/error.d.ts +0 -66
- package/dist/error.d.ts.map +0 -1
- package/dist/error.js +0 -100
- package/dist/error.js.map +0 -1
- 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
|
-
|
|
3
|
-
|
|
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
|
|
10
|
-
|
|
16
|
+
export type LexRpcResponseBody<M extends Procedure | Query> =
|
|
17
|
+
InferMethodOutputBody<M, Uint8Array>
|
|
11
18
|
|
|
12
|
-
export type
|
|
13
|
-
M
|
|
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<
|
|
27
|
+
* @implements {ResultSuccess<LexRpcResponse<M>>} for convenience in result handling contexts.
|
|
20
28
|
*/
|
|
21
|
-
export class
|
|
22
|
-
implements ResultSuccess<
|
|
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
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
}
|