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