@atproto/xrpc-server 0.1.0 → 0.3.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/dist/auth.d.ts +15 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +37873 -26206
- package/dist/index.js.map +4 -4
- package/dist/src/index.d.ts +2 -0
- package/dist/src/logger.d.ts +2 -0
- package/dist/src/server.d.ts +19 -0
- package/dist/src/types.d.ts +115 -0
- package/dist/src/util.d.ts +10 -0
- package/dist/stream/server.d.ts +1 -1
- package/dist/stream/types.d.ts +6 -6
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/types.d.ts +10 -6
- package/dist/util.d.ts +15 -0
- package/package.json +10 -4
- package/src/auth.ts +111 -0
- package/src/index.ts +4 -0
- package/src/server.ts +18 -3
- package/src/stream/server.ts +15 -10
- package/src/types.ts +2 -0
- package/src/util.ts +33 -0
- package/tests/auth.test.ts +2 -2
- package/tests/bodies.test.ts +6 -6
- package/tests/errors.test.ts +29 -8
- package/tests/parameters.test.ts +1 -42
- package/tests/procedures.test.ts +12 -12
- package/tests/queries.test.ts +19 -14
- package/tests/responses.test.ts +77 -0
- package/tests/subscriptions.test.ts +14 -14
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.json +1 -0
package/src/stream/server.ts
CHANGED
|
@@ -12,20 +12,24 @@ export class XrpcStreamServer {
|
|
|
12
12
|
this.wss.on('connection', async (socket, req) => {
|
|
13
13
|
socket.on('error', (err) => logger.error(err, 'websocket error'))
|
|
14
14
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
const ac = new AbortController()
|
|
16
|
+
const iterator = unwrapIterator(handler(req, ac.signal, socket, this))
|
|
17
|
+
socket.once('close', () => {
|
|
18
|
+
iterator.return?.()
|
|
19
|
+
ac.abort()
|
|
20
|
+
})
|
|
17
21
|
const safeFrames = wrapIterator(iterator)
|
|
18
22
|
for await (const frame of safeFrames) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
await new Promise((res, rej) => {
|
|
24
|
+
socket.send(frame.toBytes(), { binary: true }, (err) => {
|
|
25
|
+
// @TODO this callback may give more aggressive on backpressure than
|
|
26
|
+
// we ultimately want, but trying it out for the time being.
|
|
27
|
+
if (err) return rej(err)
|
|
28
|
+
res(undefined)
|
|
25
29
|
})
|
|
30
|
+
})
|
|
31
|
+
if (frame instanceof ErrorFrame) {
|
|
26
32
|
throw new DisconnectError(CloseCode.Policy, frame.body.error)
|
|
27
|
-
} else {
|
|
28
|
-
socket.send(frame.toBytes(), { binary: true })
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
} catch (err) {
|
|
@@ -43,6 +47,7 @@ export class XrpcStreamServer {
|
|
|
43
47
|
|
|
44
48
|
export type Handler = (
|
|
45
49
|
req: IncomingMessage,
|
|
50
|
+
signal: AbortSignal,
|
|
46
51
|
socket: WebSocket,
|
|
47
52
|
server: XrpcStreamServer,
|
|
48
53
|
) => AsyncIterable<Frame>
|
package/src/types.ts
CHANGED
|
@@ -37,6 +37,7 @@ export type HandlerAuth = zod.infer<typeof handlerAuth>
|
|
|
37
37
|
export const handlerSuccess = zod.object({
|
|
38
38
|
encoding: zod.string(),
|
|
39
39
|
body: zod.any(),
|
|
40
|
+
headers: zod.record(zod.string()).optional(),
|
|
40
41
|
})
|
|
41
42
|
export type HandlerSuccess = zod.infer<typeof handlerSuccess>
|
|
42
43
|
|
|
@@ -61,6 +62,7 @@ export type XRPCStreamHandler = (ctx: {
|
|
|
61
62
|
auth: HandlerAuth | undefined
|
|
62
63
|
params: Params
|
|
63
64
|
req: IncomingMessage
|
|
65
|
+
signal: AbortSignal
|
|
64
66
|
}) => AsyncIterable<unknown>
|
|
65
67
|
|
|
66
68
|
export type AuthOutput = HandlerAuth | HandlerError
|
package/src/util.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
1
2
|
import { Readable, Transform } from 'stream'
|
|
2
3
|
import { createDeflate, createGunzip } from 'zlib'
|
|
3
4
|
import express from 'express'
|
|
@@ -266,3 +267,35 @@ function decodeBodyStream(
|
|
|
266
267
|
|
|
267
268
|
return stream
|
|
268
269
|
}
|
|
270
|
+
|
|
271
|
+
export function serverTimingHeader(timings: ServerTiming[]) {
|
|
272
|
+
return timings
|
|
273
|
+
.map((timing) => {
|
|
274
|
+
let header = timing.name
|
|
275
|
+
if (timing.duration) header += `;dur=${timing.duration}`
|
|
276
|
+
if (timing.description) header += `;desc="${timing.description}"`
|
|
277
|
+
return header
|
|
278
|
+
})
|
|
279
|
+
.join(', ')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export class ServerTimer implements ServerTiming {
|
|
283
|
+
public duration?: number
|
|
284
|
+
private startMs?: number
|
|
285
|
+
constructor(public name: string, public description?: string) {}
|
|
286
|
+
start() {
|
|
287
|
+
this.startMs = Date.now()
|
|
288
|
+
return this
|
|
289
|
+
}
|
|
290
|
+
stop() {
|
|
291
|
+
assert(this.startMs, "timer hasn't been started")
|
|
292
|
+
this.duration = Date.now() - this.startMs
|
|
293
|
+
return this
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface ServerTiming {
|
|
298
|
+
name: string
|
|
299
|
+
duration?: number
|
|
300
|
+
description?: string
|
|
301
|
+
}
|
package/tests/auth.test.ts
CHANGED
|
@@ -82,7 +82,7 @@ describe('Auth', () => {
|
|
|
82
82
|
)
|
|
83
83
|
throw new Error('Didnt throw')
|
|
84
84
|
} catch (e: any) {
|
|
85
|
-
expect(e
|
|
85
|
+
expect(e).toBeInstanceOf(XRPCError)
|
|
86
86
|
expect(e.success).toBeFalsy()
|
|
87
87
|
expect(e.error).toBe('AuthenticationRequired')
|
|
88
88
|
expect(e.message).toBe('Authentication Required')
|
|
@@ -105,7 +105,7 @@ describe('Auth', () => {
|
|
|
105
105
|
)
|
|
106
106
|
throw new Error('Didnt throw')
|
|
107
107
|
} catch (e: any) {
|
|
108
|
-
expect(e
|
|
108
|
+
expect(e).toBeInstanceOf(XRPCError)
|
|
109
109
|
expect(e.success).toBeFalsy()
|
|
110
110
|
expect(e.error).toBe('InvalidRequest')
|
|
111
111
|
expect(e.message).toBe('Input/present must be true')
|
package/tests/bodies.test.ts
CHANGED
|
@@ -23,7 +23,7 @@ const LEXICONS = [
|
|
|
23
23
|
required: ['foo'],
|
|
24
24
|
properties: {
|
|
25
25
|
foo: { type: 'string' },
|
|
26
|
-
bar: { type: '
|
|
26
|
+
bar: { type: 'integer' },
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
},
|
|
@@ -34,7 +34,7 @@ const LEXICONS = [
|
|
|
34
34
|
required: ['foo'],
|
|
35
35
|
properties: {
|
|
36
36
|
foo: { type: 'string' },
|
|
37
|
-
bar: { type: '
|
|
37
|
+
bar: { type: 'integer' },
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
40
|
},
|
|
@@ -43,7 +43,7 @@ const LEXICONS = [
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
lexicon: 1,
|
|
46
|
-
id: 'io.example.
|
|
46
|
+
id: 'io.example.validationTestTwo',
|
|
47
47
|
defs: {
|
|
48
48
|
main: {
|
|
49
49
|
type: 'query',
|
|
@@ -54,7 +54,7 @@ const LEXICONS = [
|
|
|
54
54
|
required: ['foo'],
|
|
55
55
|
properties: {
|
|
56
56
|
foo: { type: 'string' },
|
|
57
|
-
bar: { type: '
|
|
57
|
+
bar: { type: 'integer' },
|
|
58
58
|
},
|
|
59
59
|
},
|
|
60
60
|
},
|
|
@@ -101,7 +101,7 @@ describe('Bodies', () => {
|
|
|
101
101
|
body: ctx.input?.body,
|
|
102
102
|
}),
|
|
103
103
|
)
|
|
104
|
-
server.method('io.example.
|
|
104
|
+
server.method('io.example.validationTestTwo', () => ({
|
|
105
105
|
encoding: 'json',
|
|
106
106
|
body: { wrong: 'data' },
|
|
107
107
|
}))
|
|
@@ -175,7 +175,7 @@ describe('Bodies', () => {
|
|
|
175
175
|
return logger.error(obj, ...args)
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
await expect(client.call('io.example.
|
|
178
|
+
await expect(client.call('io.example.validationTestTwo')).rejects.toThrow(
|
|
179
179
|
'Internal Server Error',
|
|
180
180
|
)
|
|
181
181
|
expect(error).toEqual(`Output must have the property "foo"`)
|
package/tests/errors.test.ts
CHANGED
|
@@ -26,6 +26,15 @@ const LEXICONS = [
|
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
|
+
{
|
|
30
|
+
lexicon: 1,
|
|
31
|
+
id: 'io.example.throwFalsyValue',
|
|
32
|
+
defs: {
|
|
33
|
+
main: {
|
|
34
|
+
type: 'query',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
29
38
|
{
|
|
30
39
|
lexicon: 1,
|
|
31
40
|
id: 'io.example.query',
|
|
@@ -107,6 +116,9 @@ describe('Errors', () => {
|
|
|
107
116
|
return { status: 400 }
|
|
108
117
|
}
|
|
109
118
|
})
|
|
119
|
+
server.method('io.example.throwFalsyValue', () => {
|
|
120
|
+
throw ''
|
|
121
|
+
})
|
|
110
122
|
server.method('io.example.query', () => {
|
|
111
123
|
return undefined
|
|
112
124
|
})
|
|
@@ -140,7 +152,7 @@ describe('Errors', () => {
|
|
|
140
152
|
})
|
|
141
153
|
throw new Error('Didnt throw')
|
|
142
154
|
} catch (e) {
|
|
143
|
-
expect(e
|
|
155
|
+
expect(e).toBeInstanceOf(XRPCError)
|
|
144
156
|
expect((e as XRPCError).success).toBeFalsy()
|
|
145
157
|
expect((e as XRPCError).error).toBe('Foo')
|
|
146
158
|
expect((e as XRPCError).message).toBe('It was this one!')
|
|
@@ -151,18 +163,27 @@ describe('Errors', () => {
|
|
|
151
163
|
})
|
|
152
164
|
throw new Error('Didnt throw')
|
|
153
165
|
} catch (e) {
|
|
154
|
-
expect(e
|
|
166
|
+
expect(e).toBeInstanceOf(XRPCError)
|
|
155
167
|
expect((e as XRPCError).success).toBeFalsy()
|
|
156
168
|
expect((e as XRPCError).error).toBe('Bar')
|
|
157
169
|
expect((e as XRPCError).message).toBe('It was that one!')
|
|
158
170
|
}
|
|
171
|
+
try {
|
|
172
|
+
await client.call('io.example.throwFalsyValue')
|
|
173
|
+
throw new Error('Didnt throw')
|
|
174
|
+
} catch (e) {
|
|
175
|
+
expect(e instanceof XRPCError).toBeTruthy()
|
|
176
|
+
expect((e as XRPCError).success).toBeFalsy()
|
|
177
|
+
expect((e as XRPCError).error).toBe('InternalServerError')
|
|
178
|
+
expect((e as XRPCError).message).toBe('Internal Server Error')
|
|
179
|
+
}
|
|
159
180
|
try {
|
|
160
181
|
await client.call('io.example.error', {
|
|
161
182
|
which: 'other',
|
|
162
183
|
})
|
|
163
184
|
throw new Error('Didnt throw')
|
|
164
185
|
} catch (e) {
|
|
165
|
-
expect(e
|
|
186
|
+
expect(e).toBeInstanceOf(XRPCError)
|
|
166
187
|
expect((e as XRPCError).success).toBeFalsy()
|
|
167
188
|
expect((e as XRPCError).error).toBe('InvalidRequest')
|
|
168
189
|
expect((e as XRPCError).message).toBe('Invalid Request')
|
|
@@ -171,8 +192,8 @@ describe('Errors', () => {
|
|
|
171
192
|
await client.call('io.example.invalidResponse')
|
|
172
193
|
throw new Error('Didnt throw')
|
|
173
194
|
} catch (e: any) {
|
|
174
|
-
expect(e
|
|
175
|
-
expect(e
|
|
195
|
+
expect(e).toBeInstanceOf(XRPCError)
|
|
196
|
+
expect(e).toBeInstanceOf(XRPCInvalidResponseError)
|
|
176
197
|
expect(e.success).toBeFalsy()
|
|
177
198
|
expect(e.error).toBe('Invalid Response')
|
|
178
199
|
expect(e.message).toBe(
|
|
@@ -193,7 +214,7 @@ describe('Errors', () => {
|
|
|
193
214
|
await badClient.call('io.example.query')
|
|
194
215
|
throw new Error('Didnt throw')
|
|
195
216
|
} catch (e: any) {
|
|
196
|
-
expect(e
|
|
217
|
+
expect(e).toBeInstanceOf(XRPCError)
|
|
197
218
|
expect(e.success).toBeFalsy()
|
|
198
219
|
expect(e.error).toBe('InvalidRequest')
|
|
199
220
|
expect(e.message).toBe('Incorrect HTTP method (POST) expected GET')
|
|
@@ -202,7 +223,7 @@ describe('Errors', () => {
|
|
|
202
223
|
await badClient.call('io.example.procedure')
|
|
203
224
|
throw new Error('Didnt throw')
|
|
204
225
|
} catch (e: any) {
|
|
205
|
-
expect(e
|
|
226
|
+
expect(e).toBeInstanceOf(XRPCError)
|
|
206
227
|
expect(e.success).toBeFalsy()
|
|
207
228
|
expect(e.error).toBe('InvalidRequest')
|
|
208
229
|
expect(e.message).toBe('Incorrect HTTP method (GET) expected POST')
|
|
@@ -211,7 +232,7 @@ describe('Errors', () => {
|
|
|
211
232
|
await badClient.call('io.example.doesNotExist')
|
|
212
233
|
throw new Error('Didnt throw')
|
|
213
234
|
} catch (e: any) {
|
|
214
|
-
expect(e
|
|
235
|
+
expect(e).toBeInstanceOf(XRPCError)
|
|
215
236
|
expect(e.success).toBeFalsy()
|
|
216
237
|
expect(e.error).toBe('MethodNotImplemented')
|
|
217
238
|
expect(e.message).toBe('Method Not Implemented')
|
package/tests/parameters.test.ts
CHANGED
|
@@ -13,11 +13,10 @@ const LEXICONS = [
|
|
|
13
13
|
type: 'query',
|
|
14
14
|
parameters: {
|
|
15
15
|
type: 'params',
|
|
16
|
-
required: ['str', 'int', '
|
|
16
|
+
required: ['str', 'int', 'bool', 'arr'],
|
|
17
17
|
properties: {
|
|
18
18
|
str: { type: 'string', minLength: 2, maxLength: 10 },
|
|
19
19
|
int: { type: 'integer', minimum: 2, maximum: 10 },
|
|
20
|
-
num: { type: 'float', minimum: 2, maximum: 10 },
|
|
21
20
|
bool: { type: 'boolean' },
|
|
22
21
|
arr: { type: 'array', items: { type: 'integer' }, maxLength: 2 },
|
|
23
22
|
def: { type: 'integer', default: 0 },
|
|
@@ -57,7 +56,6 @@ describe('Parameters', () => {
|
|
|
57
56
|
const res1 = await client.call('io.example.paramTest', {
|
|
58
57
|
str: 'valid',
|
|
59
58
|
int: 5,
|
|
60
|
-
num: 5.5,
|
|
61
59
|
bool: true,
|
|
62
60
|
arr: [1, 2],
|
|
63
61
|
def: 5,
|
|
@@ -65,7 +63,6 @@ describe('Parameters', () => {
|
|
|
65
63
|
expect(res1.success).toBeTruthy()
|
|
66
64
|
expect(res1.data.str).toBe('valid')
|
|
67
65
|
expect(res1.data.int).toBe(5)
|
|
68
|
-
expect(res1.data.num).toBe(5.5)
|
|
69
66
|
expect(res1.data.bool).toBe(true)
|
|
70
67
|
expect(res1.data.arr).toEqual([1, 2])
|
|
71
68
|
expect(res1.data.def).toEqual(5)
|
|
@@ -73,14 +70,12 @@ describe('Parameters', () => {
|
|
|
73
70
|
const res2 = await client.call('io.example.paramTest', {
|
|
74
71
|
str: 10,
|
|
75
72
|
int: '5',
|
|
76
|
-
num: '5.5',
|
|
77
73
|
bool: 'foo',
|
|
78
74
|
arr: '3',
|
|
79
75
|
})
|
|
80
76
|
expect(res2.success).toBeTruthy()
|
|
81
77
|
expect(res2.data.str).toBe('10')
|
|
82
78
|
expect(res2.data.int).toBe(5)
|
|
83
|
-
expect(res2.data.num).toBe(5.5)
|
|
84
79
|
expect(res2.data.bool).toBe(true)
|
|
85
80
|
expect(res2.data.arr).toEqual([3])
|
|
86
81
|
expect(res2.data.def).toEqual(0)
|
|
@@ -90,7 +85,6 @@ describe('Parameters', () => {
|
|
|
90
85
|
client.call('io.example.paramTest', {
|
|
91
86
|
str: 'n',
|
|
92
87
|
int: 5,
|
|
93
|
-
num: 5.5,
|
|
94
88
|
bool: true,
|
|
95
89
|
arr: [1],
|
|
96
90
|
}),
|
|
@@ -99,7 +93,6 @@ describe('Parameters', () => {
|
|
|
99
93
|
client.call('io.example.paramTest', {
|
|
100
94
|
str: 'loooooooooooooong',
|
|
101
95
|
int: 5,
|
|
102
|
-
num: 5.5,
|
|
103
96
|
bool: true,
|
|
104
97
|
arr: [1],
|
|
105
98
|
}),
|
|
@@ -107,7 +100,6 @@ describe('Parameters', () => {
|
|
|
107
100
|
await expect(
|
|
108
101
|
client.call('io.example.paramTest', {
|
|
109
102
|
int: 5,
|
|
110
|
-
num: 5.5,
|
|
111
103
|
bool: true,
|
|
112
104
|
arr: [1],
|
|
113
105
|
}),
|
|
@@ -117,7 +109,6 @@ describe('Parameters', () => {
|
|
|
117
109
|
client.call('io.example.paramTest', {
|
|
118
110
|
str: 'valid',
|
|
119
111
|
int: -1,
|
|
120
|
-
num: 5.5,
|
|
121
112
|
bool: true,
|
|
122
113
|
arr: [1],
|
|
123
114
|
}),
|
|
@@ -126,7 +117,6 @@ describe('Parameters', () => {
|
|
|
126
117
|
client.call('io.example.paramTest', {
|
|
127
118
|
str: 'valid',
|
|
128
119
|
int: 11,
|
|
129
|
-
num: 5.5,
|
|
130
120
|
bool: true,
|
|
131
121
|
arr: [1],
|
|
132
122
|
}),
|
|
@@ -134,7 +124,6 @@ describe('Parameters', () => {
|
|
|
134
124
|
await expect(
|
|
135
125
|
client.call('io.example.paramTest', {
|
|
136
126
|
str: 'valid',
|
|
137
|
-
num: 5.5,
|
|
138
127
|
bool: true,
|
|
139
128
|
arr: [1],
|
|
140
129
|
}),
|
|
@@ -144,34 +133,6 @@ describe('Parameters', () => {
|
|
|
144
133
|
client.call('io.example.paramTest', {
|
|
145
134
|
str: 'valid',
|
|
146
135
|
int: 5,
|
|
147
|
-
num: -5.5,
|
|
148
|
-
bool: true,
|
|
149
|
-
arr: [1],
|
|
150
|
-
}),
|
|
151
|
-
).rejects.toThrow('num can not be less than 2')
|
|
152
|
-
await expect(
|
|
153
|
-
client.call('io.example.paramTest', {
|
|
154
|
-
str: 'valid',
|
|
155
|
-
int: 5,
|
|
156
|
-
num: 50.5,
|
|
157
|
-
bool: true,
|
|
158
|
-
arr: [1],
|
|
159
|
-
}),
|
|
160
|
-
).rejects.toThrow('num can not be greater than 10')
|
|
161
|
-
await expect(
|
|
162
|
-
client.call('io.example.paramTest', {
|
|
163
|
-
str: 'valid',
|
|
164
|
-
int: 5,
|
|
165
|
-
bool: true,
|
|
166
|
-
arr: [1],
|
|
167
|
-
}),
|
|
168
|
-
).rejects.toThrow(`Params must have the property "num"`)
|
|
169
|
-
|
|
170
|
-
await expect(
|
|
171
|
-
client.call('io.example.paramTest', {
|
|
172
|
-
str: 'valid',
|
|
173
|
-
int: 5,
|
|
174
|
-
num: 5.5,
|
|
175
136
|
arr: [1],
|
|
176
137
|
}),
|
|
177
138
|
).rejects.toThrow(`Params must have the property "bool"`)
|
|
@@ -180,7 +141,6 @@ describe('Parameters', () => {
|
|
|
180
141
|
client.call('io.example.paramTest', {
|
|
181
142
|
str: 'valid',
|
|
182
143
|
int: 5,
|
|
183
|
-
num: 5.5,
|
|
184
144
|
bool: true,
|
|
185
145
|
arr: [],
|
|
186
146
|
}),
|
|
@@ -189,7 +149,6 @@ describe('Parameters', () => {
|
|
|
189
149
|
client.call('io.example.paramTest', {
|
|
190
150
|
str: 'valid',
|
|
191
151
|
int: 5,
|
|
192
|
-
num: 5.5,
|
|
193
152
|
bool: true,
|
|
194
153
|
arr: [1, 2, 3],
|
|
195
154
|
}),
|
package/tests/procedures.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ import * as xrpcServer from '../src'
|
|
|
8
8
|
const LEXICONS = [
|
|
9
9
|
{
|
|
10
10
|
lexicon: 1,
|
|
11
|
-
id: 'io.example.
|
|
11
|
+
id: 'io.example.pingOne',
|
|
12
12
|
defs: {
|
|
13
13
|
main: {
|
|
14
14
|
type: 'procedure',
|
|
@@ -26,7 +26,7 @@ const LEXICONS = [
|
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
lexicon: 1,
|
|
29
|
-
id: 'io.example.
|
|
29
|
+
id: 'io.example.pingTwo',
|
|
30
30
|
defs: {
|
|
31
31
|
main: {
|
|
32
32
|
type: 'procedure',
|
|
@@ -41,7 +41,7 @@ const LEXICONS = [
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
lexicon: 1,
|
|
44
|
-
id: 'io.example.
|
|
44
|
+
id: 'io.example.pingThree',
|
|
45
45
|
defs: {
|
|
46
46
|
main: {
|
|
47
47
|
type: 'procedure',
|
|
@@ -56,7 +56,7 @@ const LEXICONS = [
|
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
lexicon: 1,
|
|
59
|
-
id: 'io.example.
|
|
59
|
+
id: 'io.example.pingFour',
|
|
60
60
|
defs: {
|
|
61
61
|
main: {
|
|
62
62
|
type: 'procedure',
|
|
@@ -84,17 +84,17 @@ const LEXICONS = [
|
|
|
84
84
|
describe('Procedures', () => {
|
|
85
85
|
let s: http.Server
|
|
86
86
|
const server = xrpcServer.createServer(LEXICONS)
|
|
87
|
-
server.method('io.example.
|
|
87
|
+
server.method('io.example.pingOne', (ctx: { params: xrpcServer.Params }) => {
|
|
88
88
|
return { encoding: 'text/plain', body: ctx.params.message }
|
|
89
89
|
})
|
|
90
90
|
server.method(
|
|
91
|
-
'io.example.
|
|
91
|
+
'io.example.pingTwo',
|
|
92
92
|
(ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
|
|
93
93
|
return { encoding: 'text/plain', body: ctx.input?.body }
|
|
94
94
|
},
|
|
95
95
|
)
|
|
96
96
|
server.method(
|
|
97
|
-
'io.example.
|
|
97
|
+
'io.example.pingThree',
|
|
98
98
|
async (ctx: {
|
|
99
99
|
params: xrpcServer.Params
|
|
100
100
|
input?: xrpcServer.HandlerInput
|
|
@@ -112,7 +112,7 @@ describe('Procedures', () => {
|
|
|
112
112
|
},
|
|
113
113
|
)
|
|
114
114
|
server.method(
|
|
115
|
-
'io.example.
|
|
115
|
+
'io.example.pingFour',
|
|
116
116
|
(ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
|
|
117
117
|
return {
|
|
118
118
|
encoding: 'application/json',
|
|
@@ -133,14 +133,14 @@ describe('Procedures', () => {
|
|
|
133
133
|
})
|
|
134
134
|
|
|
135
135
|
it('serves requests', async () => {
|
|
136
|
-
const res1 = await client.call('io.example.
|
|
136
|
+
const res1 = await client.call('io.example.pingOne', {
|
|
137
137
|
message: 'hello world',
|
|
138
138
|
})
|
|
139
139
|
expect(res1.success).toBeTruthy()
|
|
140
140
|
expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8')
|
|
141
141
|
expect(res1.data).toBe('hello world')
|
|
142
142
|
|
|
143
|
-
const res2 = await client.call('io.example.
|
|
143
|
+
const res2 = await client.call('io.example.pingTwo', {}, 'hello world', {
|
|
144
144
|
encoding: 'text/plain',
|
|
145
145
|
})
|
|
146
146
|
expect(res2.success).toBeTruthy()
|
|
@@ -148,7 +148,7 @@ describe('Procedures', () => {
|
|
|
148
148
|
expect(res2.data).toBe('hello world')
|
|
149
149
|
|
|
150
150
|
const res3 = await client.call(
|
|
151
|
-
'io.example.
|
|
151
|
+
'io.example.pingThree',
|
|
152
152
|
{},
|
|
153
153
|
new TextEncoder().encode('hello world'),
|
|
154
154
|
{ encoding: 'application/octet-stream' },
|
|
@@ -158,7 +158,7 @@ describe('Procedures', () => {
|
|
|
158
158
|
expect(new TextDecoder().decode(res3.data)).toBe('hello world')
|
|
159
159
|
|
|
160
160
|
const res4 = await client.call(
|
|
161
|
-
'io.example.
|
|
161
|
+
'io.example.pingFour',
|
|
162
162
|
{},
|
|
163
163
|
{ message: 'hello world' },
|
|
164
164
|
)
|
package/tests/queries.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as xrpcServer from '../src'
|
|
|
7
7
|
const LEXICONS = [
|
|
8
8
|
{
|
|
9
9
|
lexicon: 1,
|
|
10
|
-
id: 'io.example.
|
|
10
|
+
id: 'io.example.pingOne',
|
|
11
11
|
defs: {
|
|
12
12
|
main: {
|
|
13
13
|
type: 'query',
|
|
@@ -25,7 +25,7 @@ const LEXICONS = [
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
lexicon: 1,
|
|
28
|
-
id: 'io.example.
|
|
28
|
+
id: 'io.example.pingTwo',
|
|
29
29
|
defs: {
|
|
30
30
|
main: {
|
|
31
31
|
type: 'query',
|
|
@@ -43,7 +43,7 @@ const LEXICONS = [
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
lexicon: 1,
|
|
46
|
-
id: 'io.example.
|
|
46
|
+
id: 'io.example.pingThree',
|
|
47
47
|
defs: {
|
|
48
48
|
main: {
|
|
49
49
|
type: 'query',
|
|
@@ -69,21 +69,25 @@ const LEXICONS = [
|
|
|
69
69
|
describe('Queries', () => {
|
|
70
70
|
let s: http.Server
|
|
71
71
|
const server = xrpcServer.createServer(LEXICONS)
|
|
72
|
-
server.method('io.example.
|
|
72
|
+
server.method('io.example.pingOne', (ctx: { params: xrpcServer.Params }) => {
|
|
73
73
|
return { encoding: 'text/plain', body: ctx.params.message }
|
|
74
74
|
})
|
|
75
|
-
server.method('io.example.
|
|
75
|
+
server.method('io.example.pingTwo', (ctx: { params: xrpcServer.Params }) => {
|
|
76
76
|
return {
|
|
77
77
|
encoding: 'application/octet-stream',
|
|
78
78
|
body: new TextEncoder().encode(String(ctx.params.message)),
|
|
79
79
|
}
|
|
80
80
|
})
|
|
81
|
-
server.method(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
server.method(
|
|
82
|
+
'io.example.pingThree',
|
|
83
|
+
(ctx: { params: xrpcServer.Params }) => {
|
|
84
|
+
return {
|
|
85
|
+
encoding: 'application/json',
|
|
86
|
+
body: { message: ctx.params.message },
|
|
87
|
+
headers: { 'x-test-header-name': 'test-value' },
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
)
|
|
87
91
|
xrpc.addLexicons(LEXICONS)
|
|
88
92
|
|
|
89
93
|
let client: ServiceClient
|
|
@@ -97,25 +101,26 @@ describe('Queries', () => {
|
|
|
97
101
|
})
|
|
98
102
|
|
|
99
103
|
it('serves requests', async () => {
|
|
100
|
-
const res1 = await client.call('io.example.
|
|
104
|
+
const res1 = await client.call('io.example.pingOne', {
|
|
101
105
|
message: 'hello world',
|
|
102
106
|
})
|
|
103
107
|
expect(res1.success).toBeTruthy()
|
|
104
108
|
expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8')
|
|
105
109
|
expect(res1.data).toBe('hello world')
|
|
106
110
|
|
|
107
|
-
const res2 = await client.call('io.example.
|
|
111
|
+
const res2 = await client.call('io.example.pingTwo', {
|
|
108
112
|
message: 'hello world',
|
|
109
113
|
})
|
|
110
114
|
expect(res2.success).toBeTruthy()
|
|
111
115
|
expect(res2.headers['content-type']).toBe('application/octet-stream')
|
|
112
116
|
expect(new TextDecoder().decode(res2.data)).toBe('hello world')
|
|
113
117
|
|
|
114
|
-
const res3 = await client.call('io.example.
|
|
118
|
+
const res3 = await client.call('io.example.pingThree', {
|
|
115
119
|
message: 'hello world',
|
|
116
120
|
})
|
|
117
121
|
expect(res3.success).toBeTruthy()
|
|
118
122
|
expect(res3.headers['content-type']).toBe('application/json; charset=utf-8')
|
|
119
123
|
expect(res3.data?.message).toBe('hello world')
|
|
124
|
+
expect(res3.headers['x-test-header-name']).toEqual('test-value')
|
|
120
125
|
})
|
|
121
126
|
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as http from 'http'
|
|
2
|
+
import getPort from 'get-port'
|
|
3
|
+
import xrpc, { ServiceClient } from '@atproto/xrpc'
|
|
4
|
+
import { byteIterableToStream } from '@atproto/common'
|
|
5
|
+
import { createServer, closeServer } from './_util'
|
|
6
|
+
import * as xrpcServer from '../src'
|
|
7
|
+
|
|
8
|
+
const LEXICONS = [
|
|
9
|
+
{
|
|
10
|
+
lexicon: 1,
|
|
11
|
+
id: 'io.example.readableStream',
|
|
12
|
+
defs: {
|
|
13
|
+
main: {
|
|
14
|
+
type: 'query',
|
|
15
|
+
parameters: {
|
|
16
|
+
type: 'params',
|
|
17
|
+
properties: {
|
|
18
|
+
shouldErr: { type: 'boolean' },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
output: {
|
|
22
|
+
encoding: 'application/vnd.ipld.car',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
describe('Responses', () => {
|
|
30
|
+
let s: http.Server
|
|
31
|
+
const server = xrpcServer.createServer(LEXICONS)
|
|
32
|
+
server.method(
|
|
33
|
+
'io.example.readableStream',
|
|
34
|
+
async (ctx: { params: xrpcServer.Params }) => {
|
|
35
|
+
async function* iter(): AsyncIterable<Uint8Array> {
|
|
36
|
+
for (let i = 0; i < 5; i++) {
|
|
37
|
+
yield new Uint8Array([i])
|
|
38
|
+
}
|
|
39
|
+
if (ctx.params.shouldErr) {
|
|
40
|
+
throw new Error('error')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
encoding: 'application/vnd.ipld.car',
|
|
45
|
+
body: byteIterableToStream(iter()),
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
xrpc.addLexicons(LEXICONS)
|
|
50
|
+
|
|
51
|
+
let client: ServiceClient
|
|
52
|
+
let url: string
|
|
53
|
+
beforeAll(async () => {
|
|
54
|
+
const port = await getPort()
|
|
55
|
+
s = await createServer(port, server)
|
|
56
|
+
url = `http://localhost:${port}`
|
|
57
|
+
client = xrpc.service(url)
|
|
58
|
+
})
|
|
59
|
+
afterAll(async () => {
|
|
60
|
+
await closeServer(s)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('returns readable streams of bytes', async () => {
|
|
64
|
+
const res = await client.call('io.example.readableStream', {
|
|
65
|
+
shouldErr: false,
|
|
66
|
+
})
|
|
67
|
+
const expected = new Uint8Array([0, 1, 2, 3, 4])
|
|
68
|
+
expect(res.data).toEqual(expected)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('handles errs on readable streams of bytes', async () => {
|
|
72
|
+
const attempt = client.call('io.example.readableStream', {
|
|
73
|
+
shouldErr: true,
|
|
74
|
+
})
|
|
75
|
+
await expect(attempt).rejects.toThrow()
|
|
76
|
+
})
|
|
77
|
+
})
|