@atproto/xrpc-server 0.0.1 → 0.1.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.
@@ -0,0 +1,137 @@
1
+ import * as cborx from 'cbor-x'
2
+ import * as uint8arrays from 'uint8arrays'
3
+ import { MessageFrame, ErrorFrame, Frame, FrameType } from '../src'
4
+
5
+ describe('Frames', () => {
6
+ it('creates and parses message frame.', async () => {
7
+ const messageFrame = new MessageFrame(
8
+ { a: 'b', c: [1, 2, 3] },
9
+ { type: '#d' },
10
+ )
11
+
12
+ expect(messageFrame.header).toEqual({
13
+ op: FrameType.Message,
14
+ t: '#d',
15
+ })
16
+ expect(messageFrame.op).toEqual(FrameType.Message)
17
+ expect(messageFrame.type).toEqual('#d')
18
+ expect(messageFrame.body).toEqual({ a: 'b', c: [1, 2, 3] })
19
+
20
+ const bytes = messageFrame.toBytes()
21
+ expect(
22
+ uint8arrays.equals(
23
+ bytes,
24
+ new Uint8Array([
25
+ /*header*/ 162, 97, 116, 98, 35, 100, 98, 111, 112, 1, /*body*/ 162,
26
+ 97, 97, 97, 98, 97, 99, 131, 1, 2, 3,
27
+ ]),
28
+ ),
29
+ ).toEqual(true)
30
+
31
+ const parsedFrame = Frame.fromBytes(bytes)
32
+ if (!(parsedFrame instanceof MessageFrame)) {
33
+ throw new Error('Did not parse as message frame')
34
+ }
35
+
36
+ expect(parsedFrame.header).toEqual(messageFrame.header)
37
+ expect(parsedFrame.op).toEqual(messageFrame.op)
38
+ expect(parsedFrame.type).toEqual(messageFrame.type)
39
+ expect(parsedFrame.body).toEqual(messageFrame.body)
40
+ })
41
+
42
+ it('creates and parses error frame.', async () => {
43
+ const errorFrame = new ErrorFrame({
44
+ error: 'BigOops',
45
+ message: 'Something went awry',
46
+ })
47
+
48
+ expect(errorFrame.header).toEqual({ op: FrameType.Error })
49
+ expect(errorFrame.op).toEqual(FrameType.Error)
50
+ expect(errorFrame.code).toEqual('BigOops')
51
+ expect(errorFrame.message).toEqual('Something went awry')
52
+ expect(errorFrame.body).toEqual({
53
+ error: 'BigOops',
54
+ message: 'Something went awry',
55
+ })
56
+
57
+ const bytes = errorFrame.toBytes()
58
+ expect(
59
+ uint8arrays.equals(
60
+ bytes,
61
+ new Uint8Array([
62
+ /*header*/ 161, 98, 111, 112, 32, /*body*/ 162, 101, 101, 114, 114,
63
+ 111, 114, 103, 66, 105, 103, 79, 111, 112, 115, 103, 109, 101, 115,
64
+ 115, 97, 103, 101, 115, 83, 111, 109, 101, 116, 104, 105, 110, 103,
65
+ 32, 119, 101, 110, 116, 32, 97, 119, 114, 121,
66
+ ]),
67
+ ),
68
+ ).toEqual(true)
69
+
70
+ const parsedFrame = Frame.fromBytes(bytes)
71
+ if (!(parsedFrame instanceof ErrorFrame)) {
72
+ throw new Error('Did not parse as error frame')
73
+ }
74
+
75
+ expect(parsedFrame.header).toEqual(errorFrame.header)
76
+ expect(parsedFrame.op).toEqual(errorFrame.op)
77
+ expect(parsedFrame.code).toEqual(errorFrame.code)
78
+ expect(parsedFrame.message).toEqual(errorFrame.message)
79
+ expect(parsedFrame.body).toEqual(errorFrame.body)
80
+ })
81
+
82
+ it('parsing fails when frame is not CBOR.', async () => {
83
+ const bytes = Buffer.from('some utf8 bytes')
84
+ const emptyBytes = Buffer.from('')
85
+ expect(() => Frame.fromBytes(bytes)).toThrow('Unexpected end of CBOR data')
86
+ expect(() => Frame.fromBytes(emptyBytes)).toThrow(
87
+ 'Unexpected end of CBOR data',
88
+ )
89
+ })
90
+
91
+ it('parsing fails when frame header is malformed.', async () => {
92
+ const bytes = uint8arrays.concat([
93
+ cborx.encode({ op: -2 }), // Unknown op
94
+ cborx.encode({ a: 'b', c: [1, 2, 3] }),
95
+ ])
96
+
97
+ expect(() => Frame.fromBytes(bytes)).toThrow('Invalid frame header:')
98
+ })
99
+
100
+ it('parsing fails when frame is missing body.', async () => {
101
+ const messageFrame = new MessageFrame(
102
+ { a: 'b', c: [1, 2, 3] },
103
+ { type: '#d' },
104
+ )
105
+
106
+ const headerBytes = cborx.encode(messageFrame.header)
107
+
108
+ expect(() => Frame.fromBytes(headerBytes)).toThrow('Missing frame body')
109
+ })
110
+
111
+ it('parsing fails when frame has too many data items.', async () => {
112
+ const messageFrame = new MessageFrame(
113
+ { a: 'b', c: [1, 2, 3] },
114
+ { type: '#d' },
115
+ )
116
+
117
+ const bytes = uint8arrays.concat([
118
+ messageFrame.toBytes(),
119
+ cborx.encode({ d: 'e', f: [4, 5, 6] }),
120
+ ])
121
+
122
+ expect(() => Frame.fromBytes(bytes)).toThrow(
123
+ 'Too many CBOR data items in frame',
124
+ )
125
+ })
126
+
127
+ it('parsing fails when error frame has invalid body.', async () => {
128
+ const errorFrame = new ErrorFrame({ error: 'BadOops' })
129
+
130
+ const bytes = uint8arrays.concat([
131
+ cborx.encode(errorFrame.header),
132
+ cborx.encode({ blah: 1 }),
133
+ ])
134
+
135
+ expect(() => Frame.fromBytes(bytes)).toThrow('Invalid error frame body:')
136
+ })
137
+ })
@@ -0,0 +1,96 @@
1
+ import * as http from 'http'
2
+ import xrpc, { ServiceClient } from '@atproto/xrpc'
3
+ import { CID } from 'multiformats/cid'
4
+ import getPort from 'get-port'
5
+ import { createServer, closeServer } from './_util'
6
+ import * as xrpcServer from '../src'
7
+
8
+ const LEXICONS = [
9
+ {
10
+ lexicon: 1,
11
+ id: 'io.example.ipld',
12
+ defs: {
13
+ main: {
14
+ type: 'procedure',
15
+ input: {
16
+ encoding: 'application/json',
17
+ schema: {
18
+ type: 'object',
19
+ properties: {
20
+ cid: {
21
+ type: 'cid-link',
22
+ },
23
+ bytes: {
24
+ type: 'bytes',
25
+ },
26
+ },
27
+ },
28
+ },
29
+ output: {
30
+ encoding: 'application/json',
31
+ schema: {
32
+ type: 'object',
33
+ properties: {
34
+ cid: {
35
+ type: 'cid-link',
36
+ },
37
+ bytes: {
38
+ type: 'bytes',
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ ]
47
+
48
+ describe('Ipld vals', () => {
49
+ let s: http.Server
50
+ const server = xrpcServer.createServer(LEXICONS)
51
+ server.method(
52
+ 'io.example.ipld',
53
+ (ctx: { input?: xrpcServer.HandlerInput }) => {
54
+ const asCid = CID.asCID(ctx.input?.body.cid)
55
+ if (!(asCid instanceof CID)) {
56
+ throw new Error('expected cid')
57
+ }
58
+ const bytes = ctx.input?.body.bytes
59
+ if (!(bytes instanceof Uint8Array)) {
60
+ throw new Error('expected bytes')
61
+ }
62
+ return { encoding: 'application/json', body: ctx.input?.body }
63
+ },
64
+ )
65
+ xrpc.addLexicons(LEXICONS)
66
+
67
+ let client: ServiceClient
68
+ beforeAll(async () => {
69
+ const port = await getPort()
70
+ s = await createServer(port, server)
71
+ client = xrpc.service(`http://localhost:${port}`)
72
+ })
73
+ afterAll(async () => {
74
+ await closeServer(s)
75
+ })
76
+
77
+ it('can send and receive ipld vals', async () => {
78
+ const cid = CID.parse(
79
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
80
+ )
81
+ const bytes = new Uint8Array([0, 1, 2, 3])
82
+ const res = await client.call(
83
+ 'io.example.ipld',
84
+ {},
85
+ {
86
+ cid,
87
+ bytes,
88
+ },
89
+ { encoding: 'application/json' },
90
+ )
91
+ expect(res.success).toBeTruthy()
92
+ expect(res.headers['content-type']).toBe('application/json; charset=utf-8')
93
+ expect(cid.equals(res.data.cid)).toBeTruthy()
94
+ expect(bytes).toEqual(res.data.bytes)
95
+ })
96
+ })
@@ -1,7 +1,8 @@
1
1
  import * as http from 'http'
2
+ import getPort from 'get-port'
3
+ import xrpc, { ServiceClient } from '@atproto/xrpc'
2
4
  import { createServer, closeServer } from './_util'
3
5
  import * as xrpcServer from '../src'
4
- import xrpc from '@atproto/xrpc'
5
6
 
6
7
  const LEXICONS = [
7
8
  {
@@ -16,9 +17,10 @@ const LEXICONS = [
16
17
  properties: {
17
18
  str: { type: 'string', minLength: 2, maxLength: 10 },
18
19
  int: { type: 'integer', minimum: 2, maximum: 10 },
19
- num: { type: 'number', minimum: 2, maximum: 10 },
20
+ num: { type: 'float', minimum: 2, maximum: 10 },
20
21
  bool: { type: 'boolean' },
21
22
  arr: { type: 'array', items: { type: 'integer' }, maxLength: 2 },
23
+ def: { type: 'integer', default: 0 },
22
24
  },
23
25
  },
24
26
  output: {
@@ -39,10 +41,13 @@ describe('Parameters', () => {
39
41
  body: ctx.params,
40
42
  }),
41
43
  )
42
- const client = xrpc.service(`http://localhost:8889`)
43
44
  xrpc.addLexicons(LEXICONS)
45
+
46
+ let client: ServiceClient
44
47
  beforeAll(async () => {
45
- s = await createServer(8889, server)
48
+ const port = await getPort()
49
+ s = await createServer(port, server)
50
+ client = xrpc.service(`http://localhost:${port}`)
46
51
  })
47
52
  afterAll(async () => {
48
53
  await closeServer(s)
@@ -55,6 +60,7 @@ describe('Parameters', () => {
55
60
  num: 5.5,
56
61
  bool: true,
57
62
  arr: [1, 2],
63
+ def: 5,
58
64
  })
59
65
  expect(res1.success).toBeTruthy()
60
66
  expect(res1.data.str).toBe('valid')
@@ -62,6 +68,7 @@ describe('Parameters', () => {
62
68
  expect(res1.data.num).toBe(5.5)
63
69
  expect(res1.data.bool).toBe(true)
64
70
  expect(res1.data.arr).toEqual([1, 2])
71
+ expect(res1.data.def).toEqual(5)
65
72
 
66
73
  const res2 = await client.call('io.example.paramTest', {
67
74
  str: 10,
@@ -76,7 +83,9 @@ describe('Parameters', () => {
76
83
  expect(res2.data.num).toBe(5.5)
77
84
  expect(res2.data.bool).toBe(true)
78
85
  expect(res2.data.arr).toEqual([3])
86
+ expect(res2.data.def).toEqual(0)
79
87
 
88
+ // @TODO test sending blatantly bad types
80
89
  await expect(
81
90
  client.call('io.example.paramTest', {
82
91
  str: 'n',
@@ -1,6 +1,7 @@
1
1
  import * as http from 'http'
2
2
  import { Readable } from 'stream'
3
- import xrpc from '@atproto/xrpc'
3
+ import xrpc, { ServiceClient } from '@atproto/xrpc'
4
+ import getPort from 'get-port'
4
5
  import { createServer, closeServer } from './_util'
5
6
  import * as xrpcServer from '../src'
6
7
 
@@ -119,10 +120,13 @@ describe('Procedures', () => {
119
120
  }
120
121
  },
121
122
  )
122
- const client = xrpc.service(`http://localhost:8891`)
123
123
  xrpc.addLexicons(LEXICONS)
124
+
125
+ let client: ServiceClient
124
126
  beforeAll(async () => {
125
- s = await createServer(8891, server)
127
+ const port = await getPort()
128
+ s = await createServer(port, server)
129
+ client = xrpc.service(`http://localhost:${port}`)
126
130
  })
127
131
  afterAll(async () => {
128
132
  await closeServer(s)
@@ -1,7 +1,8 @@
1
1
  import * as http from 'http'
2
+ import getPort from 'get-port'
3
+ import xrpc, { ServiceClient } from '@atproto/xrpc'
2
4
  import { createServer, closeServer } from './_util'
3
5
  import * as xrpcServer from '../src'
4
- import xrpc from '@atproto/xrpc'
5
6
 
6
7
  const LEXICONS = [
7
8
  {
@@ -83,10 +84,13 @@ describe('Queries', () => {
83
84
  body: { message: ctx.params.message },
84
85
  }
85
86
  })
86
- const client = xrpc.service(`http://localhost:8890`)
87
87
  xrpc.addLexicons(LEXICONS)
88
+
89
+ let client: ServiceClient
88
90
  beforeAll(async () => {
89
- s = await createServer(8890, server)
91
+ const port = await getPort()
92
+ s = await createServer(port, server)
93
+ client = xrpc.service(`http://localhost:${port}`)
90
94
  })
91
95
  afterAll(async () => {
92
96
  await closeServer(s)
@@ -0,0 +1,169 @@
1
+ import * as http from 'http'
2
+ import { once } from 'events'
3
+ import { AddressInfo } from 'net'
4
+ import { WebSocket } from 'ws'
5
+ import { XRPCError } from '@atproto/xrpc'
6
+ import {
7
+ ErrorFrame,
8
+ Frame,
9
+ MessageFrame,
10
+ XrpcStreamServer,
11
+ byFrame,
12
+ byMessage,
13
+ } from '../src'
14
+
15
+ describe('Stream', () => {
16
+ const wait = (ms) => new Promise((res) => setTimeout(res, ms))
17
+ it('streams message and info frames.', async () => {
18
+ const httpServer = http.createServer()
19
+ const server = new XrpcStreamServer({
20
+ server: httpServer,
21
+ handler: async function* () {
22
+ await wait(1)
23
+ yield new MessageFrame(1)
24
+ await wait(1)
25
+ yield new MessageFrame(2)
26
+ await wait(1)
27
+ yield new MessageFrame(3)
28
+ return
29
+ },
30
+ })
31
+
32
+ await once(httpServer.listen(), 'listening')
33
+ const { port } = server.wss.address() as AddressInfo
34
+
35
+ const ws = new WebSocket(`ws://localhost:${port}`)
36
+ const frames: Frame[] = []
37
+ for await (const frame of byFrame(ws)) {
38
+ frames.push(frame)
39
+ }
40
+
41
+ expect(frames).toEqual([
42
+ new MessageFrame(1),
43
+ new MessageFrame(2),
44
+ new MessageFrame(3),
45
+ ])
46
+
47
+ httpServer.close()
48
+ })
49
+
50
+ it('kills handler and closes on error frame.', async () => {
51
+ let proceededAfterError = false
52
+ const httpServer = http.createServer()
53
+ const server = new XrpcStreamServer({
54
+ server: httpServer,
55
+ handler: async function* () {
56
+ await wait(1)
57
+ yield new MessageFrame(1)
58
+ await wait(1)
59
+ yield new MessageFrame(2)
60
+ await wait(1)
61
+ yield new ErrorFrame({ error: 'BadOops' })
62
+ proceededAfterError = true
63
+ await wait(1)
64
+ yield new MessageFrame(3)
65
+ return
66
+ },
67
+ })
68
+
69
+ await once(httpServer.listen(), 'listening')
70
+ const { port } = server.wss.address() as AddressInfo
71
+
72
+ const ws = new WebSocket(`ws://localhost:${port}`)
73
+ const frames: Frame[] = []
74
+ for await (const frame of byFrame(ws)) {
75
+ frames.push(frame)
76
+ }
77
+
78
+ await wait(5) // Ensure handler hasn't kept running
79
+ expect(proceededAfterError).toEqual(false)
80
+
81
+ expect(frames).toEqual([
82
+ new MessageFrame(1),
83
+ new MessageFrame(2),
84
+ new ErrorFrame({ error: 'BadOops' }),
85
+ ])
86
+
87
+ httpServer.close()
88
+ })
89
+
90
+ it('kills handler and closes client disconnect.', async () => {
91
+ const httpServer = http.createServer()
92
+ let i = 1
93
+ const server = new XrpcStreamServer({
94
+ server: httpServer,
95
+ handler: async function* () {
96
+ while (true) {
97
+ await wait(0)
98
+ yield new MessageFrame(i++)
99
+ }
100
+ },
101
+ })
102
+
103
+ await once(httpServer.listen(), 'listening')
104
+ const { port } = server.wss.address() as AddressInfo
105
+
106
+ const ws = new WebSocket(`ws://localhost:${port}`)
107
+ const frames: Frame[] = []
108
+ for await (const frame of byFrame(ws)) {
109
+ frames.push(frame)
110
+ if (frame.body === 3) ws.terminate()
111
+ }
112
+
113
+ // Grace period to let close take place on the server
114
+ await wait(5)
115
+ // Ensure handler hasn't kept running
116
+ const currentCount = i
117
+ await wait(5)
118
+ expect(i).toBe(currentCount)
119
+
120
+ httpServer.close()
121
+ })
122
+
123
+ describe('byMessage()', () => {
124
+ it('kills handler and closes client disconnect on error frame.', async () => {
125
+ const httpServer = http.createServer()
126
+ const server = new XrpcStreamServer({
127
+ server: httpServer,
128
+ handler: async function* () {
129
+ await wait(1)
130
+ yield new MessageFrame(1)
131
+ await wait(1)
132
+ yield new MessageFrame(2)
133
+ await wait(1)
134
+ yield new ErrorFrame({
135
+ error: 'BadOops',
136
+ message: 'That was a bad one',
137
+ })
138
+ await wait(1)
139
+ yield new MessageFrame(3)
140
+ return
141
+ },
142
+ })
143
+ await once(httpServer.listen(), 'listening')
144
+ const { port } = server.wss.address() as AddressInfo
145
+
146
+ const ws = new WebSocket(`ws://localhost:${port}`)
147
+ const frames: Frame[] = []
148
+
149
+ let error
150
+ try {
151
+ for await (const frame of byMessage(ws)) {
152
+ frames.push(frame)
153
+ }
154
+ } catch (err) {
155
+ error = err
156
+ }
157
+
158
+ expect(ws.readyState).toEqual(ws.CLOSING)
159
+ expect(frames).toEqual([new MessageFrame(1), new MessageFrame(2)])
160
+ expect(error).toBeInstanceOf(XRPCError)
161
+ if (error instanceof XRPCError) {
162
+ expect(error.error).toEqual('BadOops')
163
+ expect(error.message).toEqual('That was a bad one')
164
+ }
165
+
166
+ httpServer.close()
167
+ })
168
+ })
169
+ })