@atproto/xrpc 0.4.3 → 0.5.1-rc.0
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 +11 -0
- package/README.md +56 -31
- package/dist/client.d.ts +11 -6
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +83 -0
- package/dist/client.js.map +1 -0
- package/dist/fetch-handler.d.ts +33 -0
- package/dist/fetch-handler.d.ts.map +1 -0
- package/dist/fetch-handler.js +24 -0
- package/dist/fetch-handler.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -15322
- package/dist/index.js.map +1 -7
- package/dist/types.d.ts +17 -12
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +186 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +9 -5
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +297 -0
- package/dist/util.js.map +1 -0
- package/dist/xrpc-client.d.ts +10 -0
- package/dist/xrpc-client.d.ts.map +1 -0
- package/dist/xrpc-client.js +79 -0
- package/dist/xrpc-client.js.map +1 -0
- package/package.json +9 -8
- package/src/client.ts +30 -117
- package/src/fetch-handler.ts +73 -0
- package/src/index.ts +5 -1
- package/src/types.ts +77 -24
- package/src/util.ts +284 -84
- package/src/xrpc-client.ts +107 -0
- package/tsconfig.build.json +6 -2
- package/tsconfig.json +2 -11
- package/babel.config.js +0 -1
- package/build.js +0 -14
package/src/util.ts
CHANGED
|
@@ -6,12 +6,28 @@ import {
|
|
|
6
6
|
} from '@atproto/lexicon'
|
|
7
7
|
import {
|
|
8
8
|
CallOptions,
|
|
9
|
-
|
|
9
|
+
errorResponseBody,
|
|
10
|
+
ErrorResponseBody,
|
|
11
|
+
Gettable,
|
|
10
12
|
QueryParams,
|
|
11
13
|
ResponseType,
|
|
12
14
|
XRPCError,
|
|
13
15
|
} from './types'
|
|
14
16
|
|
|
17
|
+
const ReadableStream =
|
|
18
|
+
globalThis.ReadableStream ||
|
|
19
|
+
(class {
|
|
20
|
+
constructor() {
|
|
21
|
+
// This anonymous class will never pass any "instanceof" check and cannot
|
|
22
|
+
// be instantiated.
|
|
23
|
+
throw new Error('ReadableStream is not supported in this environment')
|
|
24
|
+
}
|
|
25
|
+
} as typeof globalThis.ReadableStream)
|
|
26
|
+
|
|
27
|
+
export function isErrorResponseBody(v: unknown): v is ErrorResponseBody {
|
|
28
|
+
return errorResponseBody.safeParse(v).success
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
export function getMethodSchemaHTTPMethod(
|
|
16
32
|
schema: LexXrpcProcedure | LexXrpcQuery,
|
|
17
33
|
) {
|
|
@@ -27,33 +43,43 @@ export function constructMethodCallUri(
|
|
|
27
43
|
serviceUri: URL,
|
|
28
44
|
params?: QueryParams,
|
|
29
45
|
): string {
|
|
30
|
-
const uri = new URL(serviceUri)
|
|
31
|
-
uri.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
const uri = new URL(constructMethodCallUrl(nsid, schema, params), serviceUri)
|
|
47
|
+
return uri.toString()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function constructMethodCallUrl(
|
|
51
|
+
nsid: string,
|
|
52
|
+
schema: LexXrpcProcedure | LexXrpcQuery,
|
|
53
|
+
params?: QueryParams,
|
|
54
|
+
): string {
|
|
55
|
+
const pathname = `/xrpc/${encodeURIComponent(nsid)}`
|
|
56
|
+
if (!params) return pathname
|
|
57
|
+
|
|
58
|
+
const searchParams: [string, string][] = []
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(params)) {
|
|
61
|
+
const paramSchema = schema.parameters?.properties?.[key]
|
|
62
|
+
if (!paramSchema) {
|
|
63
|
+
throw new Error(`Invalid query parameter: ${key}`)
|
|
64
|
+
}
|
|
65
|
+
if (value !== undefined) {
|
|
66
|
+
if (paramSchema.type === 'array') {
|
|
67
|
+
const values = Array.isArray(value) ? value : [value]
|
|
68
|
+
for (const val of values) {
|
|
69
|
+
searchParams.push([
|
|
70
|
+
key,
|
|
71
|
+
encodeQueryParam(paramSchema.items.type, val),
|
|
72
|
+
])
|
|
51
73
|
}
|
|
74
|
+
} else {
|
|
75
|
+
searchParams.push([key, encodeQueryParam(paramSchema.type, value)])
|
|
52
76
|
}
|
|
53
77
|
}
|
|
54
78
|
}
|
|
55
79
|
|
|
56
|
-
|
|
80
|
+
if (!searchParams.length) return pathname
|
|
81
|
+
|
|
82
|
+
return `${pathname}?${new URLSearchParams(searchParams).toString()}`
|
|
57
83
|
}
|
|
58
84
|
|
|
59
85
|
export function encodeQueryParam(
|
|
@@ -85,100 +111,274 @@ export function encodeQueryParam(
|
|
|
85
111
|
throw new Error(`Unsupported query param type: ${type}`)
|
|
86
112
|
}
|
|
87
113
|
|
|
88
|
-
export function normalizeHeaders(headers: Headers): Headers {
|
|
89
|
-
const normalized: Headers = {}
|
|
90
|
-
for (const [header, value] of Object.entries(headers)) {
|
|
91
|
-
normalized[header.toLowerCase()] = value
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return normalized
|
|
95
|
-
}
|
|
96
|
-
|
|
97
114
|
export function constructMethodCallHeaders(
|
|
98
115
|
schema: LexXrpcProcedure | LexXrpcQuery,
|
|
99
|
-
data?:
|
|
116
|
+
data?: unknown,
|
|
100
117
|
opts?: CallOptions,
|
|
101
118
|
): Headers {
|
|
102
|
-
|
|
119
|
+
// Not using `new Headers(opts?.headers)` to avoid duplicating headers values
|
|
120
|
+
// due to inconsistent casing in headers name. In case of multiple headers
|
|
121
|
+
// with the same name (but using a different case), the last one will be used.
|
|
122
|
+
|
|
123
|
+
// new Headers({ 'content-type': 'foo', 'Content-Type': 'bar' }).get('content-type')
|
|
124
|
+
// => 'foo, bar'
|
|
125
|
+
const headers = new Headers()
|
|
126
|
+
|
|
127
|
+
if (opts?.headers) {
|
|
128
|
+
for (const name in opts.headers) {
|
|
129
|
+
if (headers.has(name)) {
|
|
130
|
+
throw new TypeError(`Duplicate header: ${name}`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const value = opts.headers[name]
|
|
134
|
+
if (value != null) {
|
|
135
|
+
headers.set(name, value)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
103
140
|
if (schema.type === 'procedure') {
|
|
104
141
|
if (opts?.encoding) {
|
|
105
|
-
headers
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
142
|
+
headers.set('content-type', opts.encoding)
|
|
143
|
+
} else if (!headers.has('content-type') && typeof data !== 'undefined') {
|
|
144
|
+
// Special handling of BodyInit types before falling back to JSON encoding
|
|
145
|
+
if (
|
|
146
|
+
data instanceof ArrayBuffer ||
|
|
147
|
+
data instanceof ReadableStream ||
|
|
148
|
+
ArrayBuffer.isView(data)
|
|
149
|
+
) {
|
|
150
|
+
headers.set('content-type', 'application/octet-stream')
|
|
151
|
+
} else if (data instanceof FormData) {
|
|
152
|
+
// Note: The multipart form data boundary is missing from the header
|
|
153
|
+
// we set here, making that header invalid. This special case will be
|
|
154
|
+
// handled in encodeMethodCallBody()
|
|
155
|
+
headers.set('content-type', 'multipart/form-data')
|
|
156
|
+
} else if (data instanceof URLSearchParams) {
|
|
157
|
+
headers.set(
|
|
158
|
+
'content-type',
|
|
159
|
+
'application/x-www-form-urlencoded;charset=UTF-8',
|
|
160
|
+
)
|
|
161
|
+
} else if (isBlobLike(data)) {
|
|
162
|
+
headers.set('content-type', data.type || 'application/octet-stream')
|
|
163
|
+
} else if (typeof data === 'string') {
|
|
164
|
+
headers.set('content-type', 'text/plain;charset=UTF-8')
|
|
165
|
+
}
|
|
166
|
+
// At this point, data is not a valid BodyInit type.
|
|
167
|
+
else if (isIterable(data)) {
|
|
168
|
+
headers.set('content-type', 'application/octet-stream')
|
|
169
|
+
} else if (
|
|
170
|
+
typeof data === 'boolean' ||
|
|
171
|
+
typeof data === 'number' ||
|
|
172
|
+
typeof data === 'string' ||
|
|
173
|
+
typeof data === 'object' // covers "null"
|
|
174
|
+
) {
|
|
175
|
+
headers.set('content-type', 'application/json')
|
|
176
|
+
} else {
|
|
177
|
+
// symbol, function, bigint
|
|
178
|
+
throw new XRPCError(
|
|
179
|
+
ResponseType.InvalidRequest,
|
|
180
|
+
`Unsupported data type: ${typeof data}`,
|
|
181
|
+
)
|
|
110
182
|
}
|
|
111
183
|
}
|
|
112
184
|
}
|
|
113
185
|
return headers
|
|
114
186
|
}
|
|
115
187
|
|
|
188
|
+
export function combineHeaders(
|
|
189
|
+
headersInit: undefined | HeadersInit,
|
|
190
|
+
defaultHeaders?: Iterable<[string, undefined | Gettable<null | string>]>,
|
|
191
|
+
): undefined | HeadersInit {
|
|
192
|
+
if (!defaultHeaders) return headersInit
|
|
193
|
+
|
|
194
|
+
let headers: Headers | undefined = undefined
|
|
195
|
+
|
|
196
|
+
for (const [name, definition] of defaultHeaders) {
|
|
197
|
+
// Ignore undefined values (allowed for convenience when using
|
|
198
|
+
// Object.entries).
|
|
199
|
+
if (definition === undefined) continue
|
|
200
|
+
|
|
201
|
+
// Lazy initialization of the headers object
|
|
202
|
+
headers ??= new Headers(headersInit)
|
|
203
|
+
|
|
204
|
+
if (headers.has(name)) continue
|
|
205
|
+
|
|
206
|
+
const value = typeof definition === 'function' ? definition() : definition
|
|
207
|
+
|
|
208
|
+
if (typeof value === 'string') headers.set(name, value)
|
|
209
|
+
else if (value === null) headers.delete(name)
|
|
210
|
+
else throw new TypeError(`Invalid "${name}" header value: ${typeof value}`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return headers ?? headersInit
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isBlobLike(value: unknown): value is Blob {
|
|
217
|
+
if (value == null) return false
|
|
218
|
+
if (typeof value !== 'object') return false
|
|
219
|
+
if (typeof Blob === 'function' && value instanceof Blob) return true
|
|
220
|
+
|
|
221
|
+
// Support for Blobs provided by libraries that don't use the native Blob
|
|
222
|
+
// (e.g. fetch-blob from node-fetch).
|
|
223
|
+
// https://github.com/node-fetch/fetch-blob/blob/a1a182e5978811407bef4ea1632b517567dda01f/index.js#L233-L244
|
|
224
|
+
|
|
225
|
+
const tag = value[Symbol.toStringTag]
|
|
226
|
+
if (tag === 'Blob' || tag === 'File') {
|
|
227
|
+
return 'stream' in value && typeof value.stream === 'function'
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function isBodyInit(value: unknown): value is BodyInit {
|
|
234
|
+
switch (typeof value) {
|
|
235
|
+
case 'string':
|
|
236
|
+
return true
|
|
237
|
+
case 'object':
|
|
238
|
+
return (
|
|
239
|
+
value instanceof ArrayBuffer ||
|
|
240
|
+
value instanceof FormData ||
|
|
241
|
+
value instanceof URLSearchParams ||
|
|
242
|
+
value instanceof ReadableStream ||
|
|
243
|
+
ArrayBuffer.isView(value) ||
|
|
244
|
+
isBlobLike(value)
|
|
245
|
+
)
|
|
246
|
+
default:
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function isIterable(
|
|
252
|
+
value: unknown,
|
|
253
|
+
): value is Iterable<unknown> | AsyncIterable<unknown> {
|
|
254
|
+
return (
|
|
255
|
+
value != null &&
|
|
256
|
+
typeof value === 'object' &&
|
|
257
|
+
(Symbol.iterator in value || Symbol.asyncIterator in value)
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
116
261
|
export function encodeMethodCallBody(
|
|
117
262
|
headers: Headers,
|
|
118
|
-
data?:
|
|
119
|
-
):
|
|
120
|
-
|
|
263
|
+
data?: unknown,
|
|
264
|
+
): BodyInit | undefined {
|
|
265
|
+
// Silently ignore the body if there is no content-type header.
|
|
266
|
+
const contentType = headers.get('content-type')
|
|
267
|
+
if (!contentType) {
|
|
121
268
|
return undefined
|
|
122
269
|
}
|
|
123
|
-
|
|
270
|
+
|
|
271
|
+
if (typeof data === 'undefined') {
|
|
272
|
+
// This error would be returned by the server, but we can catch it earlier
|
|
273
|
+
// to avoid un-necessary requests. Note that a content-length of 0 does not
|
|
274
|
+
// necessary mean that the body is "empty" (e.g. an empty txt file).
|
|
275
|
+
throw new XRPCError(
|
|
276
|
+
ResponseType.InvalidRequest,
|
|
277
|
+
`A request body is expected but none was provided`,
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (isBodyInit(data)) {
|
|
282
|
+
if (data instanceof FormData && contentType === 'multipart/form-data') {
|
|
283
|
+
// fetch() will encode FormData payload itself, but it won't override the
|
|
284
|
+
// content-type header if already present. This would cause the boundary
|
|
285
|
+
// to be missing from the content-type header, resulting in a 400 error.
|
|
286
|
+
// Deleting the content-type header here to let fetch() re-create it.
|
|
287
|
+
headers.delete('content-type')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Will be encoded by the fetch API.
|
|
124
291
|
return data
|
|
125
292
|
}
|
|
126
|
-
|
|
127
|
-
|
|
293
|
+
|
|
294
|
+
if (isIterable(data)) {
|
|
295
|
+
// Note that some environments support using Iterable & AsyncIterable as the
|
|
296
|
+
// body (e.g. Node's fetch), but not all of them do (browsers).
|
|
297
|
+
return iterableToReadableStream(data)
|
|
128
298
|
}
|
|
129
|
-
|
|
130
|
-
|
|
299
|
+
|
|
300
|
+
if (contentType.startsWith('text/')) {
|
|
301
|
+
return new TextEncoder().encode(String(data))
|
|
302
|
+
}
|
|
303
|
+
if (contentType.startsWith('application/json')) {
|
|
304
|
+
const json = stringifyLex(data)
|
|
305
|
+
// Server would return a 400 error if the JSON is invalid (e.g. trying to
|
|
306
|
+
// JSONify a function, or an object that implements toJSON() poorly).
|
|
307
|
+
if (json === undefined) {
|
|
308
|
+
throw new XRPCError(
|
|
309
|
+
ResponseType.InvalidRequest,
|
|
310
|
+
`Failed to encode request body as JSON`,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
return new TextEncoder().encode(json)
|
|
131
314
|
}
|
|
132
|
-
|
|
315
|
+
|
|
316
|
+
// At this point, "data" is not a valid BodyInit value, and we don't know how
|
|
317
|
+
// to encode it into one. Passing it to fetch would result in an error. Let's
|
|
318
|
+
// throw our own error instead.
|
|
319
|
+
|
|
320
|
+
const type =
|
|
321
|
+
!data || typeof data !== 'object'
|
|
322
|
+
? typeof data
|
|
323
|
+
: data.constructor !== Object &&
|
|
324
|
+
typeof data.constructor === 'function' &&
|
|
325
|
+
typeof data.constructor?.name === 'string'
|
|
326
|
+
? data.constructor.name
|
|
327
|
+
: 'object'
|
|
328
|
+
|
|
329
|
+
throw new XRPCError(
|
|
330
|
+
ResponseType.InvalidRequest,
|
|
331
|
+
`Unable to encode ${type} as ${contentType} data`,
|
|
332
|
+
)
|
|
133
333
|
}
|
|
134
334
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
335
|
+
/**
|
|
336
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/from_static}
|
|
337
|
+
*/
|
|
338
|
+
function iterableToReadableStream(
|
|
339
|
+
iterable: Iterable<unknown> | AsyncIterable<unknown>,
|
|
340
|
+
): ReadableStream<Uint8Array> {
|
|
341
|
+
// Use the native ReadableStream.from() if available.
|
|
342
|
+
if ('from' in ReadableStream && typeof ReadableStream.from === 'function') {
|
|
343
|
+
return ReadableStream.from(iterable)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// If you see this error, consider using a polyfill for ReadableStream. For
|
|
347
|
+
// example, the "web-streams-polyfill" package:
|
|
348
|
+
// https://github.com/MattiasBuelens/web-streams-polyfill
|
|
349
|
+
|
|
350
|
+
throw new TypeError(
|
|
351
|
+
'ReadableStream.from() is not supported in this environment. ' +
|
|
352
|
+
'It is required to support using iterables as the request body. ' +
|
|
353
|
+
'Consider using a polyfill or re-write your code to use a different body type.',
|
|
354
|
+
)
|
|
151
355
|
}
|
|
152
356
|
|
|
153
357
|
export function httpResponseBodyParse(
|
|
154
358
|
mimeType: string | null,
|
|
155
359
|
data: ArrayBuffer | undefined,
|
|
156
360
|
): any {
|
|
157
|
-
|
|
158
|
-
if (mimeType
|
|
159
|
-
|
|
361
|
+
try {
|
|
362
|
+
if (mimeType) {
|
|
363
|
+
if (mimeType.includes('application/json')) {
|
|
160
364
|
const str = new TextDecoder().decode(data)
|
|
161
365
|
return jsonStringToLex(str)
|
|
162
|
-
} catch (e) {
|
|
163
|
-
throw new XRPCError(
|
|
164
|
-
ResponseType.InvalidResponse,
|
|
165
|
-
`Failed to parse response body: ${String(e)}`,
|
|
166
|
-
)
|
|
167
366
|
}
|
|
168
|
-
|
|
169
|
-
if (mimeType.startsWith('text/') && data?.byteLength) {
|
|
170
|
-
try {
|
|
367
|
+
if (mimeType.startsWith('text/')) {
|
|
171
368
|
return new TextDecoder().decode(data)
|
|
172
|
-
} catch (e) {
|
|
173
|
-
throw new XRPCError(
|
|
174
|
-
ResponseType.InvalidResponse,
|
|
175
|
-
`Failed to parse response body: ${String(e)}`,
|
|
176
|
-
)
|
|
177
369
|
}
|
|
178
370
|
}
|
|
371
|
+
if (data instanceof ArrayBuffer) {
|
|
372
|
+
return new Uint8Array(data)
|
|
373
|
+
}
|
|
374
|
+
return data
|
|
375
|
+
} catch (cause) {
|
|
376
|
+
throw new XRPCError(
|
|
377
|
+
ResponseType.InvalidResponse,
|
|
378
|
+
undefined,
|
|
379
|
+
`Failed to parse response body: ${String(cause)}`,
|
|
380
|
+
undefined,
|
|
381
|
+
{ cause },
|
|
382
|
+
)
|
|
179
383
|
}
|
|
180
|
-
if (data instanceof ArrayBuffer) {
|
|
181
|
-
return new Uint8Array(data)
|
|
182
|
-
}
|
|
183
|
-
return data
|
|
184
384
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { LexiconDoc, Lexicons, ValidationError } from '@atproto/lexicon'
|
|
2
|
+
import {
|
|
3
|
+
FetchHandler,
|
|
4
|
+
FetchHandlerOptions,
|
|
5
|
+
buildFetchHandler,
|
|
6
|
+
} from './fetch-handler'
|
|
7
|
+
import {
|
|
8
|
+
CallOptions,
|
|
9
|
+
QueryParams,
|
|
10
|
+
ResponseType,
|
|
11
|
+
XRPCError,
|
|
12
|
+
XRPCInvalidResponseError,
|
|
13
|
+
XRPCResponse,
|
|
14
|
+
httpResponseCodeToEnum,
|
|
15
|
+
} from './types'
|
|
16
|
+
import {
|
|
17
|
+
constructMethodCallHeaders,
|
|
18
|
+
constructMethodCallUrl,
|
|
19
|
+
encodeMethodCallBody,
|
|
20
|
+
getMethodSchemaHTTPMethod,
|
|
21
|
+
httpResponseBodyParse,
|
|
22
|
+
isErrorResponseBody,
|
|
23
|
+
} from './util'
|
|
24
|
+
|
|
25
|
+
export class XrpcClient {
|
|
26
|
+
readonly fetchHandler: FetchHandler
|
|
27
|
+
readonly lex: Lexicons
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
fetchHandlerOpts: FetchHandler | FetchHandlerOptions,
|
|
31
|
+
// "Lexicons" is redundant here (because that class implements
|
|
32
|
+
// "Iterable<LexiconDoc>") but we keep it for explicitness:
|
|
33
|
+
lex: Lexicons | Iterable<LexiconDoc>,
|
|
34
|
+
) {
|
|
35
|
+
this.fetchHandler = buildFetchHandler(fetchHandlerOpts)
|
|
36
|
+
|
|
37
|
+
this.lex = lex instanceof Lexicons ? lex : new Lexicons(lex)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async call(
|
|
41
|
+
methodNsid: string,
|
|
42
|
+
params?: QueryParams,
|
|
43
|
+
data?: unknown,
|
|
44
|
+
opts?: CallOptions,
|
|
45
|
+
): Promise<XRPCResponse> {
|
|
46
|
+
const def = this.lex.getDefOrThrow(methodNsid)
|
|
47
|
+
if (!def || (def.type !== 'query' && def.type !== 'procedure')) {
|
|
48
|
+
throw new TypeError(
|
|
49
|
+
`Invalid lexicon: ${methodNsid}. Must be a query or procedure.`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// @TODO: should we validate the params and data here?
|
|
54
|
+
// this.lex.assertValidXrpcParams(methodNsid, params)
|
|
55
|
+
// if (data !== undefined) {
|
|
56
|
+
// this.lex.assertValidXrpcInput(methodNsid, data)
|
|
57
|
+
// }
|
|
58
|
+
|
|
59
|
+
const reqUrl = constructMethodCallUrl(methodNsid, def, params)
|
|
60
|
+
const reqMethod = getMethodSchemaHTTPMethod(def)
|
|
61
|
+
const reqHeaders = constructMethodCallHeaders(def, data, opts)
|
|
62
|
+
const reqBody = encodeMethodCallBody(reqHeaders, data)
|
|
63
|
+
|
|
64
|
+
// The duplex field is required for streaming bodies, but not yet reflected
|
|
65
|
+
// anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221.
|
|
66
|
+
const init: RequestInit & { duplex: 'half' } = {
|
|
67
|
+
method: reqMethod,
|
|
68
|
+
headers: reqHeaders,
|
|
69
|
+
body: reqBody,
|
|
70
|
+
duplex: 'half',
|
|
71
|
+
signal: opts?.signal,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const response = await this.fetchHandler.call(undefined, reqUrl, init)
|
|
76
|
+
|
|
77
|
+
const resStatus = response.status
|
|
78
|
+
const resHeaders = Object.fromEntries(response.headers.entries())
|
|
79
|
+
const resBodyBytes = await response.arrayBuffer()
|
|
80
|
+
const resBody = httpResponseBodyParse(
|
|
81
|
+
response.headers.get('content-type'),
|
|
82
|
+
resBodyBytes,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const resCode = httpResponseCodeToEnum(resStatus)
|
|
86
|
+
if (resCode !== ResponseType.Success) {
|
|
87
|
+
const { error = undefined, message = undefined } =
|
|
88
|
+
resBody && isErrorResponseBody(resBody) ? resBody : {}
|
|
89
|
+
throw new XRPCError(resCode, error, message, resHeaders)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
this.lex.assertValidXrpcOutput(methodNsid, resBody)
|
|
94
|
+
} catch (e: unknown) {
|
|
95
|
+
if (e instanceof ValidationError) {
|
|
96
|
+
throw new XRPCInvalidResponseError(methodNsid, e, resBody)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw e
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new XRPCResponse(resBody, resHeaders)
|
|
103
|
+
} catch (err) {
|
|
104
|
+
throw XRPCError.from(err)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
package/tsconfig.build.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"rootDir": "./src",
|
|
5
|
-
"outDir": "./dist", // Your outDir,
|
|
6
|
-
"emitDeclarationOnly": true
|
|
7
|
-
},
|
|
8
|
-
"include": ["./src", "__tests__/**/**.ts"],
|
|
9
|
-
"references": [
|
|
10
|
-
{ "path": "../common/tsconfig.build.json" },
|
|
11
|
-
{ "path": "../lexicon/tsconfig.build.json" }
|
|
12
|
-
]
|
|
2
|
+
"include": [],
|
|
3
|
+
"references": [{ "path": "./tsconfig.build.json" }]
|
|
13
4
|
}
|
package/babel.config.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = require('../../babel.config.js')
|
package/build.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const { nodeExternalsPlugin } = require('esbuild-node-externals')
|
|
2
|
-
|
|
3
|
-
const buildShallow =
|
|
4
|
-
process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true'
|
|
5
|
-
|
|
6
|
-
require('esbuild').build({
|
|
7
|
-
logLevel: 'info',
|
|
8
|
-
entryPoints: ['src/index.ts'],
|
|
9
|
-
bundle: true,
|
|
10
|
-
sourcemap: true,
|
|
11
|
-
outdir: 'dist',
|
|
12
|
-
platform: 'node',
|
|
13
|
-
plugins: buildShallow ? [nodeExternalsPlugin()] : [],
|
|
14
|
-
})
|