@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.
@@ -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 iterator = unwrapIterator(handler(req, socket, this))
16
- socket.once('close', () => iterator.return?.())
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
- if (frame instanceof ErrorFrame) {
20
- await new Promise((res, rej) => {
21
- socket.send(frame.toBytes(), { binary: true }, (err) => {
22
- if (err) return rej(err)
23
- res(undefined)
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
+ }
@@ -82,7 +82,7 @@ describe('Auth', () => {
82
82
  )
83
83
  throw new Error('Didnt throw')
84
84
  } catch (e: any) {
85
- expect(e instanceof XRPCError).toBeTruthy()
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 instanceof XRPCError).toBeTruthy()
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')
@@ -23,7 +23,7 @@ const LEXICONS = [
23
23
  required: ['foo'],
24
24
  properties: {
25
25
  foo: { type: 'string' },
26
- bar: { type: 'float' },
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: 'float' },
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.validationTest2',
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: 'float' },
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.validationTest2', () => ({
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.validationTest2')).rejects.toThrow(
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"`)
@@ -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 instanceof XRPCError).toBeTruthy()
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 instanceof XRPCError).toBeTruthy()
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 instanceof XRPCError).toBeTruthy()
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 instanceof XRPCError).toBeTruthy()
175
- expect(e instanceof XRPCInvalidResponseError).toBeTruthy()
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 instanceof XRPCError).toBeTruthy()
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 instanceof XRPCError).toBeTruthy()
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 instanceof XRPCError).toBeTruthy()
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')
@@ -13,11 +13,10 @@ const LEXICONS = [
13
13
  type: 'query',
14
14
  parameters: {
15
15
  type: 'params',
16
- required: ['str', 'int', 'num', 'bool', 'arr'],
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
  }),
@@ -8,7 +8,7 @@ import * as xrpcServer from '../src'
8
8
  const LEXICONS = [
9
9
  {
10
10
  lexicon: 1,
11
- id: 'io.example.ping1',
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.ping2',
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.ping3',
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.ping4',
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.ping1', (ctx: { params: xrpcServer.Params }) => {
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.ping2',
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.ping3',
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.ping4',
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.ping1', {
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.ping2', {}, 'hello world', {
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.ping3',
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.ping4',
161
+ 'io.example.pingFour',
162
162
  {},
163
163
  { message: 'hello world' },
164
164
  )
@@ -7,7 +7,7 @@ import * as xrpcServer from '../src'
7
7
  const LEXICONS = [
8
8
  {
9
9
  lexicon: 1,
10
- id: 'io.example.ping1',
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.ping2',
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.ping3',
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.ping1', (ctx: { params: xrpcServer.Params }) => {
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.ping2', (ctx: { params: xrpcServer.Params }) => {
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('io.example.ping3', (ctx: { params: xrpcServer.Params }) => {
82
- return {
83
- encoding: 'application/json',
84
- body: { message: ctx.params.message },
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.ping1', {
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.ping2', {
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.ping3', {
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
+ })