@atproto/xrpc-server 0.11.4 → 0.11.6
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 +27 -0
- package/package.json +26 -21
- package/jest.config.cjs +0 -21
- package/src/auth.ts +0 -235
- package/src/errors.ts +0 -312
- package/src/index.ts +0 -14
- package/src/logger.ts +0 -8
- package/src/rate-limiter-http.ts +0 -82
- package/src/rate-limiter.ts +0 -279
- package/src/server.ts +0 -858
- package/src/stream/frames.ts +0 -125
- package/src/stream/index.ts +0 -5
- package/src/stream/logger.ts +0 -6
- package/src/stream/server.ts +0 -66
- package/src/stream/stream.ts +0 -39
- package/src/stream/subscription.ts +0 -96
- package/src/stream/types.ts +0 -27
- package/src/types.ts +0 -330
- package/src/util.ts +0 -708
- package/tests/_util.ts +0 -124
- package/tests/auth.test.ts +0 -333
- package/tests/bodies.test.ts +0 -608
- package/tests/errors.test.ts +0 -299
- package/tests/frames.test.ts +0 -135
- package/tests/ipld.test.ts +0 -97
- package/tests/parameters.test.ts +0 -331
- package/tests/parsing.test.ts +0 -89
- package/tests/procedures.test.ts +0 -176
- package/tests/queries.test.ts +0 -140
- package/tests/rate-limiter.test.ts +0 -312
- package/tests/responses.test.ts +0 -72
- package/tests/stream.test.ts +0 -169
- package/tests/subscriptions.test.ts +0 -398
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
package/tests/queries.test.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import * as http from 'node:http'
|
|
2
|
-
import { AddressInfo } from 'node:net'
|
|
3
|
-
import { LexiconDoc } from '@atproto/lexicon'
|
|
4
|
-
import { XrpcClient } from '@atproto/xrpc'
|
|
5
|
-
import * as xrpcServer from '../src/index.js'
|
|
6
|
-
import {
|
|
7
|
-
buildAddLexicons,
|
|
8
|
-
buildMethodLexicons,
|
|
9
|
-
closeServer,
|
|
10
|
-
createServer,
|
|
11
|
-
} from './_util.js'
|
|
12
|
-
|
|
13
|
-
const LEXICONS = [
|
|
14
|
-
{
|
|
15
|
-
lexicon: 1,
|
|
16
|
-
id: 'io.example.pingOne',
|
|
17
|
-
defs: {
|
|
18
|
-
main: {
|
|
19
|
-
type: 'query',
|
|
20
|
-
parameters: {
|
|
21
|
-
type: 'params',
|
|
22
|
-
properties: {
|
|
23
|
-
message: { type: 'string' },
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
output: {
|
|
27
|
-
encoding: 'text/plain',
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
lexicon: 1,
|
|
34
|
-
id: 'io.example.pingTwo',
|
|
35
|
-
defs: {
|
|
36
|
-
main: {
|
|
37
|
-
type: 'query',
|
|
38
|
-
parameters: {
|
|
39
|
-
type: 'params',
|
|
40
|
-
properties: {
|
|
41
|
-
message: { type: 'string' },
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
output: {
|
|
45
|
-
encoding: 'application/octet-stream',
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
lexicon: 1,
|
|
52
|
-
id: 'io.example.pingThree',
|
|
53
|
-
defs: {
|
|
54
|
-
main: {
|
|
55
|
-
type: 'query',
|
|
56
|
-
parameters: {
|
|
57
|
-
type: 'params',
|
|
58
|
-
properties: {
|
|
59
|
-
message: { type: 'string' },
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
output: {
|
|
63
|
-
encoding: 'application/json',
|
|
64
|
-
schema: {
|
|
65
|
-
type: 'object',
|
|
66
|
-
required: ['message'],
|
|
67
|
-
properties: { message: { type: 'string' } },
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
] as const satisfies LexiconDoc[]
|
|
74
|
-
|
|
75
|
-
const handlers = {
|
|
76
|
-
'io.example.pingOne': (ctx: xrpcServer.HandlerContext) => {
|
|
77
|
-
return { encoding: 'text/plain', body: ctx.params.message }
|
|
78
|
-
},
|
|
79
|
-
'io.example.pingTwo': (ctx: xrpcServer.HandlerContext) => {
|
|
80
|
-
return {
|
|
81
|
-
encoding: 'application/octet-stream',
|
|
82
|
-
body: new TextEncoder().encode(String(ctx.params.message)),
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
'io.example.pingThree': (ctx: xrpcServer.HandlerContext) => {
|
|
86
|
-
return {
|
|
87
|
-
encoding: 'application/json',
|
|
88
|
-
body: { message: ctx.params.message },
|
|
89
|
-
headers: { 'x-test-header-name': 'test-value' },
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
for (const buildServer of [buildMethodLexicons, buildAddLexicons]) {
|
|
95
|
-
describe(buildServer, () => {
|
|
96
|
-
let s: http.Server
|
|
97
|
-
let client: XrpcClient
|
|
98
|
-
let url: string
|
|
99
|
-
beforeAll(async () => {
|
|
100
|
-
const server = await buildServer(LEXICONS, handlers)
|
|
101
|
-
s = await createServer(server)
|
|
102
|
-
const { port } = s.address() as AddressInfo
|
|
103
|
-
url = `http://localhost:${port}`
|
|
104
|
-
client = new XrpcClient(url, LEXICONS)
|
|
105
|
-
})
|
|
106
|
-
afterAll(async () => {
|
|
107
|
-
if (s) await closeServer(s)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('io.example.pingOne', async () => {
|
|
111
|
-
const res = await client.call('io.example.pingOne', {
|
|
112
|
-
message: 'hello world',
|
|
113
|
-
})
|
|
114
|
-
expect(res.success).toBeTruthy()
|
|
115
|
-
expect(res.headers['content-type']).toBe('text/plain; charset=utf-8')
|
|
116
|
-
expect(res.data).toBe('hello world')
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
test('io.example.pingTwo', async () => {
|
|
120
|
-
const res = await client.call('io.example.pingTwo', {
|
|
121
|
-
message: 'hello world',
|
|
122
|
-
})
|
|
123
|
-
expect(res.success).toBeTruthy()
|
|
124
|
-
expect(res.headers['content-type']).toBe('application/octet-stream')
|
|
125
|
-
expect(new TextDecoder().decode(res.data)).toBe('hello world')
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
test('io.example.pingThree', async () => {
|
|
129
|
-
const res = await client.call('io.example.pingThree', {
|
|
130
|
-
message: 'hello world',
|
|
131
|
-
})
|
|
132
|
-
expect(res.success).toBeTruthy()
|
|
133
|
-
expect(res.headers['content-type']).toBe(
|
|
134
|
-
'application/json; charset=utf-8',
|
|
135
|
-
)
|
|
136
|
-
expect(res.data?.message).toBe('hello world')
|
|
137
|
-
expect(res.headers['x-test-header-name']).toEqual('test-value')
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
}
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import * as http from 'node:http'
|
|
2
|
-
import { AddressInfo } from 'node:net'
|
|
3
|
-
import { MINUTE } from '@atproto/common'
|
|
4
|
-
import { LexiconDoc } from '@atproto/lexicon'
|
|
5
|
-
import { XrpcClient } from '@atproto/xrpc'
|
|
6
|
-
import * as xrpcServer from '../src/index.js'
|
|
7
|
-
import { MemoryRateLimiter } from '../src/index.js'
|
|
8
|
-
import { closeServer, createServer } from './_util.js'
|
|
9
|
-
|
|
10
|
-
const LEXICONS: LexiconDoc[] = [
|
|
11
|
-
{
|
|
12
|
-
lexicon: 1,
|
|
13
|
-
id: 'io.example.routeLimit',
|
|
14
|
-
defs: {
|
|
15
|
-
main: {
|
|
16
|
-
type: 'query',
|
|
17
|
-
parameters: {
|
|
18
|
-
type: 'params',
|
|
19
|
-
required: ['str'],
|
|
20
|
-
properties: {
|
|
21
|
-
str: { type: 'string' },
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
output: {
|
|
25
|
-
encoding: 'application/json',
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
lexicon: 1,
|
|
32
|
-
id: 'io.example.routeLimitReset',
|
|
33
|
-
defs: {
|
|
34
|
-
main: {
|
|
35
|
-
type: 'query',
|
|
36
|
-
parameters: {
|
|
37
|
-
type: 'params',
|
|
38
|
-
required: ['count'],
|
|
39
|
-
properties: {
|
|
40
|
-
count: { type: 'integer' },
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
output: {
|
|
44
|
-
encoding: 'application/json',
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
lexicon: 1,
|
|
51
|
-
id: 'io.example.sharedLimitOne',
|
|
52
|
-
defs: {
|
|
53
|
-
main: {
|
|
54
|
-
type: 'query',
|
|
55
|
-
parameters: {
|
|
56
|
-
type: 'params',
|
|
57
|
-
required: ['points'],
|
|
58
|
-
properties: {
|
|
59
|
-
points: { type: 'integer' },
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
output: {
|
|
63
|
-
encoding: 'application/json',
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
lexicon: 1,
|
|
70
|
-
id: 'io.example.sharedLimitTwo',
|
|
71
|
-
defs: {
|
|
72
|
-
main: {
|
|
73
|
-
type: 'query',
|
|
74
|
-
parameters: {
|
|
75
|
-
type: 'params',
|
|
76
|
-
required: ['points'],
|
|
77
|
-
properties: {
|
|
78
|
-
points: { type: 'integer' },
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
output: {
|
|
82
|
-
encoding: 'application/json',
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
lexicon: 1,
|
|
89
|
-
id: 'io.example.toggleLimit',
|
|
90
|
-
defs: {
|
|
91
|
-
main: {
|
|
92
|
-
type: 'query',
|
|
93
|
-
parameters: {
|
|
94
|
-
type: 'params',
|
|
95
|
-
properties: {
|
|
96
|
-
shouldCount: { type: 'boolean' },
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
output: {
|
|
100
|
-
encoding: 'application/json',
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
lexicon: 1,
|
|
107
|
-
id: 'io.example.noLimit',
|
|
108
|
-
defs: {
|
|
109
|
-
main: {
|
|
110
|
-
type: 'query',
|
|
111
|
-
output: {
|
|
112
|
-
encoding: 'application/json',
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
lexicon: 1,
|
|
119
|
-
id: 'io.example.nonExistent',
|
|
120
|
-
defs: {
|
|
121
|
-
main: {
|
|
122
|
-
type: 'query',
|
|
123
|
-
output: {
|
|
124
|
-
encoding: 'application/json',
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
]
|
|
130
|
-
|
|
131
|
-
describe('Parameters', () => {
|
|
132
|
-
let s: http.Server
|
|
133
|
-
const server = xrpcServer.createServer(LEXICONS, {
|
|
134
|
-
rateLimits: {
|
|
135
|
-
creator: (opts) => new MemoryRateLimiter(opts),
|
|
136
|
-
bypass: ({ req }) => req.headers['x-ratelimit-bypass'] === 'bypass',
|
|
137
|
-
shared: [
|
|
138
|
-
{
|
|
139
|
-
name: 'shared-limit',
|
|
140
|
-
durationMs: 5 * MINUTE,
|
|
141
|
-
points: 6,
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
global: [
|
|
145
|
-
{
|
|
146
|
-
name: 'global-ip',
|
|
147
|
-
durationMs: 5 * MINUTE,
|
|
148
|
-
points: 100,
|
|
149
|
-
},
|
|
150
|
-
],
|
|
151
|
-
},
|
|
152
|
-
})
|
|
153
|
-
server.method('io.example.routeLimit', {
|
|
154
|
-
rateLimit: {
|
|
155
|
-
durationMs: 5 * MINUTE,
|
|
156
|
-
points: 5,
|
|
157
|
-
calcKey: ({ params }) => params.str as string,
|
|
158
|
-
},
|
|
159
|
-
handler: (ctx) => ({
|
|
160
|
-
encoding: 'json',
|
|
161
|
-
body: ctx.params,
|
|
162
|
-
}),
|
|
163
|
-
})
|
|
164
|
-
server.method('io.example.routeLimitReset', {
|
|
165
|
-
rateLimit: {
|
|
166
|
-
durationMs: 5 * MINUTE,
|
|
167
|
-
points: 2,
|
|
168
|
-
},
|
|
169
|
-
handler: (ctx) => {
|
|
170
|
-
if (ctx.params.count === 1) {
|
|
171
|
-
ctx.resetRouteRateLimits()
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
encoding: 'json',
|
|
176
|
-
body: {},
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
})
|
|
180
|
-
server.method('io.example.sharedLimitOne', {
|
|
181
|
-
rateLimit: {
|
|
182
|
-
name: 'shared-limit',
|
|
183
|
-
calcPoints: ({ params }) => params.points as number,
|
|
184
|
-
},
|
|
185
|
-
handler: (ctx) => ({
|
|
186
|
-
encoding: 'json',
|
|
187
|
-
body: ctx.params,
|
|
188
|
-
}),
|
|
189
|
-
})
|
|
190
|
-
server.method('io.example.sharedLimitTwo', {
|
|
191
|
-
rateLimit: {
|
|
192
|
-
name: 'shared-limit',
|
|
193
|
-
calcPoints: ({ params }) => params.points as number,
|
|
194
|
-
},
|
|
195
|
-
handler: (ctx) => ({
|
|
196
|
-
encoding: 'json',
|
|
197
|
-
body: ctx.params,
|
|
198
|
-
}),
|
|
199
|
-
})
|
|
200
|
-
server.method('io.example.toggleLimit', {
|
|
201
|
-
rateLimit: [
|
|
202
|
-
{
|
|
203
|
-
durationMs: 5 * MINUTE,
|
|
204
|
-
points: 5,
|
|
205
|
-
calcPoints: ({ params }) => (params.shouldCount ? 1 : 0),
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
durationMs: 5 * MINUTE,
|
|
209
|
-
points: 10,
|
|
210
|
-
},
|
|
211
|
-
],
|
|
212
|
-
handler: (ctx) => ({
|
|
213
|
-
encoding: 'json',
|
|
214
|
-
body: ctx.params,
|
|
215
|
-
}),
|
|
216
|
-
})
|
|
217
|
-
server.method('io.example.noLimit', {
|
|
218
|
-
handler: () => ({
|
|
219
|
-
encoding: 'json',
|
|
220
|
-
body: {},
|
|
221
|
-
}),
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
let client: XrpcClient
|
|
225
|
-
beforeAll(async () => {
|
|
226
|
-
s = await createServer(server)
|
|
227
|
-
const { port } = s.address() as AddressInfo
|
|
228
|
-
client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
|
|
229
|
-
})
|
|
230
|
-
afterAll(async () => {
|
|
231
|
-
await closeServer(s)
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
it('rate limits a given route', async () => {
|
|
235
|
-
const makeCall = () => client.call('io.example.routeLimit', { str: 'test' })
|
|
236
|
-
for (let i = 0; i < 5; i++) {
|
|
237
|
-
await makeCall()
|
|
238
|
-
}
|
|
239
|
-
await expect(makeCall).rejects.toThrow('Rate Limit Exceeded')
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('can reset route rate limits', async () => {
|
|
243
|
-
// Limit is 2.
|
|
244
|
-
// Call 0 is OK (1/2).
|
|
245
|
-
// Call 1 is OK (2/2), and resets the limit.
|
|
246
|
-
// Call 2 is OK (1/2).
|
|
247
|
-
// Call 3 is OK (2/2).
|
|
248
|
-
for (let i = 0; i < 4; i++) {
|
|
249
|
-
await client.call('io.example.routeLimitReset', { count: i })
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Call 4 exceeds the limit (3/2).
|
|
253
|
-
await expect(
|
|
254
|
-
client.call('io.example.routeLimitReset', { count: 4 }),
|
|
255
|
-
).rejects.toThrow('Rate Limit Exceeded')
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('rate limits on a shared route', async () => {
|
|
259
|
-
await client.call('io.example.sharedLimitOne', { points: 1 })
|
|
260
|
-
await client.call('io.example.sharedLimitTwo', { points: 1 })
|
|
261
|
-
await client.call('io.example.sharedLimitOne', { points: 2 })
|
|
262
|
-
await client.call('io.example.sharedLimitTwo', { points: 2 })
|
|
263
|
-
await expect(
|
|
264
|
-
client.call('io.example.sharedLimitOne', { points: 1 }),
|
|
265
|
-
).rejects.toThrow('Rate Limit Exceeded')
|
|
266
|
-
await expect(
|
|
267
|
-
client.call('io.example.sharedLimitTwo', { points: 1 }),
|
|
268
|
-
).rejects.toThrow('Rate Limit Exceeded')
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
it('applies multiple rate-limits', async () => {
|
|
272
|
-
const makeCall = (shouldCount: boolean) =>
|
|
273
|
-
client.call('io.example.toggleLimit', { shouldCount })
|
|
274
|
-
for (let i = 0; i < 5; i++) {
|
|
275
|
-
await makeCall(true)
|
|
276
|
-
}
|
|
277
|
-
await expect(() => makeCall(true)).rejects.toThrow('Rate Limit Exceeded')
|
|
278
|
-
for (let i = 0; i < 4; i++) {
|
|
279
|
-
await makeCall(false)
|
|
280
|
-
}
|
|
281
|
-
await expect(() => makeCall(false)).rejects.toThrow('Rate Limit Exceeded')
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
it('applies global limits', async () => {
|
|
285
|
-
const makeCall = () => client.call('io.example.noLimit')
|
|
286
|
-
const calls: Promise<unknown>[] = []
|
|
287
|
-
for (let i = 0; i < 110; i++) {
|
|
288
|
-
calls.push(makeCall())
|
|
289
|
-
}
|
|
290
|
-
await expect(Promise.all(calls)).rejects.toThrow('Rate Limit Exceeded')
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('applies global limits to xrpc catchall', async () => {
|
|
294
|
-
const makeCall = () => client.call('io.example.nonExistent')
|
|
295
|
-
await expect(makeCall()).rejects.toThrow('Rate Limit Exceeded')
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
it('can bypass rate limits', async () => {
|
|
299
|
-
const makeCall = () =>
|
|
300
|
-
client.call(
|
|
301
|
-
'io.example.noLimit',
|
|
302
|
-
{},
|
|
303
|
-
{},
|
|
304
|
-
{ headers: { 'X-RateLimit-Bypass': 'bypass' } },
|
|
305
|
-
)
|
|
306
|
-
const calls: Promise<unknown>[] = []
|
|
307
|
-
for (let i = 0; i < 110; i++) {
|
|
308
|
-
calls.push(makeCall())
|
|
309
|
-
}
|
|
310
|
-
await Promise.all(calls)
|
|
311
|
-
})
|
|
312
|
-
})
|
package/tests/responses.test.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import * as http from 'node:http'
|
|
2
|
-
import { AddressInfo } from 'node:net'
|
|
3
|
-
import { byteIterableToStream } from '@atproto/common'
|
|
4
|
-
import { LexiconDoc } from '@atproto/lexicon'
|
|
5
|
-
import { XrpcClient } from '@atproto/xrpc'
|
|
6
|
-
import * as xrpcServer from '../src/index.js'
|
|
7
|
-
import { closeServer, createServer } from './_util.js'
|
|
8
|
-
|
|
9
|
-
const LEXICONS: LexiconDoc[] = [
|
|
10
|
-
{
|
|
11
|
-
lexicon: 1,
|
|
12
|
-
id: 'io.example.readableStream',
|
|
13
|
-
defs: {
|
|
14
|
-
main: {
|
|
15
|
-
type: 'query',
|
|
16
|
-
parameters: {
|
|
17
|
-
type: 'params',
|
|
18
|
-
properties: {
|
|
19
|
-
shouldErr: { type: 'boolean' },
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
output: {
|
|
23
|
-
encoding: 'application/vnd.ipld.car',
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
describe('Responses', () => {
|
|
31
|
-
let s: http.Server
|
|
32
|
-
const server = xrpcServer.createServer(LEXICONS)
|
|
33
|
-
server.method('io.example.readableStream', async (ctx) => {
|
|
34
|
-
async function* iter(): AsyncIterable<Uint8Array> {
|
|
35
|
-
for (let i = 0; i < 5; i++) {
|
|
36
|
-
yield new Uint8Array([i])
|
|
37
|
-
}
|
|
38
|
-
if (ctx.params.shouldErr) {
|
|
39
|
-
throw new Error('error')
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
encoding: 'application/vnd.ipld.car',
|
|
44
|
-
body: byteIterableToStream(iter()),
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
let client: XrpcClient
|
|
49
|
-
beforeAll(async () => {
|
|
50
|
-
s = await createServer(server)
|
|
51
|
-
const { port } = s.address() as AddressInfo
|
|
52
|
-
client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
|
|
53
|
-
})
|
|
54
|
-
afterAll(async () => {
|
|
55
|
-
await closeServer(s)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('returns readable streams of bytes', async () => {
|
|
59
|
-
const res = await client.call('io.example.readableStream', {
|
|
60
|
-
shouldErr: false,
|
|
61
|
-
})
|
|
62
|
-
const expected = new Uint8Array([0, 1, 2, 3, 4])
|
|
63
|
-
expect(res.data).toEqual(expected)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('handles errs on readable streams of bytes', async () => {
|
|
67
|
-
const attempt = client.call('io.example.readableStream', {
|
|
68
|
-
shouldErr: true,
|
|
69
|
-
})
|
|
70
|
-
await expect(attempt).rejects.toThrow()
|
|
71
|
-
})
|
|
72
|
-
})
|
package/tests/stream.test.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { once } from 'node:events'
|
|
2
|
-
import * as http from 'node:http'
|
|
3
|
-
import { AddressInfo } from 'node: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/index.js'
|
|
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
|
-
})
|